diff --git a/Examples/Plugins/HioImage/hioPpm_Assets/example.usda b/Examples/Plugins/HioImage/hioPpm_Assets/example.usda new file mode 100644 index 0000000000..a041f1e11a --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Assets/example.usda @@ -0,0 +1,52 @@ +#usda 1.0 +( + upAxis = "Y" +) + +def Mesh "MyMesh" ( + prepend apiSchemas = ["MaterialBindingAPI"] +) +{ + float3[] extent = [(-3, -1, 0), (3, 1, 0)] + int[] faceVertexCounts = [4] + int[] faceVertexIndices = [0, 1, 2, 3] + rel material:binding = + point3f[] points = [(-3, -1, 0), (3, -1, 0), (3, 1, 0), (-3, 1, 0)] + texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] ( + interpolation = "vertex" + ) + uniform token subdivisionScheme = "none" + + def Material "MyMat" + { + token outputs:surface.connect = + + def Shader "MyPreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor.connect = <../diffuseTexture.outputs:rgb> + float inputs:metallic = 0 + float inputs:roughness = 1 + token outputs:surface + } + + def Shader "stReader" + { + uniform token info:id = "UsdPrimvarReader_float2" + string inputs:varname = "st" + float2 outputs:result + } + + def Shader "diffuseTexture" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file = @myImage.ppm@ + float2 inputs:st.connect = <../stReader.outputs:result> + token inputs:wrapS = "clamp" + token inputs:wrapT = "clamp" + token inputs:magFilter = "nearest" + token inputs:minFilter = "nearest" + float3 outputs:rgb + } + } +} \ No newline at end of file diff --git a/Examples/Plugins/HioImage/hioPpm_Assets/example.usdz b/Examples/Plugins/HioImage/hioPpm_Assets/example.usdz new file mode 100644 index 0000000000..196da45aab Binary files /dev/null and b/Examples/Plugins/HioImage/hioPpm_Assets/example.usdz differ diff --git a/Examples/Plugins/HioImage/hioPpm_Assets/myImage.ppm b/Examples/Plugins/HioImage/hioPpm_Assets/myImage.ppm new file mode 100644 index 0000000000..73c5f927d9 --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Assets/myImage.ppm @@ -0,0 +1,16 @@ +P3 +# "P3" means this is a RGB color image in ASCII +# "3 2" is the width and height of the image in pixels +# "255" is the maximum value for each color +# This, up through the "255" line below are the header. +# Everything after that is the image data: RGB triplets. +# In order: red, green, blue, yellow, white, and black. +3 2 +255 +255 0 0 + 0 255 0 + 0 0 255 +255 255 0 +255 255 255 + 0 0 0 + \ No newline at end of file diff --git a/Examples/Plugins/HioImage/hioPpm_Cxx/Package.swift b/Examples/Plugins/HioImage/hioPpm_Cxx/Package.swift new file mode 100644 index 0000000000..29d1bcd592 --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Cxx/Package.swift @@ -0,0 +1,49 @@ +// swift-tools-version: 6.1 +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + + +import PackageDescription + +let package = Package( + name: "hioPpm", + platforms: [.macOS(.v14), .iOS(.v17)], + products: [ + .library( + name: "hioPpm", + targets: ["hioPpm"] + ), + ], + dependencies: [ + .package(path: "SwiftUsd") + // We use a symlink to the SwiftUsd repo for this example, + // but typically you'd do something like this: + // .package(url: "https://github.com/apple/SwiftUsd", from: "8.0.0"), + ], + targets: [ + .target( + name: "hioPpm", + dependencies: [.product(name: "OpenUSD", package: "SwiftUsd")], + resources: [.copy("plugInfo.json")], + plugins: [.plugin(name: "generate-plug-info-json", package: "SwiftUsd")], + ), + ], + cxxLanguageStandard: .gnucxx17 +) diff --git a/Examples/Plugins/HioImage/hioPpm_Cxx/README.md b/Examples/Plugins/HioImage/hioPpm_Cxx/README.md new file mode 100644 index 0000000000..98db0b187e --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Cxx/README.md @@ -0,0 +1,23 @@ +# HioImage PPM plugin (C++) + +This package is an example of how to write an HioImage plugin in C++. + +## Key details +- In `plugInfo.json`, we use `@PLUG_INFO_LIBRARY_PATH@`, `@PLUG_INFO_RESOURCE_PATH@`, and `@PLUG_INFO_ROOT@` to allow our plugin to be used in both SwiftUsd-based apps and vanilla OpenUSD-based runtimes. +- In `Package.swift`, we use `.copy("plugInfo.json")` and `.plugin(name: "generate-plug-info-json", package: "SwiftUsd")` to have the build system fill in the `@PLUG_INFO_*@` values based on the target architecture +- In `hioPpm.h`, we subclass from `pxr::HioImage` and implement its pure-virtual methods. +- In `hioPpm.cpp`, we use the `TF_REGISTRY_FUNCTION` macro along with `pxr::TfType::Define` and `pxr::TfType::SetFactory` to tell the OpenUSD runtime how to use our custom plugin. + +## Usage +To use this plugin in a SwiftUsd-based app: +1. Add it as a package dependency to your Swift Package/Xcode project. + +To use this plugin in a vanilla OpenUSD-based app: +1. `cd` into the package directory +2. Run `swift build` +3. Run `swift package build-vanilla-openusd-plugin` and note the outputed plugin path +4. Set the `PXR_PLUGINPATH_NAME` environment variable to point to your plugin. For example: +``` +export PXR_PLUGINPATH_NAME="/Users/maddyadams/SwiftUsd/Examples/HioImage/hioPpm_Cxx/.build/plugins/build-vanilla-openusd-plugin/outputs/hioPpm.usdplugin/:$PXR_PLUGINPATH_NAME" +usdview example.usda +``` diff --git a/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/PpmImage.cpp b/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/PpmImage.cpp new file mode 100644 index 0000000000..230fd740d2 --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/PpmImage.cpp @@ -0,0 +1,237 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + + +#include "PpmImage.h" +#include "Diagnostics.h" +#include "pxr/base/tf/diagnostic.h" +#include "pxr/usd/ar/resolver.h" +#include "pxr/usd/ar/writableAsset.h" +#include "pxr/usd/ar/asset.h" + +#include + +/* static */ +std::unique_ptr PpmImage::createForReading(const std::string &filename) { + std::shared_ptr asset = pxr::ArGetResolver().OpenAsset(pxr::ArResolvedPath(filename)); + if (!asset) { + MY_RUNTIME_ERROR("PpmImage createForReading failed during ArResolver::OpenAsset '" + filename + "'"); + return {}; + } + + std::optional _width = std::nullopt; + std::optional _height = std::nullopt; + std::optional _componentMaximum = std::nullopt; + std::vector _storage; + + // Parse the file contents + std::shared_ptr bufSharedPtr = asset->GetBuffer(); + const char* buf = bufSharedPtr.get(); + size_t index = 0; + while (index < asset->GetSize()) { + // Check for the magic number at the start of the file + if (index == 0) { + if (buf[index] != 'P') { + MY_RUNTIME_ERROR("Invalid PPM magic number"); + return {}; + } else { + index += 1; + continue; + } + } + + if (index == 1) { + if (buf[index] != '3') { + MY_RUNTIME_ERROR("Invalid PPM magic number"); + return {}; + } else { + index += 1; + continue; + } + } + + // Line comments are ignored + if (buf[index] == '#' && index > 0 && buf[index - 1] == '\n') { + while (index < asset->GetSize() && buf[index] != '\n') { + index += 1; + } + continue; + } + + // Whitespace delimits values + if (isspace(buf[index])) { + while (index < asset->GetSize() && isspace(buf[index])) { + index += 1; + } + continue; + } + + // Handle numbers in the file + if (isdigit(buf[index])) { + std::string digitBuffer; + while (index < asset->GetSize() && isdigit(buf[index])) { + digitBuffer += buf[index]; + index += 1; + } + + uint16_t parsedInt; + try { + std::int32_t x = std::stoi(digitBuffer); + if (x < 0 || x > static_cast(std::numeric_limits::max())) { + MY_RUNTIME_ERROR("Invalid PPM number (out of range)"); + return {}; + } + parsedInt = static_cast(x); + } catch (...) { + MY_RUNTIME_ERROR("Invalid PPM number (failed to convert)"); + return {}; + } + + if (!_width.has_value()) { + _width = parsedInt; + if (*_width == 0) { + MY_RUNTIME_ERROR("Invalid PPM width"); + return {}; + } + continue; + } + + if (!_height.has_value()) { + _height = parsedInt; + if (*_height == 0) { + MY_RUNTIME_ERROR("Invalid PPM height"); + return {}; + } + continue; + } + + if (!_componentMaximum.has_value()) { + _componentMaximum = parsedInt; + if (*_componentMaximum == 0) { + MY_RUNTIME_ERROR("Invalid PPM component maximum"); + return {}; + } + continue; + } + + if (parsedInt > *_componentMaximum) { + MY_RUNTIME_ERROR("Invalid PPM, raster component greater than component maximum"); + return {}; + } + _storage.push_back(parsedInt); + if (_storage.size() > 3 * (*_width) * (*_height)) { + MY_RUNTIME_ERROR("Invalid PPM, raster larger than expected"); + return {}; + } + + continue; + } + + // We've handled comments, whitespace, and numbers, so + // anything else must be an invalid character in a single-image PPM file + MY_RUNTIME_ERROR("Invalid PPM file contents"); + return {}; + } + + if (!_width.has_value() || !_height.has_value() || !_componentMaximum.has_value()) { + MY_RUNTIME_ERROR("Invalid PPM file header"); + return {}; + } + if (_storage.size() != 3 * (*_width) * (*_height)) { + MY_RUNTIME_ERROR("Invalid PPM, raster size is unexpected"); + return {}; + } + + std::unique_ptr result = std::unique_ptr(new PpmImage()); + result->_width = *_width; + result->_height = *_height; + result->_componentMaximum = *_componentMaximum; + result->_storage = std::move(_storage); + + return result; +} + +/* static */ +std::unique_ptr PpmImage::createForWriting(uint16_t width, uint16_t height, uint16_t componentMaximum) { + std::unique_ptr _result = std::unique_ptr(new PpmImage()); + _result->_width = width; + _result->_height = height; + _result->_componentMaximum = componentMaximum; + _result->_storage.resize(width * height * 3); + return _result; +} + +bool PpmImage::isTwoBytesPerComponent() const { + return _componentMaximum >= 256; +} + +uint16_t PpmImage::getPixel(uint16_t x, uint16_t y, int channel) const { + size_t index = _toIndex(x, y, channel); + if (index == -1) { return 0; } + return _storage[index]; +} + +void PpmImage::setPixel(uint16_t x, uint16_t y, int channel, uint16_t value) { + size_t index = _toIndex(x, y, channel); + if (index == -1) { return; } + if (value > _componentMaximum) { + MY_CODING_ERROR("Cannot PpmImage::setPixel to a value greater than the component maximum"); + return; + } + _storage[index] = value; +} + +void PpmImage::write(const std::string &filename) { + std::shared_ptr asset = pxr::ArGetResolver().OpenAssetForWrite(pxr::ArResolvedPath(filename), pxr::ArResolver::WriteMode::Replace); + if (!asset) { + MY_RUNTIME_ERROR("PpmImage write failed during ArResolver::OpenAssetForWrite '" + filename + "'"); + return; + } + + std::stringstream ss; + ss << "P3\n"; + ss << _width << " " << _height << "\n"; + ss << _componentMaximum << "\n"; + for (size_t i = 0; i < _storage.size(); i += 3) { + ss << _storage[i] << " " << _storage[i + 1] << " " << _storage[i + 2] << "\n"; + } + + std::string s = ss.str(); + + asset->Write(s.c_str(), s.size(), 0); + MY_VERIFY(asset->Close()); +} + +uint16_t PpmImage::getWidth() const { return _width; } + +uint16_t PpmImage::getHeight() const { return _height; } + +uint16_t PpmImage::getComponentMaximum() const { return _componentMaximum; } + +size_t PpmImage::_toIndex(uint16_t x, uint16_t y, int channel) const { + if (x >= _width || y >= _height || channel >= 3 || channel < 0) { + MY_CODING_ERROR("Invalid coordinates or channel for PpmImage"); + return -1; + } + + return y * 3 * _width + 3 * x + channel; +} + +PpmImage::PpmImage() {} diff --git a/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/hioPpm.cpp b/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/hioPpm.cpp new file mode 100644 index 0000000000..376f2372d2 --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/hioPpm.cpp @@ -0,0 +1,237 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + + +#include "hioPpm.h" +#include "PpmImage.h" +#include "Diagnostics.h" + +/// We use `TF_REGISTRY_FUNCTION` and `pxr::TfType` to +/// register this plugin with the OpenUSD runtime. +namespace { + // Like the TfDiagnostic macros, TF_REGISTRY_FUNCTION expects + // to be within PXR_NAMESPACE_USING_DIRECTIVE or PXR_NAMESPACE_OPEN_SCOPE. + PXR_NAMESPACE_USING_DIRECTIVE; + TF_REGISTRY_FUNCTION(TfType) { + pxr::TfType t = pxr::TfType::Define>(); + t.SetFactory>(); + } +} + +uint16_t extract_uint6_t(void* data, size_t index, pxr::HioType hioType); + +bool HioPpm_Image::Read(const HioImage::StorageSpec &storage) { + return ReadCropped(0, 0, 0, 0, storage); +} + +bool HioPpm_Image::ReadCropped(const int cropTop, const int cropBottom, const int cropLeft, const int cropRight, const HioImage::StorageSpec &storage) { + if (cropLeft < 0 || cropRight < 0 || cropTop < 0 || cropBottom < 0 ) { + MY_CODING_ERROR("Cannot ReadCropped with negative crop amounts"); + return false; + } + + if (cropLeft + cropRight >= GetWidth() || cropTop + cropBottom >= GetHeight()) { + MY_CODING_ERROR("Cannot ReadCropped with crop amounts exceeding image size"); + return false; + } + + for (int y = 0; y < storage.height; y++) { + if (y + cropTop >= GetHeight() - cropBottom) { continue; } + for (int x = 0; x < storage.width; x++) { + if (x + cropLeft >= GetWidth() - cropRight) { continue; } + for (int c = 0; c < 3; c++) { + uint16_t value = _ppmImage->getPixel(x + cropLeft, y + cropTop, c); + int maybeFlippedY = storage.flipped ? storage.height - y - 1 : y; + size_t index = maybeFlippedY * 3 * GetWidth() + 3 * x + c; + if (_ppmImage->isTwoBytesPerComponent()) { + reinterpret_cast(storage.data)[index] = value; + } else { + reinterpret_cast(storage.data)[index] = static_cast(value); + } + } + } + } + + return true; +} + +bool HioPpm_Image::Write(const HioImage::StorageSpec &storage, VtDictionary const& metadata) { + pxr::HioType hioType = pxr::HioGetHioType(storage.format); + int componentCount = pxr::HioGetComponentCount(storage.format); + + uint16_t maxValue; + if (hioType == pxr::HioTypeUnsignedByte || hioType == pxr::HioTypeUnsignedByteSRGB) { + maxValue = std::numeric_limits::max(); + } else { + maxValue = std::numeric_limits::max(); + } + + _ppmImage = PpmImage::createForWriting(storage.width, storage.height, maxValue); + + for (int y = 0; y < storage.height; y++) { + for (int x = 0; x < storage.width; x++) { + for (int c = 0; c < std::min(3, componentCount); c++) { + int maybeFlippedY = storage.flipped ? storage.height - y - 1 : y; + size_t index = maybeFlippedY * storage.width * componentCount + x * componentCount + c; + + _ppmImage->setPixel(x, y, c, extract_uint6_t(storage.data, index, hioType)); + } + } + } + + _ppmImage->write(_filename); + return true; +} + +std::string const& HioPpm_Image::GetFilename() const { + return _filename; +} + +int HioPpm_Image::GetWidth() const { + if (!_ppmImage) { return 0; } + return _ppmImage->getWidth(); +} + +int HioPpm_Image::GetHeight() const { + if (!_ppmImage) { return 0; } + return _ppmImage->getHeight(); +} + +HioFormat HioPpm_Image::GetFormat() const { + if (!_ppmImage) { + MY_CODING_ERROR("Invalid image"); + return HioFormatUInt16Vec3; + } + + if (_ppmImage->isTwoBytesPerComponent()) { + return HioFormatUInt16Vec3; + } else { + return HioFormatUNorm8Vec3srgb; + } +} + +int HioPpm_Image::GetBytesPerPixel() const { + if (!_ppmImage) { return 0; } + return _ppmImage->isTwoBytesPerComponent() ? 6 : 3; +} + +int HioPpm_Image::GetNumMipLevels() const { + return 1; +} + +bool HioPpm_Image::IsColorSpaceSRGB() const { + return true; +} + +bool HioPpm_Image::GetMetadata(const TfToken &key, VtValue *value) const { + return false; +} + +bool HioPpm_Image::GetSamplerMetadata(HioAddressDimension dim, HioAddressMode *param) const { + return false; +} + +bool HioPpm_Image::_OpenForReading(const std::string &filename, int subimage, int mip, SourceColorSpace sourceColorSpace, bool suppressErrors) { + _filename = filename; + _ppmImage = PpmImage::createForReading(filename); + if (!_ppmImage && !suppressErrors) { + return false; + } + return true; +} + +bool HioPpm_Image::_OpenForWriting(const std::string &filename) { + _filename = filename; + _ppmImage = nullptr; + return true; +} + + +/// Utility function from extracting a uint16_t from a buffer of `hioType` values +uint16_t extract_uint6_t(void* data, size_t index, pxr::HioType hioType) { + switch (hioType) { + case HioTypeUnsignedByte: + { + uint8_t raw = reinterpret_cast(data)[index]; + return static_cast(raw); + } + case HioTypeUnsignedByteSRGB: + { + uint8_t raw = reinterpret_cast(data)[index]; + return static_cast(raw); + } + case HioTypeSignedByte: + { + int8_t raw = reinterpret_cast(data)[index]; + if (raw < 0) { raw = 0; } + return static_cast(raw); + } + case HioTypeUnsignedShort: + { + uint16_t raw = reinterpret_cast(data)[index]; + return raw; + } + case HioTypeSignedShort: + { + int16_t raw = reinterpret_cast(data)[index]; + if (raw < 0) { raw = 0; } + return static_cast(raw); + } + case HioTypeUnsignedInt: + { + uint32_t raw = reinterpret_cast(data)[index]; + raw = std::min(raw, static_cast(std::numeric_limits::max())); + return static_cast(raw); + } + case HioTypeInt: + { + int32_t raw = reinterpret_cast(data)[index]; + if (raw < 0) { raw = 0; } + raw = std::min(raw, static_cast(std::numeric_limits::max())); + return static_cast(raw); + } + case HioTypeHalfFloat: + { + double raw = static_cast(reinterpret_cast(data)[index]); + raw *= static_cast(std::numeric_limits::max()); + if (raw < 0) { raw = 0; } + raw = std::min(raw, static_cast(std::numeric_limits::max())); + return static_cast(raw); + } + case HioTypeFloat: + { + double raw = static_cast(reinterpret_cast(data)[index]); + raw *= static_cast(std::numeric_limits::max()); + if (raw < 0) { raw = 0; } + raw = std::min(raw, static_cast(std::numeric_limits::max())); + return static_cast(raw); + } + case HioTypeDouble: + { + double raw = reinterpret_cast(data)[index]; + raw *= static_cast(std::numeric_limits::max()); + if (raw < 0) { raw = 0; } + raw = std::min(raw, static_cast(std::numeric_limits::max())); + return static_cast(raw); + } + case HioTypeCount: + return static_cast(0); + } +} diff --git a/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/include/Diagnostics.h b/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/include/Diagnostics.h new file mode 100644 index 0000000000..252a4091c9 --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/include/Diagnostics.h @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + + +#ifndef HIOPPM_DIAGNOSTICS_H +#define HIOPPM_DIAGNOSTICS_H + +#include "pxr/base/tf/diagnostic.h" + +// TfDiagnostic macros expect to be within `PXR_NAMESPACE_USING_DIRECTIVE` or `PXR_NAMESPACE_OPEN_SCOPE`, +// but we don't want to have to repeat those everywhere, so expose our own version of the macros + +#define MY_RUNTIME_ERROR(s) { PXR_NAMESPACE_USING_DIRECTIVE; TF_RUNTIME_ERROR(s); } +#define MY_CODING_ERROR(s) { PXR_NAMESPACE_USING_DIRECTIVE; TF_CODING_ERROR(s); } +#define MY_VERIFY(s) { PXR_NAMESPACE_USING_DIRECTIVE; TF_VERIFY(s); } + +#endif // HIOPPM_DIAGNOSTICS_H diff --git a/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/include/PpmImage.h b/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/include/PpmImage.h new file mode 100644 index 0000000000..3d3aad9152 --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/include/PpmImage.h @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + + +#ifndef HIOPPM_PPMIMAGE_H +#define HIOPPM_PPMIMAGE_H + +#include +#include +#include +#include + +/// Class for reading and writing PPM images +class PpmImage { +public: + static std::unique_ptr createForReading(std::string const& filename); + static std::unique_ptr createForWriting(uint16_t width, uint16_t height, uint16_t componentMaximum); + + bool isTwoBytesPerComponent() const; + uint16_t getPixel(uint16_t x, uint16_t y, int channel) const; + void setPixel(uint16_t x, uint16_t y, int channel, uint16_t value); + + void write(std::string const& filename); + + uint16_t getWidth() const; + uint16_t getHeight() const; + uint16_t getComponentMaximum() const; + + PpmImage(const PpmImage&) = delete; + PpmImage& operator=(const PpmImage&) = delete; + +private: + PpmImage(); + + size_t _toIndex(uint16_t x, uint16_t y, int channel) const; + + uint16_t _width; + uint16_t _height; + uint16_t _componentMaximum; + std::vector _storage; +}; + +#endif // HIOPPM_PPMIMAGE_H diff --git a/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/include/hioPpm.h b/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/include/hioPpm.h new file mode 100644 index 0000000000..ba33e4755d --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/include/hioPpm.h @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +#ifndef HIOPPM_HIOPPM_H +#define HIOPPM_HIOPPM_H + +#include "pxr/imaging/hio/image.h" + +class PpmImage; + +/// The plugin entry point for this plugin. We subclass from +/// `pxr::HioImage` to implement an HioImage plugin, and in +/// `hioPpm.cpp`, we register this plugin with the OpenUSD runtime. +/// This class handles HioImage concerns and delegates actually +/// working with the PPM image file format to the ``PpmImage`` class. +class HioPpm_Image final: public pxr::HioImage { +public: + using Base = HioImage; + + HioPpm_Image() = default; + ~HioPpm_Image() = default; + + bool Read(pxr::HioImage::StorageSpec const& storage) override; + + bool ReadCropped(int const cropTop, + int const cropBottom, + int const cropLeft, + int const cropRight, + pxr::HioImage::StorageSpec const& storage) override; + + bool Write(pxr::HioImage::StorageSpec const& storage, + pxr::VtDictionary const& metadata = pxr::VtDictionary()) override; + + std::string const& GetFilename() const override; + + int GetWidth() const override; + + int GetHeight() const override; + + pxr::HioFormat GetFormat() const override; + + int GetBytesPerPixel() const override; + + int GetNumMipLevels() const override; + + bool IsColorSpaceSRGB() const override; + + bool GetMetadata(pxr::TfToken const & key, pxr::VtValue * value) const override; + + bool GetSamplerMetadata(pxr::HioAddressDimension dim, + pxr::HioAddressMode * param) const override; + +protected: + bool _OpenForReading(std::string const & filename, + int subimage, + int mip, + pxr::HioImage::SourceColorSpace sourceColorSpace, + bool suppressErrors) override; + + bool _OpenForWriting(std::string const & filename) override; + +private: + std::string _filename; + std::unique_ptr _ppmImage; +}; + +#endif // HIOPPM_HIOPPM_H diff --git a/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/plugInfo.json b/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/plugInfo.json new file mode 100644 index 0000000000..9255f0bebb --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Cxx/Sources/hioPpm/plugInfo.json @@ -0,0 +1,20 @@ +{ + "Plugins": [ + { + "Info": { + "Types": { + "HioPpm_Image" : { + "bases" : ["HioImage"], + "imageTypes": ["ppm"], + "precedence": 15 + } + } + }, + "Name": "hioPpm", + "LibraryPath": "@PLUG_INFO_LIBRARY_PATH@", + "ResourcePath": "@PLUG_INFO_RESOURCE_PATH@", + "Root": "@PLUG_INFO_ROOT@", + "Type": "library" + } + ] +} diff --git a/Examples/Plugins/HioImage/hioPpm_Cxx/SwiftUsd b/Examples/Plugins/HioImage/hioPpm_Cxx/SwiftUsd new file mode 120000 index 0000000000..c866b86874 --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Cxx/SwiftUsd @@ -0,0 +1 @@ +../../../.. \ No newline at end of file diff --git a/Examples/Plugins/HioImage/hioPpm_Swift/Package.swift b/Examples/Plugins/HioImage/hioPpm_Swift/Package.swift new file mode 100644 index 0000000000..261b6ea416 --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Swift/Package.swift @@ -0,0 +1,67 @@ +// swift-tools-version: 6.1 +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +import PackageDescription + +let package = Package( + name: "hioPpm", + platforms: [.macOS(.v14), .iOS(.v17)], + products: [ + .library( + name: "hioPpm", + targets: ["hioPpm"] + ), + ], + dependencies: [ + .package(path: "SwiftUsd") + // We use a symlink to the SwiftUsd repo for this example, + // but typically you'd do something like this: + // .package(url: "https://github.com/apple/SwiftUsd", from: "8.0.0"), + ], + targets: [ + .target( + name: "hioPpm", + dependencies: [ + .product(name: "OpenUSD", package: "SwiftUsd") + ], + resources: [ + .copy("plugInfo.json") + ], + swiftSettings: [ + .interoperabilityMode(.Cxx), + ] + enableExperimentalFeature_SymbolLinkageMarkers_ifNeeded(), + plugins: [ + .plugin(name: "generate-plug-info-json", package: "SwiftUsd") + ], + ), + ], + cxxLanguageStandard: .gnucxx17 +) + +// Swift 6.3 adds the `@used` and `@section` attributes, but Swift 6.1 and Swift 6.2 +// can use the `@_used` and `@_section` underscored attributes with `SymbolLinkageMarkers` enabled +func enableExperimentalFeature_SymbolLinkageMarkers_ifNeeded() -> [SwiftSetting] { + #if compiler(<6.3) + [.enableExperimentalFeature("SymbolLinkageMarkers")] + #else + [] + #endif +} diff --git a/Examples/Plugins/HioImage/hioPpm_Swift/README.md b/Examples/Plugins/HioImage/hioPpm_Swift/README.md new file mode 100644 index 0000000000..333043b9e8 --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Swift/README.md @@ -0,0 +1,23 @@ +# HioImage PPM plugin (Swift) + +This package is an example of how to write an HioImage plugin in Swift. + +## Key details +- In `plugInfo.json`, we use `@PLUG_INFO_LIBRARY_PATH@`, `@PLUG_INFO_RESOURCE_PATH@`, and `@PLUG_INFO_ROOT@` to allow our plugin to be used in both SwiftUsd-based apps and vanilla OpenUSD-based runtimes. +- In `Package.swift`, we use `.copy("plugInfo.json")` and `.plugin(name: "generate-plug-info-json", package: "SwiftUsd")` to have the build system fill in the `@PLUG_INFO_*@` values based on the target architecture +- In `Package.swift`, we add `.enableExperimentalFeature("SymbolLinkageMarkers")` as a Swift setting when using Swift 6.1/6.2, because Swift didn't gain official support for the `@used` and `@section` attributes until Swift 6.3. This allows us to write a plugin that can be used across as many Swift compiler versions as possible. +- In `hioPpm.swift`, we use the `@SWIFTUSD_PLUGIN` macro to tell the OpenUSD runtime how to use our custom plugin. We also subclass from `Overlay.HioImageSubclass` and implement its required (pure-virtual) methods. + +## Usage +To use this plugin in a SwiftUsd-based app: +1. Add it as a package dependency to your Swift Package/Xcode project. + +To use this plugin in a vanilla OpenUSD-based app: +1. `cd` into the package directory +2. Run `swift build` +3. Run `swift package build-vanilla-openusd-plugin` and note the outputed plugin path +4. Set the `PXR_PLUGINPATH_NAME` environment variable to point to your plugin. For example: +``` +export PXR_PLUGINPATH_NAME="/Users/maddyadams/SwiftUsd/Examples/HioImage/hioPpm_Swift/.build/plugins/build-vanilla-openusd-plugin/outputs/hioPpm.usdplugin/:$PXR_PLUGINPATH_NAME" +usdview example.usda +``` diff --git a/Examples/Plugins/HioImage/hioPpm_Swift/Sources/hioPpm/hioPpm.swift b/Examples/Plugins/HioImage/hioPpm_Swift/Sources/hioPpm/hioPpm.swift new file mode 100644 index 0000000000..151108e23f --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Swift/Sources/hioPpm/hioPpm.swift @@ -0,0 +1,231 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +import OpenUSD + +public typealias pxr = pxrInternal_v0_26_5__pxrReserved__ + +/// The plugin entry point for this plugin. It uses `@SWIFTUSD_PLUGIN` to register +/// this plugin with the OpenUSD runtime, and subclass from `Overlay.HioImageSubclass` +/// to implement an HioImage plugin. +/// It handles HioImage concerns and delegates actually working with the PPM image file format +/// to the ``PpmImage`` class +@SWIFTUSD_PLUGIN +final class HioPpm_Image: Overlay.HioImageSubclass { + private var filename: std.string = "" + private var ppmImage: PpmImage? + + func Read(_ storage: pxr.HioImage.StorageSpec) -> CBool { + ReadCropped(0, 0, 0, 0, storage) + } + + func ReadCropped(_ cropTop: CInt, _ cropBottom: CInt, _ cropLeft: CInt, _ cropRight: CInt, _ storage: pxr.HioImage.StorageSpec) -> CBool { + guard let ppmImage else { + TF_CODING_ERROR("Cannot ReadCropped on an unopened image") + return false + } + + if cropLeft < 0 || cropRight < 0 || cropTop < 0 || cropBottom < 0 { + TF_CODING_ERROR("Cannot ReadCropped with negative crop amounts") + return false + } + + if (cropLeft + cropRight >= GetWidth()) || (cropTop + cropBottom >= GetHeight()) { + TF_CODING_ERROR("Cannot ReadCropped with crop amounts exceeding image size") + return false + } + + for y in 0..= GetHeight() - cropBottom { continue } + for x in 0..= GetWidth() - cropRight { continue } + for c in 0..<3 { + let value = ppmImage.getPixel(x: UInt16(x + cropLeft), y: UInt16(y + cropTop), channel: c) + let maybeFlippedY = storage.flipped ? storage.height - y - 1 : y + let index = Int(maybeFlippedY * 3 * GetWidth()) + Int(3 * x) + c + if ppmImage.isTwoBytesPerComponent { + storage.data.assumingMemoryBound(to: UInt16.self)[index] = value + } else { + storage.data.assumingMemoryBound(to: UInt8.self)[index] = UInt8(value) + } + } + } + } + + return true + } + + func Write(_ storage: pxr.HioImage.StorageSpec, _ metadata: pxr.VtDictionary) -> CBool { + let hioType = pxr.HioGetHioType(storage.format) + let componentCount = pxr.HioGetComponentCount(storage.format) + + var maxValue: UInt16 = 0 + if hioType == .HioTypeUnsignedByte || hioType == .HioTypeUnsignedByteSRGB { + maxValue = UInt16(UInt8.max) + } else { + maxValue = UInt16.max + } + + ppmImage = .init(forWritingWithWidth: UInt16(storage.width), height: UInt16(storage.height), componentMaximum: maxValue) + + for y in 0.. std.string { + return filename + } + + func GetWidth() -> CInt { + guard let ppmImage else { return 0 } + return Int32(ppmImage.width) + } + + func GetHeight() -> CInt { + guard let ppmImage else { return 0 } + return Int32(ppmImage.height) + } + + func GetFormat() -> pxr.HioFormat { + guard let ppmImage else { + TF_CODING_ERROR("Invalid image") + return .HioFormatUInt16Vec3 + } + + if ppmImage.isTwoBytesPerComponent { + return .HioFormatUInt16Vec3 + } else { + return .HioFormatUNorm8Vec3srgb + } + } + + func GetBytesPerPixel() -> CInt { + guard let ppmImage else { return 0 } + return ppmImage.isTwoBytesPerComponent ? 6 : 3 + } + + func GetNumMipLevels() -> CInt { + 1 + } + + func IsColorSpaceSRGB() -> CBool { + true + } + + func GetMetadata(_ key: pxr.TfToken, _ value: UnsafeMutablePointer?) -> CBool { + false + } + + func GetSamplerMetadata(_ dim: pxr.HioAddressDimension, _ param: UnsafeMutablePointer?) -> CBool { + false + } + + func _OpenForReading(_ filename: std.string, _ subimage: CInt, _ mip: CInt, _ sourceColorSpace: pxr.HioImage.SourceColorSpace, _ suppressErrors: CBool) -> CBool { + self.filename = filename + ppmImage = .init(forReading: filename) + if ppmImage == nil && !suppressErrors { + return false + } + return true + } + + func _OpenForWriting(_ filename: std.string) -> CBool { + self.filename = filename + self.ppmImage = nil + return true + } +} + +/// Utility function from extracting a UInt16 from a buffer of `hioType` values +fileprivate func extractUInt16(bytes: UnsafeMutableRawPointer, index: Int, hioType: pxr.HioType) -> UInt16 { + switch hioType { + case .HioTypeUnsignedByte: + let raw = bytes.assumingMemoryBound(to: UInt8.self)[index] + return UInt16(raw) + + case .HioTypeUnsignedByteSRGB: + let raw = bytes.assumingMemoryBound(to: UInt8.self)[index] + return UInt16(raw) + + case .HioTypeSignedByte: + var raw = bytes.assumingMemoryBound(to: Int8.self)[index] + if raw < 0 { raw = 0} + return UInt16(raw) + + case .HioTypeUnsignedShort: + let raw = bytes.assumingMemoryBound(to: UInt16.self)[index] + return raw + + case .HioTypeSignedShort: + var raw = bytes.assumingMemoryBound(to: Int16.self)[index] + if raw < 0 { raw = 0 } + raw = min(raw, Int16(UInt16.max)) + return UInt16(raw) + + case .HioTypeUnsignedInt: + var raw = bytes.assumingMemoryBound(to: UInt32.self)[index] + raw = min(raw, UInt32(UInt16.max)) + return UInt16(raw) + + case .HioTypeInt: + var raw = bytes.assumingMemoryBound(to: Int32.self)[index] + if raw < 0 { raw = 0} + raw = min(raw, Int32(UInt16.max)) + return UInt16(raw) + + case .HioTypeHalfFloat: + var raw = Double(bytes.assumingMemoryBound(to: pxr.GfHalf.self)[index]) + raw *= Double(UInt16.max) + if raw < 0 { raw = 0 } + raw = min(raw, Double(UInt16.max)) + return UInt16(raw) + + case .HioTypeFloat: + var raw = Double(bytes.assumingMemoryBound(to: Float.self)[index]) + raw *= Double(UInt16.max) + if raw < 0 { raw = 0 } + raw = min(raw, Double(UInt16.max)) + return UInt16(raw) + + case .HioTypeDouble: + var raw = bytes.assumingMemoryBound(to: Double.self)[index] + raw *= Double(UInt16.max) + if raw < 0 { raw = 0 } + raw = min(raw, Double(UInt16.max)) + return UInt16(raw) + + case .HioTypeCount: + return 0 + + default: + TF_CODING_ERROR(std.string("Unknown hioType \(hioType)")) + return 0 + } +} diff --git a/Examples/Plugins/HioImage/hioPpm_Swift/Sources/hioPpm/plugInfo.json b/Examples/Plugins/HioImage/hioPpm_Swift/Sources/hioPpm/plugInfo.json new file mode 100644 index 0000000000..9255f0bebb --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Swift/Sources/hioPpm/plugInfo.json @@ -0,0 +1,20 @@ +{ + "Plugins": [ + { + "Info": { + "Types": { + "HioPpm_Image" : { + "bases" : ["HioImage"], + "imageTypes": ["ppm"], + "precedence": 15 + } + } + }, + "Name": "hioPpm", + "LibraryPath": "@PLUG_INFO_LIBRARY_PATH@", + "ResourcePath": "@PLUG_INFO_RESOURCE_PATH@", + "Root": "@PLUG_INFO_ROOT@", + "Type": "library" + } + ] +} diff --git a/Examples/Plugins/HioImage/hioPpm_Swift/Sources/hioPpm/ppmImage.swift b/Examples/Plugins/HioImage/hioPpm_Swift/Sources/hioPpm/ppmImage.swift new file mode 100644 index 0000000000..e7fac6e97c --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Swift/Sources/hioPpm/ppmImage.swift @@ -0,0 +1,224 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +import Foundation +import OpenUSD + +extension PpmImage { + /// Error type for errors while parsing a PPM image + fileprivate struct ParseError: Error { + var message: String + + init(_ message: String) { + self.message = message + } + + var description: String { message } + } +} + +/// Class for reading and writing PPM images +class PpmImage { + private(set) var width: UInt16 + private(set) var height: UInt16 + private(set) var componentMaximum: UInt16 + private var storage: [UInt16] + + init?(forReading filename: std.string) { + do throws(ParseError) { + + let asset = Overlay.ArGetResolver().OpenAsset(pxr.ArResolvedPath(filename)) + guard Bool(asset) else { + throw ParseError("PpmImage createForReading failed during ArResolver::OpenAsset '\(filename)'") + } + + var width: UInt16? + var height: UInt16? + var componentMaximum: UInt16? + var storage: [UInt16] = [] + + // Parse the file contents + try withExtendedLifetime((asset, asset.GetBuffer())) { (asset, bufSharedPtr) throws(ParseError) in + let buf = bufSharedPtr.__getUnsafe()! + + var index = 0 + while index < asset.GetSize() { + // Check for the magic number at the start of the file + if index == 0 { + guard buf[index] == UInt8(ascii: "P") else { + throw ParseError("Invalid PPM magic number") + } + index += 1 + continue + } + if index == 1 { + guard buf[index] == UInt8(ascii: "3") else { + throw ParseError("Invalid PPM magic number") + } + index += 1 + continue + } + + + // Line comments are ignored + if buf[index] == UInt8(ascii: "#") && index > 0 && buf[index - 1] == UInt8(ascii: "\n") { + // Advance through the end of the line + while index < asset.GetSize() && buf[index] != UInt8(ascii: "\n") { + index += 1 + } + continue + } + + // Whitespace delimits values + if isspace(Int32(buf[index])) != 0 { + // Advance through the last contiguous whitespace + while index < asset.GetSize() && isspace(Int32(buf[index])) != 0 { + index += 1 + } + continue + } + + // Handle numbers in the file + if isdigit(Int32(buf[index])) != 0 { + var digitBuffer = "" + while index < asset.GetSize() && isdigit(Int32(buf[index])) != 0 { + digitBuffer += String(UnicodeScalar(UInt8(buf[index]))) + index += 1 + } + + guard let parsedInt = UInt16(digitBuffer) else { + throw ParseError("Invalid PPM number (failed to convert)") + } + + if width == nil { + guard parsedInt > 0 else { + throw ParseError("Invalid PPM width") + } + width = parsedInt + continue + } + + if height == nil { + guard parsedInt > 0 else { + throw ParseError("Invalid PPM height") + } + height = parsedInt + continue + } + + if componentMaximum == nil { + guard parsedInt > 0 else { + throw ParseError("Invalid PPM component maximum") + } + componentMaximum = parsedInt + continue + } + + guard parsedInt <= componentMaximum! else { + throw ParseError("Invalid PPM, raster component greater than component maximum") + } + + storage.append(parsedInt) + guard storage.count <= 3 * width! * height! else { + throw ParseError("Invalid PPM, raster larger than expected") + } + + continue + } // `if isdigit(Int32(buf[index])) != 0` + + // We've handled comments, whitespace, and numbers, so + // anything else must be an invalid character in a single-image PPM file + throw ParseError("Invalid PPM file contents") + } // outermost `while index < asset.GetSize()` + + } // `withExtendedLifetime` + + guard let width, let height, let componentMaximum else { + throw ParseError("Invalid PPM file header") + } + + guard storage.count == 3 * width * height else { + throw ParseError("Invalid PPM, raster size is unexpected") + } + + self.width = width + self.height = height + self.componentMaximum = componentMaximum + self.storage = storage + + } catch let error as ParseError { + TF_RUNTIME_ERROR(std.string(error.message)) + return nil + } + } + + init(forWritingWithWidth width: UInt16, height: UInt16, componentMaximum: UInt16) { + self.width = width + self.height = height + self.componentMaximum = componentMaximum + self.storage = .init(repeating: 0, count: Int(width) * Int(height) * 3) + } + + var isTwoBytesPerComponent: Bool { componentMaximum >= 256 } + + func getPixel(x: UInt16, y: UInt16, channel: Int) -> UInt16 { + guard let index = _toIndex(x: x, y: y, channel: channel) else { return 0 } + return storage[index] + } + + func setPixel(x: UInt16, y: UInt16, channel: Int, value: UInt16) { + guard let index = _toIndex(x: x, y: y, channel: channel) else { return } + guard value <= componentMaximum else { + TF_CODING_ERROR("Cannot PpmImage.setPixel to a value greater than the component maximum") + return + } + storage[index] = value + } + + func write(filename: std.string) { + var asset = Overlay.ArGetResolver().OpenAssetForWrite(pxr.ArResolvedPath(filename), .Replace) + guard Bool(asset) else { + TF_RUNTIME_ERROR(std.string("PpmImage write failed during ArResolver::OpenAssetForWrite '\(filename)'")) + return + } + + var lines = [String]() + lines.append("P3") + lines.append("\(width) \(height)") + lines.append("\(componentMaximum)") + for i in stride(from: 0, to: storage.count, by: 3) { + lines.append("\(storage[i]) \(storage[i + 1]) \(storage[i + 2])") + } + let s = lines.joined(separator: "\n") + "\n" + + + asset.Write(s, s.count, 0) + #TF_VERIFY(asset.Close()) + } + + private func _toIndex(x: UInt16, y: UInt16, channel: Int) -> Int? { + if x >= width || y >= height || channel >= 3 || channel < 0 { + TF_CODING_ERROR("Invalid coordinates or channel for PpmImage") + return nil + } + + return Int(y) * 3 * Int(width) + 3 * Int(x) + channel + } +} diff --git a/Examples/Plugins/HioImage/hioPpm_Swift/SwiftUsd b/Examples/Plugins/HioImage/hioPpm_Swift/SwiftUsd new file mode 120000 index 0000000000..c866b86874 --- /dev/null +++ b/Examples/Plugins/HioImage/hioPpm_Swift/SwiftUsd @@ -0,0 +1 @@ +../../../.. \ No newline at end of file diff --git a/SwiftUsd.docc/AboutThisRepo/OngoingWork.md b/SwiftUsd.docc/AboutThisRepo/OngoingWork.md index 7b25dc72c3..b015d83757 100644 --- a/SwiftUsd.docc/AboutThisRepo/OngoingWork.md +++ b/SwiftUsd.docc/AboutThisRepo/OngoingWork.md @@ -49,8 +49,10 @@ SwiftUsd is currently a work-in-progress. Here is a list of potential future imp - Should propagate to computed properties and function bodies in views - Should be able to wire up a model to observation tracking, so app models that vend wrapper types for Usd data are compatible - Use macros in unit tests to catch observation keypaths by tracking the same way SwiftUI does -- Add support for custom file format plugins implemented in Swift - - Requires supporting static initialization in Swift via `@_section`. Could probably be wrapped in a macro +- ~~Add support for custom file format plugins implemented in Swift~~ + - Swift plugin support added in SwiftUsd 8.0.0 + - ~~Requires supporting static initialization in Swift via `@_section`. Could probably be wrapped in a macro~~ + - ~~Investigate if SwiftUsd works on Linux~~ - Experimental Linux support added in SwiftUsd 2.0.1 - Add Swift function body macros for validity warning diff --git a/SwiftUsd.docc/AboutThisRepo/ReleaseChecklist.md b/SwiftUsd.docc/AboutThisRepo/ReleaseChecklist.md index 7f9e327dd9..48f8548d0e 100644 --- a/SwiftUsd.docc/AboutThisRepo/ReleaseChecklist.md +++ b/SwiftUsd.docc/AboutThisRepo/ReleaseChecklist.md @@ -43,6 +43,7 @@ Checklist for releasing new versions of SwiftUsd 1. Links to vanilla OpenUSD source files on GitHub 1. Default openusd-ref in .github/workflows/build-swiftusd.yml 1. ci-at-desk sample YAML config file + 1. `PXR_VERSION` in `source/_OpenUSD_MacroImplementations/PluginAndTfMacros.swift` 1. Pixar namespace 1. [Getting Started, "Using SwiftUsd"]() 1. [Getting Started, "Common Issues"]() diff --git a/SwiftUsd.docc/OpenUSD.md b/SwiftUsd.docc/OpenUSD.md index a2cb256b10..fb6c04a0dd 100644 --- a/SwiftUsd.docc/OpenUSD.md +++ b/SwiftUsd.docc/OpenUSD.md @@ -55,6 +55,8 @@ This Swift Package provides a number of protocol conformances to make OpenUSD mo - ### Technical details +- +- - - - diff --git a/SwiftUsd.docc/TechnicalDetails/WritingAndUsingOpenUSDPlugins.md b/SwiftUsd.docc/TechnicalDetails/WritingAndUsingOpenUSDPlugins.md new file mode 100644 index 0000000000..15f2045b62 --- /dev/null +++ b/SwiftUsd.docc/TechnicalDetails/WritingAndUsingOpenUSDPlugins.md @@ -0,0 +1,131 @@ +# Writing and Using OpenUSD Plugins + +Learn how to write and use OpenUSD plugins written in Swift or C++ + +## Overview + +OpenUSD has a plugin system for extending its core capabilities, such as adding support for new image or Sdf file formats or adding new Hydra Render Delegates. With SwiftUsd, you can write plugins in either C++ or Swift, and use them in both SwiftUsd-based apps and vanilla OpenUSD runtimes (e.g. `usdview` and `usdrecord` from a vanilla install of OpenUSD). + +## Writing plugins in C++ with SwiftUsd +See [SwiftUsd/Examples/Plugins/HioImage/hioPpm_Cxx](https://github.com/apple/SwiftUsd/tree/main/Examples/Plugins/HioImage/hioPpm_Cxx) for an example plugin. + +1. Create a Swift Package for your plugin. + +2. Within the `Package.swift` package manifest, add a `.library` product that depends on your C++ target or targets. +For example: +```swift +products: [.library(name: "hioPpm", targets: ["hioPpm"]),], +``` + +3. In your package manifest, add these lines to the definition of your C++ target: +```swift +resources: [.copy("plugInfo.json")], +plugins: [.plugin(name: "generate-plug-info-json", package: "SwiftUsd")], +``` + +4. Add a `plugInfo.json` file to your C++ target. +Make sure to use the following key-value pairs within your plugin instead of hard-coding in specific values. This will ensure that the correct values are inserted into your `plugInfo.json` file at compile time. +```json +"LibraryPath": "@PLUG_INFO_LIBRARY_PATH@", +"ResourcePath": "@PLUG_INFO_RESOURCE_PATH@", +"Root": "@PLUG_INFO_ROOT@", +``` + +5. Subclass one or more OpenUSD types as needed for your plugin. +For example, to write a PPM HioImage plugin, you might write: +```cpp +class HioPpm_Image final: public pxr::HioImage { + ... + }; +``` +> Important: The type name in the `Types` object in your `plugInfo.json` file must match the name of your C++ plugin subclass. + +6. Within a `.cpp` file, make sure to use `TF_REGISTRY_FUNCTION` to let the OpenUSD runtime know about your plugin subclass. +For example: +```cpp +#include "pxr/base/tf/type.h" +#include "pxr/base/tf/registryManager.h" +#include "pxr/imaging/hio/image.h" + +PXR_NAMESPACE_OPEN_SCOPE +TF_REGISTRY_FUNCTION(TfType) { + TfType t = pxr::TfType::Define>(); + t.SetFactory>(); +} +PXR_NAMESPACE_CLOSE_SCOPE +``` + +## Writing plugins in Swift with SwiftUsd +See [SwiftUsd/Examples/Plugins/HioImage/hioPpm_Swift](https://github.com/apple/SwiftUsd/tree/main/Examples/Plugins/HioImage/hioPpm_Swift) for an example plugin. +> Note: Currently, only HioImage plugins can be written in Swift. + +1. Create a Swift Package for your plugin + +2. Within the `Package.swift` package manifest, add a `.library` product that depends on your Swift target or targets. +For example: +```swift +products: [.library(name: "hioPpm", targets: ["hioPpm"]),], +``` + +3. In your package manifest, add these lines to the definition of your Swift target: +```swift +resources: [.copy("plugInfo.json")], +swiftSettings: [.interoperabilityMode(.Cxx)], +plugins: [.plugin(name: "generate-plug-info-json", package: "SwiftUsd")], +``` +> Important: On Swift 6.1-6.2, you also need to add `.enableExperimentalFeature("SymbolLinkageMarkers")` to the `swiftSettings` array. + +4. Add a `plugInfo.json` file to your target. +Make sure to use the following key-value pairs within your plugin instead of hard-coding in specific values. This will ensure that the correct values are inserted into your `plugInfo.json` file at compile time. +```json +"LibraryPath": "@PLUG_INFO_LIBRARY_PATH@", +"ResourcePath": "@PLUG_INFO_RESOURCE_PATH@", +"Root": "@PLUG_INFO_ROOT@", +``` + +5. Subclass one or more OpenUSD types as needed for your plugin using `Overlay.FooSubclass`. +For example: +```swift +final class HioPpm_Image: Overlay.HioImageSubclass { + .... +} +``` +> Note: Not all OpenUSD types can be subclassed in Swift. See for the complete list. + +6. Apply `@SWIFTUSD_PLUGIN` to your plugin entry point. For example: +```swift +@SWIFTUSD_PLUGIN +final class HioPpm_Image: Overlay.HioImageSubclass { + ... +} +``` +> Note: You only need to use the macro once per plugin, on the plugin entry point, not on every Swift subclass of an OpenUSD type. The name of the Swift class you apply the macro to must match the value in the `Types` object in your `plugInfo.json` file. + +## Using plugins within a SwiftUsd-based application +1. Add the SwiftUsd-based plugin you want to use as a remote or local package dependency to your app. The OpenUSD runtime will automatically discover and use the plugin when needed. + +## Using plugins within a vanilla OpenUSD-based application +1. Clone the SwiftUsd-based plugin you want to use. + +2. Run the following commands: +```zsh +cd /path/to/cloned-swiftusd-plugin +swift build +swift package build-vanilla-openusd-plugin +``` +The `swift build` command builds the package like normal, and the `swift package build-vanilla-openusd-plugin` invokes a SwiftUsd-provided custom build command that converts the output from `swift build` into an OpenUSD plugin. + +3. Set the `PXR_PLUGINPATH_NAME` environment variable to include the path to the plugin produced by `swift package build-vanilla-openusd-plugin`. For example: +``` +export PXR_PLUGINPATH_NAME="/Users/maddyadams/SwiftUsd/Examples/Plugins/HioImage/hioPpm_Swift/.build/plugins/build-vanilla-openusd-plugin/outputs/hioPpm.usdplugin:$PXR_PLUGINPATH_NAME" +``` +Then, use a command like `usdview` or `usdrecord`. +> Note: The `.usdplugin` directory produced by `swift package build-vanilla-openusd-plugin` is fully relocatable, so you can move it to another directory after compiling. + +## Technical details +After you add `plugins: [.plugin(name: "generate-plug-info-json", package: "SwiftUsd")],` to a target in your Swift Package manifest, the build system will automatically invoke the Swift Package Manager plugin `generate-plug-info-json` provided by SwiftUsd when building that target. (Note that Swift Package Manager plugins are completely unrelated to OpenUSD plugins.) +`generate-plug-info-json` produces three modified copies of your `plugInfo.json` file: `plugInfo_macOS.json`, `plugInfo_iOS.json`, and `plugInfo_vanilla.json`. SwiftUsd-based applications will automatically use `plugInfo_macOS.json` in macOS apps and `plugInfo_iOS.json` in iOS/visionOS apps, ignoring the other files. After using `swift package build-vanilla-openusd-plugin` on a plugin, vanilla OpenUSD runtimes will automatically use `plugInfo_vanilla.json`, ignoring the other files. +The different files are required because macOS, iOS, and vanilla OpenUSD plugins use different bundle structures. When you use the `@PLUG_INFO_FOO@` values in your `plugInfo.json` file instead of hard-coding values, `generate-plug-info-json` automatically fills them in with the right values for the intended target architecture/use case. + +`swift package build-vanilla-openusd-plugin` invokes the Swift Package Manager plugin `build-vanilla-openusd-plugin` provided by SwiftUsd. (Note that Swift Package Manager plugins are completely unrelated to OpenUSD plugins.) +`build-vanilla-openusd-plugin` expects that you've already run `swift build` to compile your code. It traverses the build directory to find and link together object files produced by your targets. It also weak-links the OpenUSD frameworks provided by SwiftUsd. This means that your dylib may still work if the host application wasn't built with certain feature flags (e.g. Alembic), but it also means that your dylib may crash the host application if it tries to use missing symbols without checking for their existence at runtime. Finally, `build-vanilla-openusd-plugin` uses `install_name_tool` to change the `LC_LOAD_DYLIB` commands to the form used by vanilla OpenUSD installs. \ No newline at end of file diff --git a/SwiftUsd.docc/generated/SwiftSubclassCxx.md b/SwiftUsd.docc/generated/SwiftSubclassCxx.md new file mode 120000 index 0000000000..2456b13283 --- /dev/null +++ b/SwiftUsd.docc/generated/SwiftSubclassCxx.md @@ -0,0 +1 @@ +../../source/generated/SwiftSubclassCxx.md \ No newline at end of file diff --git a/scripts/make-swift-package/Sources/CLIArgs.swift b/scripts/make-swift-package/Sources/CLIArgs.swift index 8107c4acaa..e14c8d2962 100644 --- a/scripts/make-swift-package/Sources/CLIArgs.swift +++ b/scripts/make-swift-package/Sources/CLIArgs.swift @@ -54,16 +54,6 @@ struct CLIArgs: AsyncParsableCommand { #endif }() - @Option(name: .customLong("copy-plugins"), - help: nil, // todo - transform: URL.init(fileURLWithPath:)) - var copiedPlugins: [URL] = [] - - @Option(name: .customLong("symlink-plugins"), - help: nil, // todo - transform: URL.init(fileURLWithPath:)) - var symlinkedPlugins: [URL] = [] - @Option(help: "Controls whether `SwiftUsd/source` is copied or symlinked into the generated package directory.") var sourceStrategy: SourceStrategy = .symlink @@ -104,10 +94,6 @@ struct CLIArgs: AsyncParsableCommand { } #endif - if !copiedPlugins.isEmpty || !symlinkedPlugins.isEmpty { - throw ValidationError("Custom Usd plugins are not supported yet. Remove --copy-plugins and --symlink-plugins options.") - } - #if !os(macOS) if checksummedArtifactsDir != nil || artifactsHostingURL != nil { throw ValidationError("Checksummed artifacts are only supported on Apple platforms.") diff --git a/scripts/make-swift-package/Sources/FileSystemInfo.swift b/scripts/make-swift-package/Sources/FileSystemInfo.swift index 56d28de62a..11157ea11c 100644 --- a/scripts/make-swift-package/Sources/FileSystemInfo.swift +++ b/scripts/make-swift-package/Sources/FileSystemInfo.swift @@ -74,14 +74,6 @@ struct FileSystemInfo { result.append("- \(usdInstall.url.path(percentEncoded: false))") } result.append("Usd install strategy: \(cliArgs.usdInstallStrategy)") - result.append("Copied plugins:") - for x in cliArgs.copiedPlugins { - result.append("- \(x.relativePath)") - } - result.append("Symlinked plugins:") - for x in cliArgs.symlinkedPlugins { - result.append("- \(x.relativePath)") - } result.append("Source strategy: \(cliArgs.sourceStrategy)") result.append("Checksummed artifacts dir: \(cliArgs.checksummedArtifactsDir?.relativePath ?? "nil")") result.append("Artifacts hosting URL: \(cliArgs.artifactsHostingURL ?? "nil")") @@ -203,6 +195,8 @@ extension FileSystemInfo { var sources_OpenUSD_SwiftBindingHelpers: URL { sources.appending(path: "_OpenUSD_SwiftBindingHelpers") } /// `package/Sources/_OpenUSD_MacroImplementations` var sources_OpenUSD_MacroImplementations: URL { sources.appending(path: "_OpenUSD_MacroImplementations") } + /// `package/plugins` + var plugins: URL { generatedSwiftPackageDir.appending(path: "Plugins") } /// `package/Sources/_OpenUSD_SwiftBindingHelpers/include` var sourcesInclude: URL { sources_OpenUSD_SwiftBindingHelpers.appending(path: "include") } diff --git a/scripts/make-swift-package/Sources/Framework.swift b/scripts/make-swift-package/Sources/Framework.swift index 0c3ca68192..4a37d77598 100644 --- a/scripts/make-swift-package/Sources/Framework.swift +++ b/scripts/make-swift-package/Sources/Framework.swift @@ -126,14 +126,19 @@ struct Framework: Sendable { // except that we use a capital-R for macOS bundle. // `../../../../*.framework/Resources/` finds hydra plugin definitions // after Xcode has processed XCFrameworks to extract the platform's framework - // In both bases, `Resources_iOS` is where the resources live in an iOS bundle + // In both bases, `Resources_iOS` is where the resources live in an iOS bundle. + // To support OpenUSD plugins distributed with SPM, we bootstrap to either plugInfo_macOS.json + // or plugInfo_iOS.json within a bundle, because the dylibs will live in different places + // relative to the bundle, and there's no way to conditionalize that in USD's eyes. let plugInfoContents = """ { "Includes": [ "*/Resources/", "*/Resources_iOS/", "../../../../*.framework/Resources/", - "../../*.framework/Resources_iOS/" + "../../*.framework/Resources_iOS/", + "../../../../../Resources/*.bundle/Contents/Resources/plugInfo_macOS.json", + "../../../*.bundle/plugInfo_iOS.json" ] } diff --git a/scripts/make-swift-package/Sources/SwiftPackage.swift b/scripts/make-swift-package/Sources/SwiftPackage.swift index 1075a7e798..1fdc78a543 100644 --- a/scripts/make-swift-package/Sources/SwiftPackage.swift +++ b/scripts/make-swift-package/Sources/SwiftPackage.swift @@ -420,8 +420,11 @@ struct SwiftPackage { destParent = fsInfo.swiftUsdPackage.sources_OpenUSD_SwiftBindingHelpers case let x where x.hasSuffix(".swift") || x.hasSuffix(".metal") || x.hasSuffix(".usdz"): let sourceMacroImplementations = fsInfo.swiftUsdPackage.repoSource.appending(path: fsInfo.swiftUsdPackage.sources_OpenUSD_MacroImplementations.lastPathComponent) + let sourceSPMPlugins = fsInfo.swiftUsdPackage.repoSource.appending(path: "SPMPlugins") if fileURL.path(percentEncoded: false).starts(with: sourceMacroImplementations.path(percentEncoded: false)) { destParent = fsInfo.swiftUsdPackage.sources_OpenUSD_MacroImplementations + } else if fileURL.path(percentEncoded: false).starts(with: sourceSPMPlugins.path(percentEncoded: false)) { + destParent = fsInfo.swiftUsdPackage.plugins } else { destParent = fsInfo.swiftUsdPackage.sourcesOpenUSD } @@ -436,6 +439,8 @@ struct SwiftPackage { let dest: URL if fileURL.lastPathComponent.hasSuffix(".apinotes") { dest = destParent.appending(path: fileURL.lastPathComponent) + } else if destParent == fsInfo.swiftUsdPackage.plugins { + dest = destParent.appending(path: fileURL.urlRelative(to: fsInfo.swiftUsdPackage.repoSource).relativePath.replacingOccurrences(of: "SPMPlugins/", with: "")) } else { dest = destParent.appending(path: fileURL.urlRelative(to: fsInfo.swiftUsdPackage.repoSource).relativePath) } diff --git a/source/Package.swift.in b/source/Package.swift.in index ed588b76a9..eb5fff23a0 100644 --- a/source/Package.swift.in +++ b/source/Package.swift.in @@ -31,7 +31,9 @@ let package = Package( .library(name: "OpenUSD", targets: [ "_OpenUSD_SwiftBindingHelpers", "OpenUSD" - ]) + ]), + .plugin(name: "build-vanilla-openusd-plugin", targets: ["build-vanilla-openusd-plugin"]), + .plugin(name: "generate-plug-info-json", targets: ["generate-plug-info-json"]), ], dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax.git", "600.0.0-latest"..."603.0.0"), @@ -60,6 +62,16 @@ let package = Package( dependencies: cppTarget_dependencies(), path: "${generated-package-prefix}Sources/_OpenUSD_SwiftBindingHelpers", cxxSettings: conditionalCxxSettings()), + + .plugin(name: "generate-plug-info-json", + capability: .buildTool(), + path: "${generated-package-prefix}Plugins/generate-plug-info-json"), + + .plugin(name: "build-vanilla-openusd-plugin", + capability: .command( + intent: .custom(verb: "build-vanilla-openusd-plugin", description: "Builds an OpenUSD plugin for use with vanilla OpenUSD installs") + ), + path: "${generated-package-prefix}Plugins/build-vanilla-openusd-plugin"), ] + xcframeworkBinaryTargets(), cxxLanguageStandard: .gnucxx17 diff --git a/source/SPMPlugins/build-vanilla-openusd-plugin/argParse.swift b/source/SPMPlugins/build-vanilla-openusd-plugin/argParse.swift new file mode 100644 index 0000000000..0a871aa9d8 --- /dev/null +++ b/source/SPMPlugins/build-vanilla-openusd-plugin/argParse.swift @@ -0,0 +1,242 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +import Foundation +import PackagePlugin + +// Dedicated argument parsing for build-vanilla-openusd-plugin, +// because we can't use swift-argument-parser and PackagePlugin.ArgumentExtractor +// leaves something to be desired +struct ArgParse { + // Fully resolved/parsed args + var force: Bool = false + var outputDirectory: URL! + var productName: String = "" + var configuration: String = "" + + // Derived information that isn't really from the CLI + var context: PluginContext + var packageName: String = "" + var selectiveRecursiveTargetNames: [String] = [] + var hadExplicitProductName = false + var shouldLinkAgainstSwift = false + + var scratchPath: URL { context.pluginWorkDirectoryURL.appending(path: ".tmp") } + var buildDirectory: URL { packagePath.appending(components: ".build", configuration) } + var packagePath: URL { context.package.directoryURL } + + init(context: PluginContext, arguments: [String]) throws { + self.context = context + try parseRawArgs(arguments: arguments) + try validateArgs() + try deriveRemainingProperties() + } + + func printHelpAndThrow(_ driverError: DriverError) throws { + let ansiPrefix = "\u{001B}[1m" + let ansiSuffix = "\u{001B}[0m" + + print(""" + usage: build-vanilla-openusd-plugin [OPTIONS] + + Build this Swift Package as an OpenUSD plugin that can be used with vanilla + OpenUSD builds. + + \(ansiPrefix)Important:\(ansiSuffix) You must run `swift build` first. + + Options: + -h, --help Print this help message and exit. + + -o, --output Write the plugin to a custom directory. You + may also need to pass it to + `swift package --allow-writing-to-directory `. + + --force If the custom output directory exists, remove + it before proceeding. + + --product Select a specific product from the package to + build. Optional if the product has a single + library product. + + -c, --configuration Build with configuration (values: debug, + release). + + """) + + throw driverError + } +} + +extension ArgParse { + private mutating func parseRawArgs(arguments: [String]) throws { + var i = 0 + while i < arguments.count { + switch arguments[i] { + case "-h", "-help", "--help": + try printHelpAndThrow(.userSpecifiedHelp) + + case "--force": force = true; i += 1 + + case "-o", "--output": + if outputDirectory != nil { try printHelpAndThrow(.tooManyOptions(.output)) } + if i + 1 >= arguments.count { try printHelpAndThrow(.optionRequiresArgument(.output)) } + outputDirectory = URL(fileURLWithPath: arguments[i + 1]); i += 2 + + case "--product": + if !productName.isEmpty { try printHelpAndThrow(.tooManyOptions(.product)) } + if i + 1 >= arguments.count { try printHelpAndThrow(.optionRequiresArgument(.product)) } + productName = arguments[i + 1]; i += 2 + hadExplicitProductName = true + + case "-c", "--configuration": + if !configuration.isEmpty { try printHelpAndThrow(.tooManyOptions(.configuration)) } + if i + 1 >= arguments.count { try printHelpAndThrow(.optionRequiresArgument(.configuration)) } + if arguments[i + 1] != "debug" && arguments[i + 1] != "release" { try printHelpAndThrow(.invalidArgumentForOption(.configuration)) } + configuration = arguments[i + 1]; i += 2 + + default: + try printHelpAndThrow(.unknownArgument(arguments[i])) + } + } + } + + private mutating func validateArgs() throws { + if productName.isEmpty { + let libraryProducts = context.package.products(ofType: LibraryProduct.self) + if libraryProducts.count == 1 { + productName = context.package.products[0].name + } else { + try printHelpAndThrow(.mustSelectProductWhenPackageHasMultipleLibraryProducts) + } + } else { + let matchingProducts = (try? context.package.products(named: [productName])) ?? [] + if matchingProducts.isEmpty { + throw DriverError.noSuchProductInPackage(productName, context.package.products.map(\.name)) + } + if matchingProducts[0] is ExecutableProduct { + throw DriverError.selectedProductIsExecutable(productName) + } + } + + if configuration.isEmpty { + var contents = try FileManager.default.contentsOfDirectory(at: buildDirectory, includingPropertiesForKeys: nil) + contents = contents.filter { $0.lastPathComponent == "debug" || $0.lastPathComponent == "release" } + if contents.count == 0 { + try printHelpAndThrow(.noConfigurationInBuildDirectory) + } else if contents.count == 2 { + try printHelpAndThrow(.mustSelectConfigurationWhenBuildDirectoryHasMultipleConfigurations) + } else { + configuration = contents[0].lastPathComponent + } + } + + if outputDirectory == nil { + force = true + outputDirectory = context.pluginWorkDirectoryURL.appending(path: "\(productName).usdplugin") + } + } + + private mutating func deriveRemainingProperties() throws { + packageName = context.package.displayName + + selectiveRecursiveTargetNames = [] + for target in try context.package.products(named: [productName])[0].targets { + handleTargetDependency(superTarget: nil, currentTarget: target) + } + + selectiveRecursiveTargetNames = Array(Set(selectiveRecursiveTargetNames)) + } + + // We want to find the list of targets we should link with, + // recursively, but exclude OpenUSD and its dependencies under + // some circumstances + private mutating func handleTargetDependency(superTarget: (any Target)?, currentTarget: any Target) { + if currentTarget is SwiftSourceModuleTarget { shouldLinkAgainstSwift = true } + + switch currentTarget.name { + case "OpenUSD": + if let superTarget { + if superTarget is SwiftSourceModuleTarget { + // Only Swift targets get to link against SwiftUsd + selectiveRecursiveTargetNames.append("OpenUSD") + selectiveRecursiveTargetNames.append("_OpenUSD_SwiftBindingHelpers") + return + } else if let clangTarget = superTarget as? ClangSourceModuleTarget { + // Clang targets can link against _OpenUSD_SwiftBindingHelpers if + // they have a `swiftUsd/swiftUsd.h` include directive + var hadSwiftUsdIncludeDirective = false + fileLoop: for file in clangTarget.sourceFiles { + guard file.type == .source || file.type == .header else { continue } + guard let s = try? String(contentsOf: file.url, encoding: .utf8) else { continue } + for l in s.components(separatedBy: .newlines) { + if l.firstMatch(of: #/#\s*include\s+["<]swiftUsd/swiftUsd\.h[">]\s*/#) != nil { + hadSwiftUsdIncludeDirective = true + break fileLoop + } + } + } + + if hadSwiftUsdIncludeDirective { + selectiveRecursiveTargetNames.append("_OpenUSD_SwiftBindingHelpers") + } + return + } else { + // The target isn't Swift and it doesn't use `swiftUsd/swiftUsd.h`, so + // it doesn't need to link against anything added by SwiftUsd that isn't + // part of vanilla OpenUSD + return + } + } + + case "_OpenUSD_SwiftBindingHelpers": + // Users shouldn't be directly dependent on this + return + + case "_OpenUSD_MacroImplementations": + // Users shouldn't be dependent on this + return + + case "generate-plug-info-json": + // Users can depend on this but shouldn't link against it + return + + case "build-vanilla-openusd-plugin": + // Users shouldn't be dependent on this + return + + default: + selectiveRecursiveTargetNames.append(currentTarget.name) + + for dependency in currentTarget.dependencies { + switch dependency { + case let .product(product): + for subTarget in product.targets { + handleTargetDependency(superTarget: currentTarget, currentTarget: subTarget) + } + case let .target(subTarget): + handleTargetDependency(superTarget: currentTarget, currentTarget: subTarget) + + @unknown default: + print("Warning! Unknown target dependency \(dependency), linking may fail") + } + } + } + } +} diff --git a/source/SPMPlugins/build-vanilla-openusd-plugin/build-vanilla-openusd-plugin.swift b/source/SPMPlugins/build-vanilla-openusd-plugin/build-vanilla-openusd-plugin.swift new file mode 100644 index 0000000000..de34bc540d --- /dev/null +++ b/source/SPMPlugins/build-vanilla-openusd-plugin/build-vanilla-openusd-plugin.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +import PackagePlugin +import Foundation + + +@main +struct BuildVanillaOpenUSDPlugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) async throws { + try await Driver.run(context: context, arguments: arguments) + } +} \ No newline at end of file diff --git a/source/SPMPlugins/build-vanilla-openusd-plugin/driver.swift b/source/SPMPlugins/build-vanilla-openusd-plugin/driver.swift new file mode 100644 index 0000000000..c9aa22ee12 --- /dev/null +++ b/source/SPMPlugins/build-vanilla-openusd-plugin/driver.swift @@ -0,0 +1,302 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +import Foundation +import PackagePlugin + +enum DriverError: Error, CustomStringConvertible { + enum OptionKind { + case output + case product + case configuration + + var options: String { + switch self { + case .output: "-o or --output" + case .product: "--product" + case .configuration: "-c or --configuration" + } + } + + var validValues: String { + switch self { + case .output: "" + case .product: "" + case .configuration: "debug, release" + } + } + } + + case userSpecifiedHelp + case tooManyOptions(OptionKind) + case optionRequiresArgument(OptionKind) + case invalidArgumentForOption(OptionKind) + case mustPassOption(OptionKind) + case unknownArgument(String) + + case noConfigurationInBuildDirectory + case mustSelectConfigurationWhenBuildDirectoryHasMultipleConfigurations + + case outputDirExistsButForceNotPassed(URL) + case couldntCreateOutputDirectory(URL) + case couldntCreateTempDirectory(URL) + case mustSelectProductWhenPackageHasMultipleLibraryProducts + case couldNotFindDylib + case noSuchProductInPackage(String, [String]) + case selectedProductIsExecutable(String) + case couldNotResolvePackage(SubprocessResult) + case couldNotFindToolchainPath(SubprocessResult) + + var description: String { + switch self { + case .userSpecifiedHelp: "" + case let .tooManyOptions(kind): "Must pass only one \(kind.options) option." + case let .optionRequiresArgument(kind): "\(kind.options) option requires an argument." + case let .invalidArgumentForOption(kind): "Invalid argument for \(kind.options). Valid values are \(kind.validValues)." + case let .mustPassOption(kind): "Must pass a \(kind.options) option." + case let .unknownArgument(x): "Unknown argument \(x)" + + case .noConfigurationInBuildDirectory: "No configuration directory was found within the build directory.\nYou must run `swift build` before `swift package build-vanilla-openusd-plugin`" + case .mustSelectConfigurationWhenBuildDirectoryHasMultipleConfigurations: "Multiple configuration directories were found within the build directory.\nUse -c or --configuration to select one." + + case .outputDirExistsButForceNotPassed: "Output directory already exists. Remove it or pass --force to overwrite it." + case let .couldntCreateOutputDirectory(x): "Couldn't create output directory. You might need to pass --allow-writing-to-directory '\(x.path(percentEncoded: false))' to `swift package`." + case let .couldntCreateTempDirectory(x): "Couldn't create temp directory '\(x.path(percentEncoded: false))'." + case .mustSelectProductWhenPackageHasMultipleLibraryProducts: "Package contains multiple library products.\nPass --product to select the product to build." + case .couldNotFindDylib: "Could not find dylib after invoking swift build." + + case let .noSuchProductInPackage(product, products): "No such product '\(product)'. Available products are: \(products.joined(separator: ", "))." + + case let .selectedProductIsExecutable(product): "Product '\(product)' is an executable. Select a library product instead." + + case let .couldNotResolvePackage(x): "`swift package resolve` returned with non-zero \(x.exitCode).\nConsider running `swift package resolve --scratch-path .build/plugins/build-vanilla-openusd-plugin/outputs` first, or `swift package --disable-sandbox build-vanilla-openusd-plugin `" + + case let .couldNotFindToolchainPath(x): "`xcrun --show-toolchain-path` returned with an invalid result." + } + } +} + +extension Driver { + static func run(context: PluginContext, arguments: [String]) async throws { + do { + var driver = try Driver(context: context, arguments: arguments) + + try driver.createOutputDirectory() + try await driver.linkIfNeeded() + try driver.copyIntoOutputDirectory() + try await driver.fixLoadCommands() + + print(#"Success! To use, set `PXR_PLUGINPATH_NAME="\#(driver.args.outputDirectory.resolvingSymlinksInPath().path(percentEncoded: false)):$PXR_PLUGINPATH_NAME"` before running commands that use OpenUSD"#) + } catch DriverError.userSpecifiedHelp { + // pass + } catch { + throw error + } + } +} + +struct Driver { + var args: ArgParse + + init(context: PluginContext, arguments: [String]) throws { + self.args = try ArgParse(context: context, arguments: arguments) + } + + mutating func createOutputDirectory() throws { + if FileManager.default.fileExists(atPath: args.outputDirectory.path(percentEncoded: false)) && !args.force { + throw DriverError.outputDirExistsButForceNotPassed(args.outputDirectory) + } + + do { + if FileManager.default.fileExists(atPath: args.outputDirectory.path(percentEncoded: false)) { + try FileManager.default.removeItem(at: args.outputDirectory) + } + try FileManager.default.createDirectory(at: args.outputDirectory, withIntermediateDirectories: true) + } catch { + throw DriverError.couldntCreateOutputDirectory(args.outputDirectory) + } + + do { + if !FileManager.default.fileExists(atPath: args.scratchPath.path(percentEncoded: false)) { + try FileManager.default.createDirectory(at: args.scratchPath, withIntermediateDirectories: true) + } + } catch { + throw DriverError.couldntCreateTempDirectory(args.scratchPath) + } + } + + + func linkIfNeeded() async throws { + // clang++ **.o -o PRODUCT.dylib -Xlinker -FBUILD_DIR -Xlinker -weak_framework -Xlinker Usd_* -dynamiclib + + let dylib = args.buildDirectory.appending(path: "lib\(args.productName).dylib") + if FileManager.default.fileExists(atPath: dylib.path(percentEncoded: false)) { + // `swift build` produced a dylib, so the product must have been a `.dynamic` library, + // and we don't need to do anything. + try FileManager.default.copyItem(at: dylib, to: args.scratchPath.appending(path: dylib.lastPathComponent)) + return + } + // `swift build` didn't produce a dylib, so we need to link things ourselves + + // todo: handle static libraries correctly + + var arguments = [String]() + // **.o + func addObjectFilesRecursively(_ dir: URL) throws { + var isDirectory: ObjCBool = false + + guard FileManager.default.fileExists(atPath: dir.path(percentEncoded: false), isDirectory: &isDirectory) else { return } + guard isDirectory.boolValue else { return } + + let contents = try FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil) + for file in contents { + if file.lastPathComponent.hasSuffix(".o") { + arguments.append(file.path(percentEncoded: false)) + } + try addObjectFilesRecursively(file) + } + + } + for targetName in args.selectiveRecursiveTargetNames { + try addObjectFilesRecursively(args.buildDirectory.appending(path: "\(targetName).build")) + } + arguments += ["-dynamiclib", "-o", args.scratchPath.appending(path: dylib.lastPathComponent).path(percentEncoded: false)] + arguments += ["-Xlinker", "-F", "-Xlinker", args.buildDirectory.path(percentEncoded: false)] + // -Xlinker -weak_framework -Xlinker *.framework + for file in try FileManager.default.contentsOfDirectory(at: args.buildDirectory.resolvingSymlinksInPath(), includingPropertiesForKeys: nil) { + if file.lastPathComponent.hasSuffix(".framework") { + arguments += ["-Xlinker", "-weak_framework", "-Xlinker", file.deletingPathExtension().lastPathComponent] + } + } + + if args.shouldLinkAgainstSwift { + let proc = try await runSubprocess(executableName: "xcrun", arguments: ["--show-toolchain-path"], + printStdout: false, printStderr: true, check: true) + guard let toolchainPath = proc.stdout.components(separatedBy: .newlines).first else { + throw DriverError.couldNotFindToolchainPath(proc) + } + + arguments += ["-Xlinker", "-L", "-Xlinker", "\(toolchainPath)/usr/lib/swift/macosx"] + arguments += ["-Xlinker", "-l", "-Xlinker", "swiftCxx", + "-Xlinker", "-l", "-Xlinker", "swiftCxxStdlib"] + } + + _ = try await runSubprocess( + executableName: "clang++", + arguments: arguments, + printStdout: true, + printStderr: true, + check: true + ) + } + + func copyIntoOutputDirectory() throws { + let dylib = args.scratchPath.appending(path: "lib\(args.productName).dylib") + + do { + try FileManager.default.copyItem(at: dylib, to: args.outputDirectory.appending(path: dylib.lastPathComponent)) + } catch { + throw DriverError.couldNotFindDylib + } + + for file in try FileManager.default.contentsOfDirectory(at: args.buildDirectory.resolvingSymlinksInPath(), includingPropertiesForKeys: nil) { + guard file.lastPathComponent.hasSuffix(".bundle") else { continue } + try FileManager.default.copyItem(at: file, to: args.outputDirectory.appending(path: file.lastPathComponent)) + } + + let topPlugInfoJsonContents = """ + { + "Includes": [ + "*.bundle/plugInfo_vanilla.json" + ] + } + """ + try topPlugInfoJsonContents.write(to: args.outputDirectory.appending(path: "plugInfo.json"), atomically: true, encoding: .utf8) + } + + func fixLoadCommands() async throws { + // This is required for vanilla `usdview` even if the user weak-links everything, otherwise it just segfaults + + let proc = try await runSubprocess( + executableName: "otool", + arguments: ["-L", args.outputDirectory.appending(path: "lib\(args.productName).dylib").path(percentEncoded: false)], + printStdout: false, + printStderr: true, + check: true + ) + + let otoolLines = proc.stdout + .components(separatedBy: .newlines) + .dropFirst() // first line is the LC_ID_DYLIB, ie ourselves and not someone we link against + + var toInstallNameTool = [String]() + var shouldWarnAboutNotWeakLinked = [String]() + + for line in otoolLines { + // extract the names of rpath'd frameworks + guard let match = line.firstMatch(of: #/@rpath/([^/]+)\.framework/[^/]+ \(compati/#) else { continue } + toInstallNameTool.append(String(match.output.1)) + if !line.hasSuffix(", weak)") { + shouldWarnAboutNotWeakLinked.append(String(match.output.1)) + } + } + + func computeOldName(_ name: some StringProtocol) -> String { + return "@rpath/\(name).framework/\(name)" + } + + func computeNewName(_ name: some StringProtocol) -> String { + var result = String(name) + if result.starts(with: "Usd_") { + result = String(result.dropFirst(4)) + result = result.first!.uppercased() + String(result.dropFirst()) + result = "usd_" + result + } + if ["usdShaders", "sdrGlslfx", "hdStorm", "hioImageIO", "hioAvif", "hioOpenEXR"].contains(result) { + result = "@rpath/\(result).dylib" + } else { + result = "@rpath/lib\(result).dylib" + } + return result + } + + for x in toInstallNameTool { + let oldName = computeOldName(x) + let newName = computeNewName(x) + + // print("install_name_tool -change \(oldName) \(newName) \(args.outputDirectory.appending(path: "lib\(args.productName).dylib").path(percentEncoded: false))") + try await runSubprocess( + executableName: "install_name_tool", + arguments: ["-change", oldName, newName, args.outputDirectory.appending(path: "lib\(args.productName).dylib").path(percentEncoded: false)], + printStdout: false, + printStderr: true, + check: true + ) + } + + print("") + for x in shouldWarnAboutNotWeakLinked { + print("Warning! '\(x)' is not weak-linked, plugin may not be compatible with vanilla Usd installations.") + } + if !shouldWarnAboutNotWeakLinked.isEmpty { + print(#"Use `linkerSettings: [.unsafeFlags(["-Xlinker", "-weak_framework", "-Xlinker", "{FRAMEWORK_NAME}"])]` in your Package.swift to weakly link frameworks"#) + } + } +} diff --git a/source/SPMPlugins/build-vanilla-openusd-plugin/runSubprocess.swift b/source/SPMPlugins/build-vanilla-openusd-plugin/runSubprocess.swift new file mode 100644 index 0000000000..61bc4ed469 --- /dev/null +++ b/source/SPMPlugins/build-vanilla-openusd-plugin/runSubprocess.swift @@ -0,0 +1,118 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +import Foundation +import os + +struct SubprocessResult { + var exitCode: Int32 + var stdout: String + var stderr: String +} + +enum SubprocessError: Error, CustomStringConvertible { + case nonZeroExitCode(String, [String], Int32) + case couldNotFindExecutable(String) + + var description: String { + switch self { + case let .nonZeroExitCode(name, args, ret): "\(name) \(args.joined(separator: " ")) exited with return code \(ret)" + case let .couldNotFindExecutable(name): "Could not find executable '\(name)'" + } + } +} + +func runSubprocess( + executableName: String, + arguments: [String], + printStdout: Bool, + printStderr: Bool, + check: Bool +) async throws -> SubprocessResult { + func findExecutableURL(named name: String) throws -> URL { + for pathEntry in (ProcessInfo.processInfo.environment["PATH"] ?? "").components(separatedBy: ":") { + let contents = try FileManager.default.contentsOfDirectory(atPath: pathEntry) + if contents.contains(executableName) { + return URL(fileURLWithPath: pathEntry).appending(path: executableName) + } + } + + throw SubprocessError.couldNotFindExecutable(name) + } + + let executableURL = try findExecutableURL(named: executableName) + + let outputPipe = Pipe() + let errorPipe = Pipe() + + let process = Process() + + // todo: Replace with Synchronization.Mutex once we bump SwiftUsd's minimum macOS to macOS 15.0 + let outputData = OSAllocatedUnfairLock(initialState: Data()) + let errorData = OSAllocatedUnfairLock(initialState: Data()) + + outputPipe.fileHandleForReading.readabilityHandler = { handle in + let d = handle.availableData + outputData.withLock { $0.append(d) } + + if d.count != 0, printStdout, let s = String(data: d, encoding: .utf8) { + print(s, terminator: "") + fflush(stdout) + } + } + + + errorPipe.fileHandleForReading.readabilityHandler = { handle in + let d = handle.availableData + errorData.withLock { $0.append(d) } + + if d.count != 0, printStderr, let s = String(data: d, encoding: .utf8) { + print(s) + } + } + + try await withCheckedThrowingContinuation { continuation in + do { + process.arguments = arguments + process.executableURL = executableURL + process.standardOutput = outputPipe + process.standardError = errorPipe + + process.terminationHandler = { + if $0.terminationStatus == 0 || !check { + continuation.resume() + } else { + continuation.resume(throwing: SubprocessError.nonZeroExitCode(executableName, arguments, $0.terminationStatus)) + } + } + try process.run() + } catch { + continuation.resume(throwing: error) + } + } + + if process.terminationStatus == 0 || !check { + let out = String(data: outputData.withLock { $0 }, encoding: .utf8) ?? "" + let err = String(data: errorData.withLock { $0 }, encoding: .utf8) ?? "" + return .init(exitCode: process.terminationStatus, stdout: out, stderr: err) + } + + throw SubprocessError.nonZeroExitCode(executableName, arguments, process.terminationStatus) +} diff --git a/source/SPMPlugins/generate-plug-info-json/generate-plug-info-json.swift b/source/SPMPlugins/generate-plug-info-json/generate-plug-info-json.swift new file mode 100644 index 0000000000..b106ac3534 --- /dev/null +++ b/source/SPMPlugins/generate-plug-info-json/generate-plug-info-json.swift @@ -0,0 +1,117 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +import PackagePlugin +import Foundation + +/// Makes the source for a shell script that transforms the `plugInfo.json` file into multiple platform-specific `plugInfo_foo.json` files +func makeShellSource(templateUrl: URL, outputDirectory: URL, packageName: String, targetName: String) -> (String, [String : String]) { + func escapeSlashes(_ x: String) -> String { x.replacingOccurrences(of: "/", with: "\\/") } + + // We want to treat the templateUrl, outputDirectory, packageName, and targetName as raw strings, but they might + // not be sanitized. So rather than try to escape any shell characters, we put them into the environment and then + // expand them within the script. + var environment = [String : String]() + environment["SWIFTUSD_GPIJ_ORIGINAL_FILE"] = templateUrl.path(percentEncoded: false) + + environment["SWIFTUSD_GPIJ_MACOS_FILE"] = outputDirectory.appending(path: "plugInfo_macOS.json").path(percentEncoded: false) + environment["SWIFTUSD_GPIJ_MACOS_LIBRARY"] = "" + environment["SWIFTUSD_GPIJ_MACOS_RESOURCE"] = escapeSlashes("\(packageName)_\(targetName).bundle/Contents/Resources") + environment["SWIFTUSD_GPIJ_MACOS_ROOT"] = escapeSlashes("../../..") + + environment["SWIFTUSD_GPIJ_IOS_FILE"] = outputDirectory.appending(path: "plugInfo_iOS.json").path(percentEncoded: false) + environment["SWIFTUSD_GPIJ_IOS_LIBRARY"] = "" + environment["SWIFTUSD_GPIJ_IOS_RESOURCE"] = escapeSlashes("\(packageName)_\(targetName).bundle") + environment["SWIFTUSD_GPIJ_IOS_ROOT"] = escapeSlashes("..") + + environment["SWIFTUSD_GPIJ_VANILLA_FILE"] = outputDirectory.appending(path: "plugInfo_vanilla.json").path(percentEncoded: false) + environment["SWIFTUSD_GPIJ_VANILLA_LIBRARY"] = escapeSlashes("lib\(targetName).dylib") + environment["SWIFTUSD_GPIJ_VANILLA_RESOURCE"] = escapeSlashes("\(packageName)_\(targetName).bundle") + environment["SWIFTUSD_GPIJ_VANILLA_ROOT"] = escapeSlashes("..") + + // Make sure to always quote the environment variable expansions, including in the sed commands. + // Use a double quoted command string for sed so we can expand environment variables within it + let script = """ + cat "$SWIFTUSD_GPIJ_ORIGINAL_FILE" |\ + sed -E "s/@PLUG_INFO_LIBRARY_PATH@/$SWIFTUSD_GPIJ_MACOS_LIBRARY/" |\ + sed -E "s/@PLUG_INFO_RESOURCE_PATH@/$SWIFTUSD_GPIJ_MACOS_RESOURCE/" |\ + sed -E "s/@PLUG_INFO_ROOT@/$SWIFTUSD_GPIJ_MACOS_ROOT/" >\ + "$SWIFTUSD_GPIJ_MACOS_FILE" + + cat "$SWIFTUSD_GPIJ_ORIGINAL_FILE" |\ + sed -E "s/@PLUG_INFO_LIBRARY_PATH@/$SWIFTUSD_GPIJ_IOS_LIBRARY/" |\ + sed -E "s/@PLUG_INFO_RESOURCE_PATH@/$SWIFTUSD_GPIJ_IOS_RESOURCE/" |\ + sed -E "s/@PLUG_INFO_ROOT@/$SWIFTUSD_GPIJ_IOS_ROOT/" >\ + "$SWIFTUSD_GPIJ_IOS_FILE" + + cat "$SWIFTUSD_GPIJ_ORIGINAL_FILE" |\ + sed -E "s/@PLUG_INFO_LIBRARY_PATH@/$SWIFTUSD_GPIJ_VANILLA_LIBRARY/" |\ + sed -E "s/@PLUG_INFO_RESOURCE_PATH@/$SWIFTUSD_GPIJ_VANILLA_RESOURCE/" |\ + sed -E "s/@PLUG_INFO_ROOT@/$SWIFTUSD_GPIJ_VANILLA_ROOT/" >\ + "$SWIFTUSD_GPIJ_VANILLA_FILE" + """ + + return (script, environment) +} + +@main +struct GeneratePlugInfoJson: BuildToolPlugin { + func createBuildCommands(context: PluginContext, + target: any Target) throws -> [Command] { + + guard let sourceModule = target.sourceModule else { + Diagnostics.error("generate-plug-info-json can only be used as a plugin on source module targets, but target '\(target.name)' is '\(type(of: target))'", file: nil, line: nil) + return [] + } + let plugInfoJsonList = sourceModule.sourceFiles(withSuffix: "plugInfo.json") + guard plugInfoJsonList.count == 1, plugInfoJsonList.first!.type == .resource else { + Diagnostics.error("generate-plug-info-json expects exactly one resource ending in plugInfo.json for the target that invokes it") + return [] + } + + let executable = URL(fileURLWithPath: "/bin/sh") + + let templateUrl = plugInfoJsonList.first!.url + + let outputFileNames = [ + "plugInfo_vanilla.json", + "plugInfo_iOS.json", + "plugInfo_macOS.json" + ] + + let outputUrls = outputFileNames.map { context.pluginWorkDirectoryURL.appending(path: $0) } + + let (shellSource, environment) = makeShellSource( + templateUrl: templateUrl, + outputDirectory: context.pluginWorkDirectoryURL, + packageName: context.package.displayName, + targetName: target.name, + ) + + return [ + .buildCommand(displayName: "generate-plug-info-json", + executable: executable, + arguments: ["-c", shellSource], + environment: environment, + inputFiles: [templateUrl], + outputFiles: outputUrls) + ] + } +} diff --git a/source/SwiftOverlay/SwiftSubclassCxx_handwritten.swift b/source/SwiftOverlay/SwiftSubclassCxx_handwritten.swift new file mode 100644 index 0000000000..31fc3a07cf --- /dev/null +++ b/source/SwiftOverlay/SwiftSubclassCxx_handwritten.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +import Foundation + +fileprivate let envVarName = "SWIFTUSD_SWIFT_SUBCLASS_CXX_ZOMBIE_CREATION_BEHAVIOR" + +extension __Overlay { + fileprivate enum Behavior { + case warn + case terminate + case ignore + + init(rawValue: String?) { + switch rawValue { + case nil, "", "warn", "default": self = .warn + case "terminate": self = .terminate + case "ignore": self = .ignore + default: + print("Warning: Unknown value '\(rawValue)' for \(envVarName), defaulting to 'warn'") + self = .warn + } + } + } + + fileprivate static nonisolated(unsafe) var behavior: Behavior = { + Behavior(rawValue: ProcessInfo.processInfo.environment[envVarName]) + }() + + static func SwiftSubclassCxx_zombieCreated(swiftInstance: UnsafeMutableRawPointer?, + cppInstance: UnsafeMutableRawPointer?, + typeName: String) { + func format(_ p: UnsafeMutableRawPointer?) -> String { + guard let p else { return "0x0" } + return String(describing: p) + } + let message = "Swift subclass of \(typeName) turned into a zombie. (Swift instance: \(format(swiftInstance)), C++ instance: \(format(cppInstance)))" + switch behavior { + case .ignore: return + case .warn: print("Warning: \(message)") + case .terminate: fatalError("Error: \(message)") + } + } +} \ No newline at end of file diff --git a/source/Wrappers/PluginAndTfMacros.cpp b/source/Wrappers/PluginAndTfMacros.cpp new file mode 100644 index 0000000000..8b30de0a80 --- /dev/null +++ b/source/Wrappers/PluginAndTfMacros.cpp @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +#include "swiftUsd/Wrappers/PluginAndTfMacros.h" + +#include +#include "pxr/base/tf/type.h" +#include "pxr/imaging/hio/image.h" + + +namespace __Overlay { + class SwiftHioImageFactory: public pxr::HioImageFactoryBase { + public: + void*_Nonnull(*_Nonnull newImpl)(); + + SwiftHioImageFactory(void*_Nonnull(*_Nonnull newImpl)()) : newImpl(newImpl) {} + + pxr::HioImageSharedPtr New() const { + void* p = newImpl(); + return pxr::HioImageSharedPtr(static_cast(p)); + } + }; + + void setSwiftHioImagePluginFactory(std::string typeName, void*_Nonnull(*_Nonnull newImpl)()) { + pxr::TfType t = pxr::TfType::Declare(typeName); + t.SetFactory(std::make_unique(newImpl)); + } +} diff --git a/source/Wrappers/PluginAndTfMacros.h b/source/Wrappers/PluginAndTfMacros.h new file mode 100644 index 0000000000..6e4dbf1ab4 --- /dev/null +++ b/source/Wrappers/PluginAndTfMacros.h @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +#ifndef SWIFTUSD_WRAPPERS_PLUGINANDTFMACROS_H +#define SWIFTUSD_WRAPPERS_PLUGINANDTFMACROS_H + +#include + +namespace __Overlay { + void setSwiftHioImagePluginFactory(std::string typeName, void*_Nonnull(*_Nonnull newImpl)()); +} + +#endif /* SWIFTUSD_WRAPPERS_PLUGINANDTFMACROS_H */ diff --git a/source/Wrappers/PluginAndTfMacros.swift b/source/Wrappers/PluginAndTfMacros.swift new file mode 100644 index 0000000000..12e18caa04 --- /dev/null +++ b/source/Wrappers/PluginAndTfMacros.swift @@ -0,0 +1,72 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +#if canImport(Darwin) + +/// Apply this macro to a Swift function to make it participate +/// in TfRegistry operations. This is the Swift equivalent of the C++ +/// macro `TF_REGISTRY_FUNCTION`. +/// ```swift +/// @TF_REGISTRY_FUNCTION(pxr.TfType) +/// private func myFunction() { +/// ... +/// } +/// ``` +@attached(peer) +public macro TF_REGISTRY_FUNCTION(_ type: Any.Type) = #externalMacro(module: "_OpenUSD_MacroImplementations", type: "TF_REGISTRY_FUNCTION_Macro") + +/// Apply this macro to an OpenUSD plugin entry point to +/// register your plugin with the OpenUSD runtime. See +/// for more information. +@attached(peer) +public macro SWIFTUSD_PLUGIN() = #externalMacro(module: "_OpenUSD_MacroImplementations", type: "SWIFTUSD_PLUGIN_Macro") + +extension __Overlay { + fileprivate static nonisolated(unsafe) var Tf_RegistryInitCtor_libraries = Set() + + /// Don't call this from user code. + // + // Fake C++ static initialization of `_ARCH_ENSURE_PER_LIB_INIT(Tf_RegistryStaticInit, _tfRegistryInit);` + // in the C++ version of `TF_REGISTRY_DEFINE` in Swift. See the comment in + // _OpenUSD_MacroImplementations/PluginAndTfMacros.swift. + public static func Tf_RegistryInitCtor(_ library: String) { + if Tf_RegistryInitCtor_libraries.insert(library).inserted { + pxr.Tf_RegistryInitCtor(library) + } + } +} + +#else + +// Add unavailable macro declarations for any platforms we don't support so users get a clear diagnostic. +// +// In theory it shouldn't be too hard to add support for other platforms, since TF_REGISTRY_FUNCTION in Swift +// is just copying TF_REGISTRY_FUNCTION in C++, which delegates its platform-specific stuff to +// ARCH_CONSTRUCTOR in pxr/base/arch/attributes.h. + +@available(*, unavailable, message: "TF_REGISTRY_FUNCTION is only available on Apple platforms in Swift") +@attached(peer) +public macro TF_REGISTRY_FUNCTION(_ type: Any.Type) = #externalMacro(module: "_OpenUSD_MacroImplementations", type: "TF_REGISTRY_FUNCTION_Macro") + +@available(*, unavailable, message: "SWIFTUSD_PLUGIN is only available on Apple platforms in Swift") +@attached(peer) +public macro SWIFTUSD_PLUGIN() = #externalMacro(module: "_OpenUSD_MacroImplementations", type: "SWIFTUSD_PLUGIN_Macro") + +#endif // #if canImport(Darwin) \ No newline at end of file diff --git a/source/_OpenUSD_MacroImplementations/PluginAndTfMacros.swift b/source/_OpenUSD_MacroImplementations/PluginAndTfMacros.swift new file mode 100644 index 0000000000..879b2fe40e --- /dev/null +++ b/source/_OpenUSD_MacroImplementations/PluginAndTfMacros.swift @@ -0,0 +1,275 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + + +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +// VERY IMPORTANT: This must match the value of the +// `PXR_VERSION` macro in `pxr/pxr.h` in order for the @TF_REGISTRY_FUNCTION +// and @SWIFTUSD_PLUGIN macros to work properly in Swift. Otherwise they +// will silently fail to do anything at runtime. +// It should be a String whose contents is a valid CUnsignedInt literal, +// so that it can be interpolated into code produced by macros. +fileprivate let PXR_VERSION: String = "2605" + +enum CustomError: Error, CustomStringConvertible { + case message(String) + + var description: String { + switch self { + case .message(let text): + return text + } + } +} + +fileprivate func TF_REGISTRY_FUNCTION_Impl(context: some MacroExpansionContext, + argumentTypeText: String, + body: String) throws -> [DeclSyntax] { + // Compute identifiers and attributes + let userArchCtorName = context.makeUniqueName("arch_ctor__Tf_RegistryAdd") + let userArchFName = context.makeUniqueName("_Tf_RegistryAdd") + + let archPerLibInitCtorName = context.makeUniqueName("arch_ctor__tfRegistryInit") + let archPerLibInitFName = context.makeUniqueName("_tfRegistryInit") + + let stringifiedMetatypeName = "\"" + argumentTypeText + "\"" + + var usedAttribute = "@_used" + var sectionAttribute = "@_section" + if let configuration = context.buildConfiguration { + if configuration.compilerVersion >= .init(components: [6, 3]) { + usedAttribute = "@used" + sectionAttribute = "@section" + } else if try configuration.hasFeature(name: "SymbolLinkageMarkers") { + // pass + } else { + throw CustomError.message("Experimental feature 'SymbolLinkageMarkers' must be enabled to use @TF_REGISTRY_FUNCTION before Swift 6.3") + } + } + + let libraryNameCode: String + if false { + libraryNameCode = #""MFB_ALT_PACKAGE_NAME""# + } else { + libraryNameCode = #"#fileID.components(separatedBy: "/").first ?? "MFB_ALT_PACKAGE_NAME""# + } + + // Finally, form the decls to return. + // + // We can't use _ARCH_ENSURE_PER_LIB_INIT, so we emit a second ctor-func + // pair with lower priority to fake it. Priority is in [0,255], with larger + // values running later + + return [ + """ + \(raw: usedAttribute) \(raw: sectionAttribute)("__DATA,pxrctor") + private let \(userArchCtorName): (@convention(c) () -> (), CUnsignedInt, CUnsignedInt) = ( + function: \(userArchFName), + version: \(raw: PXR_VERSION), + priority: 100 + ) + + private func \(userArchFName)() { + pxr.Tf_RegistryInit.Add( + \(raw: libraryNameCode), + { _, _ in \(raw: body) }, + \(raw: stringifiedMetatypeName) + ) + } + + \(raw: usedAttribute) \(raw: sectionAttribute)("__DATA,pxrctor") + private let \(archPerLibInitCtorName): (@convention(c) () -> (), CUnsignedInt, CUnsignedInt) = ( + function: \(archPerLibInitFName), + version: \(raw: PXR_VERSION), + priority: 200 + ) + + private func \(archPerLibInitFName)() { + pxr.Tf_RegistryInit.Add( + \(raw: libraryNameCode), + { _, _ in __Overlay.Tf_RegistryInitCtor(\(raw: libraryNameCode)) }, + "TfType" + ) + } + """ + ] +} + +public struct TF_REGISTRY_FUNCTION_Macro: PeerMacro { + static func extractMetatype(from expr: ExprSyntax) throws -> [String] { + var result = [String]() + var expr = expr + + while true { + if let memberAccess = expr.as(MemberAccessExprSyntax.self) { + if result.isEmpty { + guard memberAccess.declName.baseName.text == "self" else { throw CustomError.message("@TF_REGISTRY_FUNCTION argument must be a metatype. (Did you forget to use `.self` after the type name?)") } + } + result.insert(memberAccess.declName.baseName.text, at: 0) + guard let base = memberAccess.base else { break } + expr = base + } else if let declReference = expr.as(DeclReferenceExprSyntax.self) { + result.insert(declReference.baseName.text, at: 0) + break + } else { + throw CustomError.message("@TF_REGISTRY_FUNCTION argument must be a metatype") + } + } + + return result + } + + + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + // TF_REGISTRY_FUNCTION requires a single metatype argument + var argumentTypeText: String = "" + switch node.arguments { + case .argumentList(let arguments): + guard arguments.count == 1 else { throw CustomError.message("@TF_REGISTRY_FUNCTION requires one argument") } + let metatypeComponents = try extractMetatype(from: arguments.first!.expression) + guard metatypeComponents.first == "pxr" else { throw CustomError.message("@TF_REGISTRY_FUNCTION argument must be a metatype of an OpenUSD type") } + guard metatypeComponents.count > 2 else { throw CustomError.message("@TF_REGISTRY_FUNCTION argument must be a metatype of an OpenUSD type") } + argumentTypeText = metatypeComponents[1.. () function + guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else { + throw CustomError.message("@TF_REGISTRY_FUNCTION only works on functions") + } + guard context.lexicalContext.isEmpty else { + throw CustomError.message("@TF_REGISTRY_FUNCTION can only be applied to top-level functions") + } + guard funcDecl.genericParameterClause == nil else { + throw CustomError.message("@TF_REGISTRY_FUNCTION does not support generic functions") + } + guard funcDecl.signature.effectSpecifiers == nil else { + throw CustomError.message("@TF_REGISTRY_FUNCTION does not support async or throwing functions") + } + guard funcDecl.signature.parameterClause.parameters.isEmpty else { + throw CustomError.message("@TF_REGISTRY_FUNCTION requires a function that takes no arguments") + } + if let returnClause = funcDecl.signature.returnClause, + returnClause.type.as(IdentifierTypeSyntax.self)?.name.text != "Void" { + throw CustomError.message("@TF_REGISTRY_FUNCTION requires a function that returns Void") + } + + + return try TF_REGISTRY_FUNCTION_Impl(context: context, argumentTypeText: argumentTypeText, + body: "\(funcDecl.name.text)()") + } +} + +public struct SWIFTUSD_PLUGIN_Macro: PeerMacro { + static func extractType(from type: TypeSyntax) -> [String] { + var result = [String]() + var type = type + + while true { + if let memberType = type.as(MemberTypeSyntax.self) { + result.insert(memberType.name.text, at: 0) + type = memberType.baseType + } else if let identifierType = type.as(IdentifierTypeSyntax.self) { + result.insert(identifierType.name.text, at: 0) + break + } else { + return [] + } + } + + return result + } + + + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + // SWIFTUSD_PLUGIN must be applied to a top-level non-generic final class that subclasses an OpenUSD plugin entry class + guard let classDecl = declaration.as(ClassDeclSyntax.self) else { + throw CustomError.message("@SWIFTUSD_PLUGIN only works on classes") + } + guard context.lexicalContext.isEmpty else { + throw CustomError.message("@SWIFTUSD_PLUGIN can only be applied to top-level classes") + } + guard classDecl.genericParameterClause == nil else { + throw CustomError.message("@SWIFTUSD_PLUGIN does not support generic classes") + } + + guard let inheritanceClause = classDecl.inheritanceClause else { + throw CustomError.message("@SWIFTUSD_PLUGIN only works on classes that inherit from OpenUSD classes") + } + + var hasFinal = false + for modifier in classDecl.modifiers { + if modifier.name.text == "final" { + hasFinal = true + break + } + } + guard hasFinal else { + throw CustomError.message("@SWIFTUSD_PLUGIN can only be applied to final classes") + } + + var inheritsCorrectly = false + for inheritedType in inheritanceClause.inheritedTypes { + if extractType(from: inheritedType.type) == ["Overlay", "HioImageSubclass"] { + inheritsCorrectly = true + break + } + } + guard inheritsCorrectly else { + throw CustomError.message("@SWIFTUSD_PLUGIN only works on classes that inherit from OpenUSD plugin entry classes") + } + + + // Compute identifiers + let userClassName = classDecl.name.text + let factoryName = "setSwiftHioImagePluginFactory" + let adapterName = "__Overlay.HioImage.CxxAdapter" + + // Finally, form the decl to return. + // We want to just return a `@TF_REGISTRY_FUNCTION`-annotated + // function, but instead have to effectively expand it ourselves + // to work around: + // rdar://181849395 (Declarations named with `MacroExpansionContext.makeUniqueName` not visible during nested macro expansion) + + return try TF_REGISTRY_FUNCTION_Impl(context: context, + argumentTypeText: "TfType", + body: + """ + __Overlay.\(factoryName)("\(userClassName)") { + Unmanaged<\(adapterName)>.passUnretained(\(userClassName).new().get_cxx()!).toOpaque() + } + """ + ) + } +} diff --git a/source/_OpenUSD_MacroImplementations/TfDiagnosticsMacros.swift b/source/_OpenUSD_MacroImplementations/TfDiagnosticsMacros.swift index 4f18d528c7..9cd06f9ff4 100644 --- a/source/_OpenUSD_MacroImplementations/TfDiagnosticsMacros.swift +++ b/source/_OpenUSD_MacroImplementations/TfDiagnosticsMacros.swift @@ -114,6 +114,8 @@ struct MacroPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ TF_VERIFY_Macro.self, TF_AXIOM_Macro.self, - TF_DEV_AXIOM_Macro.self + TF_DEV_AXIOM_Macro.self, + TF_REGISTRY_FUNCTION_Macro.self, + SWIFTUSD_PLUGIN_Macro.self ] } diff --git a/source/generated/SwiftSubclassCxx.cpp b/source/generated/SwiftSubclassCxx.cpp new file mode 100644 index 0000000000..2c4fa3d6c2 --- /dev/null +++ b/source/generated/SwiftSubclassCxx.cpp @@ -0,0 +1,168 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +// This file was generated by SwiftUsd-ast-answerer. Do not edit! +#include "swiftUsd/generated/SwiftSubclassCxx.h" + +#if SwiftUsd_PXR_ENABLE_IMAGING_SUPPORT +// MARK: pxr::HioImage subclassing +__Overlay::HioImage::CxxAdapter::~CxxAdapter() { + if (__swiftSubclass) { + __releaseSwiftSubclass_FP(__swiftSubclass); + __swiftSubclass = nullptr; + __releaseSwiftSubclass_FP = nullptr; + } +} + +// Non-private constructors of pxr::HioImage will be exposed to Swift +__Overlay::HioImage::CxxAdapter::CxxAdapter() : +pxr::HioImage() +{} +/* static */ __Overlay::HioImage::CxxAdapter*_Nonnull __Overlay::HioImage::CxxAdapter::__swiftNew() { + return new __Overlay::HioImage::CxxAdapter(); +} + +// Non-private destructor of CxxAdapter will be exposed to Swift +void __Overlay::HioImage::CxxAdapter::__swiftDeleteCxxAdapter() { + delete this; +} + +// Start total inheritance from pxr::HioImage +// Start fields from pxr::HioImage +// End fields from pxr::HioImage + +// Start methods from pxr::HioImage +/* static */ bool __Overlay::HioImage::CxxAdapter::__IsSupportedImageFile_forward(std::string const & arg0) { + return IsSupportedImageFile(arg0); +} + +/* static */ std::shared_ptr __Overlay::HioImage::CxxAdapter::__OpenForReading_forward(std::string const & arg0, int arg1, int arg2, pxr::HioImage::SourceColorSpace arg3, bool arg4) { + return OpenForReading(arg0, arg1, arg2, arg3, arg4); +} + +bool __Overlay::HioImage::CxxAdapter::__Read_forward(pxr::HioImage::StorageSpec const & arg0) { + return Read(arg0); +} +bool __Overlay::HioImage::CxxAdapter::Read(pxr::HioImage::StorageSpec const & arg0) { + return __Read_FP(__swiftSubclass, &arg0); +} + +bool __Overlay::HioImage::CxxAdapter::__ReadCropped_forward(int const arg0, int const arg1, int const arg2, int const arg3, pxr::HioImage::StorageSpec const & arg4) { + return ReadCropped(arg0, arg1, arg2, arg3, arg4); +} +bool __Overlay::HioImage::CxxAdapter::ReadCropped(int const arg0, int const arg1, int const arg2, int const arg3, pxr::HioImage::StorageSpec const & arg4) { + return __ReadCropped_FP(__swiftSubclass, arg0, arg1, arg2, arg3, &arg4); +} + +/* static */ std::shared_ptr __Overlay::HioImage::CxxAdapter::__OpenForWriting_forward(std::string const & arg0) { + return OpenForWriting(arg0); +} + +bool __Overlay::HioImage::CxxAdapter::__Write_forward(pxr::HioImage::StorageSpec const & arg0, pxr::VtDictionary const & arg1) { + return Write(arg0, arg1); +} +bool __Overlay::HioImage::CxxAdapter::Write(pxr::HioImage::StorageSpec const & arg0, pxr::VtDictionary const & arg1) { + return __Write_FP(__swiftSubclass, &arg0, &arg1); +} + +std::string const & __Overlay::HioImage::CxxAdapter::__GetFilename_forward() const { + return GetFilename(); +} +std::string const & __Overlay::HioImage::CxxAdapter::GetFilename() const { + return *__GetFilename_FP(__swiftSubclass); +} + +int __Overlay::HioImage::CxxAdapter::__GetWidth_forward() const { + return GetWidth(); +} +int __Overlay::HioImage::CxxAdapter::GetWidth() const { + return __GetWidth_FP(__swiftSubclass); +} + +int __Overlay::HioImage::CxxAdapter::__GetHeight_forward() const { + return GetHeight(); +} +int __Overlay::HioImage::CxxAdapter::GetHeight() const { + return __GetHeight_FP(__swiftSubclass); +} + +pxr::HioFormat __Overlay::HioImage::CxxAdapter::__GetFormat_forward() const { + return GetFormat(); +} +pxr::HioFormat __Overlay::HioImage::CxxAdapter::GetFormat() const { + return __GetFormat_FP(__swiftSubclass); +} + +int __Overlay::HioImage::CxxAdapter::__GetBytesPerPixel_forward() const { + return GetBytesPerPixel(); +} +int __Overlay::HioImage::CxxAdapter::GetBytesPerPixel() const { + return __GetBytesPerPixel_FP(__swiftSubclass); +} + +int __Overlay::HioImage::CxxAdapter::__GetNumMipLevels_forward() const { + return GetNumMipLevels(); +} +int __Overlay::HioImage::CxxAdapter::GetNumMipLevels() const { + return __GetNumMipLevels_FP(__swiftSubclass); +} + +bool __Overlay::HioImage::CxxAdapter::__IsColorSpaceSRGB_forward() const { + return IsColorSpaceSRGB(); +} +bool __Overlay::HioImage::CxxAdapter::IsColorSpaceSRGB() const { + return __IsColorSpaceSRGB_FP(__swiftSubclass); +} + +bool __Overlay::HioImage::CxxAdapter::__GetMetadata_forward(pxr::TfToken const & arg0, pxr::VtValue * arg1) const { + return GetMetadata(arg0, arg1); +} +bool __Overlay::HioImage::CxxAdapter::GetMetadata(pxr::TfToken const & arg0, pxr::VtValue * arg1) const { + return __GetMetadata_FP(__swiftSubclass, &arg0, arg1); +} + +bool __Overlay::HioImage::CxxAdapter::__GetSamplerMetadata_forward(pxr::HioAddressDimension arg0, pxr::HioAddressMode * arg1) const { + return GetSamplerMetadata(arg0, arg1); +} +bool __Overlay::HioImage::CxxAdapter::GetSamplerMetadata(pxr::HioAddressDimension arg0, pxr::HioAddressMode * arg1) const { + return __GetSamplerMetadata_FP(__swiftSubclass, arg0, arg1); +} + +bool __Overlay::HioImage::CxxAdapter::___OpenForReading_forward(std::string const & arg0, int arg1, int arg2, pxr::HioImage::SourceColorSpace arg3, bool arg4) { + return _OpenForReading(arg0, arg1, arg2, arg3, arg4); +} +bool __Overlay::HioImage::CxxAdapter::_OpenForReading(std::string const & arg0, int arg1, int arg2, pxr::HioImage::SourceColorSpace arg3, bool arg4) { + return ___OpenForReading_FP(__swiftSubclass, &arg0, arg1, arg2, arg3, arg4); +} + +bool __Overlay::HioImage::CxxAdapter::___OpenForWriting_forward(std::string const & arg0) { + return _OpenForWriting(arg0); +} +bool __Overlay::HioImage::CxxAdapter::_OpenForWriting(std::string const & arg0) { + return ___OpenForWriting_FP(__swiftSubclass, &arg0); +} + +// End methods from pxr::HioImage +// End total inheritance from pxr::HioImage +#endif // #if SwiftUsd_PXR_ENABLE_IMAGING_SUPPORT + + + + diff --git a/source/generated/SwiftSubclassCxx.h b/source/generated/SwiftSubclassCxx.h new file mode 100644 index 0000000000..5bcc143c16 --- /dev/null +++ b/source/generated/SwiftSubclassCxx.h @@ -0,0 +1,149 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +// This file was generated by SwiftUsd-ast-answerer. Do not edit! +#ifndef SWIFTUSD_GENERATED_SWIFTSUBCLASSCXX_H +#define SWIFTUSD_GENERATED_SWIFTSUBCLASSCXX_H + +#include "swiftUsd/defines.h" + +#if SwiftUsd_PXR_ENABLE_IMAGING_SUPPORT +#include "pxr/imaging/hio/image.h" +#endif // #if SwiftUsd_PXR_ENABLE_IMAGING_SUPPORT + +#if SwiftUsd_PXR_ENABLE_IMAGING_SUPPORT +// MARK: pxr::HioImage subclassing +namespace __Overlay { + namespace HioImage { + class CxxAdapter final: public pxr::HioImage { + public: + // Swift pointer + typedef void*_Nullable SwiftSubclass; + + SwiftSubclass __swiftSubclass = nullptr; + void (*_Nullable __releaseSwiftSubclass_FP)(SwiftSubclass) = nullptr; + virtual ~CxxAdapter(); + + // Conditional downcasting + static inline CxxAdapter*_Nullable __dynamic_cast(pxr::HioImage*_Nullable p) { + return dynamic_cast(p); + } + + // Unconditional upcasting + static inline pxr::HioImage*_Nonnull __static_cast(CxxAdapter*_Nonnull p, pxr::HioImage*_Nullable unused) { + return static_cast(p); + } + + // Public destructor of pxr::HioImage will be exposed to Swift + static inline void __swiftDeleteBase(pxr::HioImage*_Nonnull p) { + delete p; + } + + // Non-private constructors of pxr::HioImage will be exposed to Swift + CxxAdapter(); + static CxxAdapter*_Nonnull __swiftNew(); + + // Non-private destructor of CxxAdapter will be exposed to Swift + void __swiftDeleteCxxAdapter(); + + // Start total inheritance from pxr::HioImage + // Start fields from pxr::HioImage + // End fields from pxr::HioImage + + // Start methods from pxr::HioImage + static bool __IsSupportedImageFile_forward(std::string const & filename); + + static std::shared_ptr __OpenForReading_forward(std::string const & filename, int subimage = 0, int mip = 0, pxr::HioImage::SourceColorSpace sourceColorSpace = pxr::HioImage::Auto, bool suppressErrors = false); + + bool (*_Nullable __Read_FP)(SwiftSubclass, pxr::HioImage::StorageSpec const * storage) = nullptr; + bool __Read_forward(pxr::HioImage::StorageSpec const & storage); + bool Read(pxr::HioImage::StorageSpec const & storage) override final; + + bool (*_Nullable __ReadCropped_FP)(SwiftSubclass, int const cropTop, int const cropBottom, int const cropLeft, int const cropRight, pxr::HioImage::StorageSpec const * storage) = nullptr; + bool __ReadCropped_forward(int const cropTop, int const cropBottom, int const cropLeft, int const cropRight, pxr::HioImage::StorageSpec const & storage); + bool ReadCropped(int const cropTop, int const cropBottom, int const cropLeft, int const cropRight, pxr::HioImage::StorageSpec const & storage) override final; + + static std::shared_ptr __OpenForWriting_forward(std::string const & filename); + + bool (*_Nullable __Write_FP)(SwiftSubclass, pxr::HioImage::StorageSpec const * storage, pxr::VtDictionary const * metadata) = nullptr; + bool __Write_forward(pxr::HioImage::StorageSpec const & storage, pxr::VtDictionary const & metadata = pxr::VtDictionary()); + bool Write(pxr::HioImage::StorageSpec const & storage, pxr::VtDictionary const & metadata = pxr::VtDictionary()) override final; + + std::string * (*_Nullable __GetFilename_FP)(SwiftSubclass) = nullptr; + std::string const & __GetFilename_forward() const; + std::string const & GetFilename() const override final; + + int (*_Nullable __GetWidth_FP)(SwiftSubclass) = nullptr; + int __GetWidth_forward() const; + int GetWidth() const override final; + + int (*_Nullable __GetHeight_FP)(SwiftSubclass) = nullptr; + int __GetHeight_forward() const; + int GetHeight() const override final; + + pxr::HioFormat (*_Nullable __GetFormat_FP)(SwiftSubclass) = nullptr; + pxr::HioFormat __GetFormat_forward() const; + pxr::HioFormat GetFormat() const override final; + + int (*_Nullable __GetBytesPerPixel_FP)(SwiftSubclass) = nullptr; + int __GetBytesPerPixel_forward() const; + int GetBytesPerPixel() const override final; + + int (*_Nullable __GetNumMipLevels_FP)(SwiftSubclass) = nullptr; + int __GetNumMipLevels_forward() const; + int GetNumMipLevels() const override final; + + bool (*_Nullable __IsColorSpaceSRGB_FP)(SwiftSubclass) = nullptr; + bool __IsColorSpaceSRGB_forward() const; + bool IsColorSpaceSRGB() const override final; + + bool (*_Nullable __GetMetadata_FP)(SwiftSubclass, pxr::TfToken const * key, pxr::VtValue * value) = nullptr; + bool __GetMetadata_forward(pxr::TfToken const & key, pxr::VtValue * value) const; + bool GetMetadata(pxr::TfToken const & key, pxr::VtValue * value) const override final; + + bool (*_Nullable __GetSamplerMetadata_FP)(SwiftSubclass, pxr::HioAddressDimension dim, pxr::HioAddressMode * param) = nullptr; + bool __GetSamplerMetadata_forward(pxr::HioAddressDimension dim, pxr::HioAddressMode * param) const; + bool GetSamplerMetadata(pxr::HioAddressDimension dim, pxr::HioAddressMode * param) const override final; + + bool (*_Nullable ___OpenForReading_FP)(SwiftSubclass, std::string const * filename, int subimage, int mip, pxr::HioImage::SourceColorSpace sourceColorSpace, bool suppressErrors) = nullptr; + bool ___OpenForReading_forward(std::string const & filename, int subimage, int mip, pxr::HioImage::SourceColorSpace sourceColorSpace, bool suppressErrors); + bool _OpenForReading(std::string const & filename, int subimage, int mip, pxr::HioImage::SourceColorSpace sourceColorSpace, bool suppressErrors) override final; + + bool (*_Nullable ___OpenForWriting_FP)(SwiftSubclass, std::string const * filename) = nullptr; + bool ___OpenForWriting_forward(std::string const & filename); + bool _OpenForWriting(std::string const & filename) override final; + + template + bool GetMetadata(pxr::TfToken const & key, T * value) const { + return this->pxr::HioImage::GetMetadata(key, value); + } + + // End methods from pxr::HioImage + + // End total inheritance from pxr::HioImage + } SWIFT_UNSAFE_REFERENCE; + } +} +#endif // #if SwiftUsd_PXR_ENABLE_IMAGING_SUPPORT + + + + +#endif /* SWIFTUSD_GENERATED_SWIFTSUBCLASSCXX_H */ diff --git a/source/generated/SwiftSubclassCxx.md b/source/generated/SwiftSubclassCxx.md new file mode 100644 index 0000000000..e5865e0ac2 --- /dev/null +++ b/source/generated/SwiftSubclassCxx.md @@ -0,0 +1,12 @@ +# OpenUSD types that can be subclassed in Swift + +@Comment { This file was generated by ast-answerer. Do not edit! } + +## Overview + +This page lists which Usd types can be subclassed from Swift. +Subclassing OpenUSD types in Swift should only be done for writing an OpenUSD plugin, due to an increased risk of memory-safety issues. See for more information. + + +### Hio types +- ``OpenUSD/C++/pxr/HioImage`` diff --git a/source/generated/SwiftSubclassCxx.swift b/source/generated/SwiftSubclassCxx.swift new file mode 100644 index 0000000000..845972c8ff --- /dev/null +++ b/source/generated/SwiftSubclassCxx.swift @@ -0,0 +1,248 @@ +//===----------------------------------------------------------------------===// +// This source file is part of github.com/apple/SwiftUsd +// +// Copyright © 2025 Apple Inc. and the SwiftUsd project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//===----------------------------------------------------------------------===// + +// This file was generated by SwiftUsd-ast-answerer. Do not edit! + + +public enum __OverlaySwift{} + + + +#if canImport(SwiftUsd_PXR_ENABLE_IMAGING_SUPPORT) +// MARK: pxr.HioImage subclassing +extension __Overlay.HioImage.CxxAdapter { + public var swift: Overlay.HioImageSubclass { + Unmanaged<__OverlaySwift.HioImage.SwiftAdapter>.fromOpaque(__swiftSubclass!).takeUnretainedValue() as! Overlay.HioImageSubclass + } +} + +// Conditional downcasting +extension pxr.HioImage { + public func `as`(_ t: T.Type = T.self) -> T? { + __Overlay.HioImage.CxxAdapter.__dynamic_cast(self)?.swift as? T + } +} + + +// Public destructor of pxr.HioImage will be exposed to Swift + +extension pxr.HioImage { + /// Deletes the argument using `delete x;` in C++. + /// + /// The caller is responsible for ensuring that the argument + /// is not used after this function returns. + public static func delete(_ x: consuming pxr.HioImage) { + __Overlay.HioImage.CxxAdapter.__swiftDeleteBase(x) + } +} + +public extension __OverlaySwift { + public enum HioImage { + open class SwiftAdapter { + private typealias UnmanagedSelf = Unmanaged<__OverlaySwift.HioImage.SwiftAdapter> + + // Detect calling __wireToCxx more than once, which is against core invariants + private var __swiftHasWired: Bool + + // Detect if we're a zombie object to avoid setting __swiftDidDeinit.pointee in deinit after __releaseSwiftSubclass_FP deallocated it + private var __swiftIsZombie: Bool + + // Detect whether or not a call to Unmanaged.release() deinitialized us + // or if there's an outstanding strong pointer, making us a zombie + private var __swiftDidDeinit: UnsafeMutablePointer + + public required init() { + self.__swiftHasWired = false + self.__swiftIsZombie = false + self.__swiftDidDeinit = UnsafeMutablePointer.allocate(capacity: 1) + self.__swiftDidDeinit.initialize(to: false) + } + + // Cxx pointer + private var __cxxSubclass: __Overlay.HioImage.CxxAdapter! + + public func get_cxx() -> __Overlay.HioImage.CxxAdapter? { __cxxSubclass } + + // Unconditional upcasting + public func `as`(_ t: pxr.HioImage.Type) -> pxr.HioImage { + __Overlay.HioImage.CxxAdapter.__static_cast(__cxxSubclass, nil) + } + + // Pointer/ref-returning method allocations + private var __returnIndirectionAllocation_GetFilename: UnsafeMutablePointer? + + private func __wireToCxx(_ __cxxSubclass: __Overlay.HioImage.CxxAdapter) { + func slf(_ raw: UnsafeMutableRawPointer?) -> Overlay.HioImageSubclass { + UnmanagedSelf.fromOpaque(raw!).takeUnretainedValue() as! Overlay.HioImageSubclass + } + if self.__swiftHasWired { + fatalError("Cannot call __wireToCxx more than once on a given instance. Don't call this from user code.") + } else { + self.__swiftHasWired = true + } + + self.__cxxSubclass = __cxxSubclass + __cxxSubclass.__swiftSubclass = UnmanagedSelf.passRetained(self).toOpaque() + __cxxSubclass.__releaseSwiftSubclass_FP = { + let swiftDidDeinitPointer = slf($0).__swiftDidDeinit + let cxxSubclassPointer = slf($0).__cxxSubclass + slf($0).__cxxSubclass = nil + UnmanagedSelf.fromOpaque($0!).release() + let isZombie = !swiftDidDeinitPointer.pointee + swiftDidDeinitPointer.deallocate() + if isZombie { + slf($0).__swiftIsZombie = true + let cppInstance: UnsafeMutableRawPointer? = if let cxxSubclassPointer { Unmanaged<__Overlay.HioImage.CxxAdapter>.passUnretained(cxxSubclassPointer).toOpaque() } else { nil } + __Overlay.SwiftSubclassCxx_zombieCreated(swiftInstance: $0, cppInstance: cppInstance, typeName: "pxr.HioImage") + } + } + // Start wire virtual methods from pxr.HioImage + __cxxSubclass.__Read_FP = { slf($0).Read($1.pointee) } + __cxxSubclass.__ReadCropped_FP = { slf($0).ReadCropped($1, $2, $3, $4, $5.pointee) } + __cxxSubclass.__Write_FP = { slf($0).Write($1.pointee, $2.pointee) } + __cxxSubclass.__GetFilename_FP = { + let newValue = slf($0).GetFilename() + if slf($0).__returnIndirectionAllocation_GetFilename?.pointee != newValue { + if let p = slf($0).__returnIndirectionAllocation_GetFilename { + p.deinitialize(count: 1).deallocate() + } + slf($0).__returnIndirectionAllocation_GetFilename = UnsafeMutablePointer.allocate(capacity: 1) + slf($0).__returnIndirectionAllocation_GetFilename!.initialize(to: newValue) + } + return slf($0).__returnIndirectionAllocation_GetFilename + } + __cxxSubclass.__GetWidth_FP = { slf($0).GetWidth() } + __cxxSubclass.__GetHeight_FP = { slf($0).GetHeight() } + __cxxSubclass.__GetFormat_FP = { slf($0).GetFormat() } + __cxxSubclass.__GetBytesPerPixel_FP = { slf($0).GetBytesPerPixel() } + __cxxSubclass.__GetNumMipLevels_FP = { slf($0).GetNumMipLevels() } + __cxxSubclass.__IsColorSpaceSRGB_FP = { slf($0).IsColorSpaceSRGB() } + __cxxSubclass.__GetMetadata_FP = { slf($0).GetMetadata($1.pointee, $2) } + __cxxSubclass.__GetSamplerMetadata_FP = { slf($0).GetSamplerMetadata($1, $2) } + __cxxSubclass.___OpenForReading_FP = { slf($0)._OpenForReading($1.pointee, $2, $3, $4, $5) } + __cxxSubclass.___OpenForWriting_FP = { slf($0)._OpenForWriting($1.pointee) } + // End wire virtual methods from pxr.HioImage + + } // public func __wireToCxx + + deinit { + // __releaseSwiftSubclass_FP, which is run by the C++ destructor, sets __cxxSubclass to nil. + // CxxAdapter has a strong pointer to SwiftAdapter, so SwiftAdapter.deinit running before ~CxxAdapter is a user-induced memory-safety issue + if self.__cxxSubclass != nil { + fatalError("Swift deinit running before C++ destructor ran violates invariants. Did you overrelease the Swift subclass instance?") + } + __returnIndirectionAllocation_GetFilename?.deinitialize(count: 1).deallocate() + if !self.__swiftIsZombie { + // Set the flag but don't deallocate. The releaseFP will deallocate after checking if this flag was set + self.__swiftDidDeinit.pointee = true + } + } // deinit + + // Non-private constructors of pxr.HioImage will be exposed to Swift + + /// Creates a new instance of this subclass using `new pxr::HioImage(...);` in C++. + /// + /// The caller is responsible for deleting the instance in one of the following ways: + /// - Using `Overlay.HioImageSubclass.delete(_:)` in Swift + /// - Using `pxr.HioImage.delete(_:)` on the value returned by `self.get_cxx()` + /// - Using `delete p;` in C++ on the value returned by `self.get_cxx()` + /// - Passing the value returned by `self.get_cxx()` to something that will ensure it is eventually deleted, like `std::unique_ptr` or `std::shared_ptr` + /// Note that Swift ARC will _not_ automatically free the returned instance. Dropping the last strong reference in user code to the instance can result in memory leaks. + public static func new() -> Overlay.HioImageSubclass { + let swiftAdapter = Self.init() + if type(of: swiftAdapter) == __OverlaySwift.HioImage.SwiftAdapter.self { + fatalError("Cannot construct instance of abstract base class Overlay.HioImageSubclass") + } + guard swiftAdapter is Overlay.HioImageSubclass else { + fatalError("Subclass \(type(of: swiftAdapter)) of pxr.HioImage must inherit from Overlay.HioImageSubclass") + } + let cxxAdapter = __Overlay.HioImage.CxxAdapter.__swiftNew() + swiftAdapter.__wireToCxx(cxxAdapter) + return swiftAdapter as! Overlay.HioImageSubclass + } + + // Non-private destructor of CxxAdapter will be exposed to Swift + + /// Deletes an instance of this subclass using `delete p;` in C++ on the pointer + /// returned by `self.get_cxx()`. + /// + /// The caller is responsible for ensuring that the pointer returned by + /// `self.get_cxx()` is not used after this function returns. + /// + /// Due to the way that SwiftUsd's mechanisms for subclassing C++ types from Swift are implemented, + /// this Swift instance may become a "zombie" if there is a strong reference keeping it + /// alive after this function returns. By default, SwiftUsd will print a warning to stdout + /// when this occurs. You can change this behavior by setting the environment variable + /// `SWIFTUSD_SWIFT_SUBCLASS_CXX_ZOMBIE_CREATION_BEHAVIOR` to `ignore` to suppress the warning, + /// or `terminate` to immediately exit the program. + /// After a zombie is created, any use of it might safely terminate the program. + public static func delete(_ swiftAdapter: consuming __OverlaySwift.HioImage.SwiftAdapter) { + let cxxAdapter = swiftAdapter.__cxxSubclass + _ = consume swiftAdapter + cxxAdapter?.__swiftDeleteCxxAdapter() + } + + // Start total inheritance from pxr.HioImage + // Start fields from pxr::HioImage + // End fields from pxr::HioImage + + // Start methods from pxr::HioImage + public static func IsSupportedImageFile(_ filename: std.string) -> CBool { + __Overlay.HioImage.CxxAdapter.__IsSupportedImageFile_forward(filename) + } + public static func OpenForReading(_ filename: std.string, _ subimage: CInt, _ mip: CInt, _ sourceColorSpace: pxr.HioImage.SourceColorSpace, _ suppressErrors: CBool) -> pxr.HioImageSharedPtr { + __Overlay.HioImage.CxxAdapter.__OpenForReading_forward(filename, subimage, mip, sourceColorSpace, suppressErrors) + } + public static func OpenForWriting(_ filename: std.string) -> pxr.HioImageSharedPtr { + __Overlay.HioImage.CxxAdapter.__OpenForWriting_forward(filename) + } + // End methods from pxr::HioImage + // End total inheritance from pxr.HioImage + + public protocol PureVirtuals { + // Start methods from pxr.HioImage + func Read(_ storage: pxr.HioImage.StorageSpec) -> CBool + func ReadCropped(_ cropTop: CInt, _ cropBottom: CInt, _ cropLeft: CInt, _ cropRight: CInt, _ storage: pxr.HioImage.StorageSpec) -> CBool + func Write(_ storage: pxr.HioImage.StorageSpec, _ metadata: pxr.VtDictionary) -> CBool + func GetFilename() -> std.string + func GetWidth() -> CInt + func GetHeight() -> CInt + func GetFormat() -> pxr.HioFormat + func GetBytesPerPixel() -> CInt + func GetNumMipLevels() -> CInt + func IsColorSpaceSRGB() -> CBool + func GetMetadata(_ key: pxr.TfToken, _ value: UnsafeMutablePointer?) -> CBool + func GetSamplerMetadata(_ dim: pxr.HioAddressDimension, _ param: UnsafeMutablePointer?) -> CBool + func _OpenForReading(_ filename: std.string, _ subimage: CInt, _ mip: CInt, _ sourceColorSpace: pxr.HioImage.SourceColorSpace, _ suppressErrors: CBool) -> CBool + func _OpenForWriting(_ filename: std.string) -> CBool + // End methods from pxr.HioImage + } // public protocol PureVirtuals + } + } +} + +extension Overlay { + public typealias HioImageSubclass = __OverlaySwift.HioImage.SwiftAdapter & __OverlaySwift.HioImage.SwiftAdapter.PureVirtuals +} +#endif // #if canImport(SwiftUsd_PXR_ENABLE_IMAGING_SUPPORT) + + + + diff --git a/source/generated/_OpenUSD_SwiftBindingHelpers.apinotes b/source/generated/_OpenUSD_SwiftBindingHelpers.apinotes index a492d840c1..9089f4d77a 100644 --- a/source/generated/_OpenUSD_SwiftBindingHelpers.apinotes +++ b/source/generated/_OpenUSD_SwiftBindingHelpers.apinotes @@ -506,6 +506,11 @@ Namespaces: SwiftRetainOp: __retain__ZN3pxr27GarchGLPlatformDebugContextE SwiftReleaseOp: __release__ZN3pxr27GarchGLPlatformDebugContextE + - Name: HioImage + SwiftImportAs: reference + SwiftRetainOp: immortal + SwiftReleaseOp: immortal + - Name: HioImageRegistry SwiftImportAs: reference SwiftRetainOp: immortal diff --git a/source/swiftUsd.h b/source/swiftUsd.h index f276ae64e6..49086b1beb 100644 --- a/source/swiftUsd.h +++ b/source/swiftUsd.h @@ -47,9 +47,12 @@ #include "swiftUsd/Wrappers/ArWritableAssetWrapper.h" #include "swiftUsd/Wrappers/ExecUsdSystemWrapper.h" #include "swiftUsd/Wrappers/HgiGLWrapper.h" +#ifdef __OBJC__ #include "swiftUsd/Wrappers/HgiMetalWrapper.h" +#endif #include "swiftUsd/Wrappers/HgiWrapper.h" #include "swiftUsd/Wrappers/HioImageWrapper.h" +#include "swiftUsd/Wrappers/PluginAndTfMacros.h" #include "swiftUsd/Wrappers/UsdAppUtilsFrameRecorderWrapper.h" #include "swiftUsd/Wrappers/UsdImagingGLEngineWrapper.h" #include "swiftUsd/Wrappers/UsdPrimTypeInfoWrapper.h"