Skip to content

[VtArray] Add zero-copy withUnsafeBufferPointer and single-copy buffer init#39

Open
furbytm wants to merge 1 commit into
apple:mainfrom
furbytm:feat/vt-array-zerocopy-singlecopy
Open

[VtArray] Add zero-copy withUnsafeBufferPointer and single-copy buffer init#39
furbytm wants to merge 1 commit into
apple:mainfrom
furbytm:feat/vt-array-zerocopy-singlecopy

Conversation

@furbytm

@furbytm furbytm commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds a borrowing withUnsafeBufferPointer(_:) to the VtArray protocols, backed by cdata() so the read never detaches the copy-on-write storage, and an init(_: UnsafeBufferPointer) that assigns a contiguous buffer in a single copy rather than element-by-element push_backs. The initializer is the write-side complement of cdata()'s zero-copy reads.

Related Issues / PRs

Checklist

  • Followed the contribution guidelines in CONTRIBUTING.md
  • Omitted local generation artifacts (Package.swift / swift-package) from the commit
  • Added corresponding verification tests in the test suite

@furbytm
furbytm force-pushed the feat/vt-array-zerocopy-singlecopy branch from e64c816 to 324687f Compare July 23, 2026 05:26
@maddyadams

Copy link
Copy Markdown
Contributor

This doesn't compile on Swift 6.1 and Swift 6.2, but I think this modification should work for Swift 6.1-6.4. If it makes sense to you, could you update your PR to use this?

// expose `assign` in a way Swift will import. VtArray uses copy-on-write                                                                                                                                                                       
// storage, so this constructs the array with a single copy of the source                                                                                                                                                                       
// range, rather than the element-by-element push_back the Collection                                                                                                                                                                           
// initializer falls back to.                                                                                                                                                                                                                   
//                                                                                                                                                                                                                                              
// Swift 6.3 fixes https://github.com/swiftlang/swift/pull/82161, but on Swift 6.1 and Swift 6.2,                                                                                                                                               
// templated methods taking pointers without nullability annotations are assumed to be non-null,                                                                                                                                                
// and templated methods taking pointers with nullability annotations aren't imported. So,                                                                                                                                                      
// use ptrdiff_t instead                                                                                                                                                                                                                        
template <typename VtArray>
inline void VtArray_assign(VtArray& array, ptrdiff_t frontPtr, ptrdiff_t backPtr) {
    using Element = typename VtArray::ElementType;
    static_assert(std::is_same_v<VtArray, pxr::VtArray<Element>>);
    const Element* front = reinterpret_cast<const Element*>(frontPtr);
    const Element* back = reinterpret_cast<const Element*>(backPtr);
    array.assign(front, back);
}
public mutating func __assign(_ p: UnsafeBufferPointer<ElementType>) {
    __Overlay.VtArray_assign(&self, Int(bitPattern: p.baseAddress), Int(bitPattern: p.baseAddress.map { $0 + p.count }))
}

…r init

Add a borrowing withUnsafeBufferPointer(_:) to the VtArray protocols,
backed by cdata() so the read never detaches the copy-on-write storage,
and an init(_: UnsafeBufferPointer) that assigns a contiguous buffer in a
single copy rather than element-by-element push_backs. The initializer is
the write-side complement of cdata()'s zero-copy reads.

Signed-off-by: Furby™ <furby@wabi.foundation>
@furbytm
furbytm force-pushed the feat/vt-array-zerocopy-singlecopy branch from 324687f to 89318d1 Compare July 24, 2026 00:23
@furbytm

furbytm commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

This doesn't compile on Swift 6.1 and Swift 6.2, but I think this modification should work for Swift 6.1-6.4. If it makes sense to you, could you update your PR to use this?

Done! Your modification is in, tested with Swift 6.3.2.

@furbytm

furbytm commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Also, to make this less of a recurring thing for you to catch...

I've got SwiftUsd's run-tests.yml running against my own fork on a self-hosted runner (here) with Swift 6.2 via Swiftly. Once I've confirmed it is working correctly, I can build/test locally against whatever's current (6.3.2 right now) and lean on those workflow runs to catch anything version specific before it reaches you.

Edited later, after some trial and error...

It also appears that workflow explicitly runs swiftly unlink, so I also set the following environment variable

export TOOLCHAINS=$(plutil -extract CFBundleIdentifier raw ~/Library/Developer/Toolchains/swift-6.2.4-RELEASE.xctoolchain/Info.plist)

matching what swiftly install 6.2 set up.

Then I had to add a wrapper script for swift that was prepended to $PATH to mock the swiftlang version string format expected by the python scripts (since keeping CI specific to Xcode Swift toolchains makes sense, this is just for developer/contributor ergonomics to easily compile across different Swift versions):

#!/bin/bash
if [ "$1" == "--version" ]; then
    REAL_OUTPUT=$(command -p swift --version)
    VERSION_NUM=$(echo "$REAL_OUTPUT" | grep -oE 'version [0-9.]+' | head -n 1 | awk '{print $2}')
    echo "swift-driver version: 1.148.6 Apple Swift version $VERSION_NUM (swiftlang-$VERSION_NUM clang-2100.1.1.101)"
    echo "Target: arm64-apple-macosx26.0"
else
    exec command -p swift "$@"
fi

Lastly, it appears that the only real blocker with using Swiftly-installed toolchains are the compiler errors regarding the missing <swift/bridging> header, a fallback pattern could be used similar to wabiverse/swift-usd's implementation to manually define SWIFT_ interop macros as needed:

#ifndef __PXR_BASE_ARCH_SWIFT_INTEROP_H__
#define __PXR_BASE_ARCH_SWIFT_INTEROP_H__

#ifdef __APPLE__
# if __has_include(<swift/bridging>)
#  include <swift/bridging> // the only time <swift/bridging> is ever included, safely guarded by  __has_include
# endif // __has_include(<swift/bridging>)
#endif // __APPLE__

// Safely (re)define swift cxx interop macros if they were not already provided
// by the compiler via <swift/bridging>. This fallback also ensures that the
// interop macros are available on non-Apple platforms too (e.g., Linux).

#if defined(__has_attribute) && !defined(_CXX_INTEROP_HAS_ATTRIBUTE)
#define _CXX_INTEROP_HAS_ATTRIBUTE(x) __has_attribute(x)
#elif !defined(_CXX_INTEROP_HAS_ATTRIBUTE)
#define _CXX_INTEROP_HAS_ATTRIBUTE(x) 0
#endif

#if _CXX_INTEROP_HAS_ATTRIBUTE(swift_attr)

#if !defined(SWIFT_SELF_CONTAINED)
#define SWIFT_SELF_CONTAINED __attribute__((swift_attr("import_owned")))
#endif // SWIFT_SELF_CONTAINED

// ...
// do this for all swift/cxx interop macros defined in swift/bridging
// ...

#else // !_CXX_INTEROP_HAS_ATTRIBUTE(swift_attr)

// Empty defines for compilers that don't support `attribute(swift_attr)`.

#if !defined(SWIFT_SELF_CONTAINED)
#define SWIFT_SELF_CONTAINED
#endif // SWIFT_SELF_CONTAINED

// ...
// do this for all swift/cxx interop empty macros defined in swift/bridging
// ...

#endif // _CXX_INTEROP_HAS_ATTRIBUTE(swift_attr)

#if defined(_CXX_INTEROP_HAS_ATTRIBUTE)
#undef _CXX_INTEROP_HAS_ATTRIBUTE
#endif // _CXX_INTEROP_HAS_ATTRIBUTE

#endif // __PXR_BASE_ARCH_SWIFT_INTEROP_H__

@maddyadams

Copy link
Copy Markdown
Contributor

I've got SwiftUsd's run-tests.yml running against my own fork on a self-hosted runner (here) with Swift 6.2 via Swiftly.

Neat. It might also interest you to look at ci-at-desk, which is a custom single-machine CI system for specifically SwiftUsd's needs, inspired by GitHub Actions. ci-at-desk is now my main CI system, because it means I can kick off a huge test run on as many Xcodes, Swiftly toolchains, simulators, and physical iOS/visionOS devices as I want and immediately dig into test failures without having to download artifacts from all the distributed runners. (It also caches OpenUSD builds in a way I was never really able to do with GitHub Actions because GitHub Actions caches are scoped to branches.)

It also appears that workflow explicitly runs swiftly unlink

Yeah, for reasons I'll mention later, I prefer to use swiftly run cmd +foo and have swiftly unlinked otherwise so it doesn't manage my toolchains for me.

Then I had to add a wrapper script for swift that was prepended to $PATH to mock the swiftlang version string format expected by the python scripts (since keeping CI specific to Xcode Swift toolchains makes sense, this is just for developer/contributor ergonomics to easily compile across different Swift versions)

Oh, interesting, I guess I made the version parser too strict. Feel free to open an issue/PR about making it flexible enough to handle both Xcode and Swiftly toolchains.

Lastly, it appears that the only real blocker with using Swiftly-installed toolchains are the compiler errors regarding the missing <swift/bridging> header

Yeah, this is the big reason I don't like having swiftly manage my toolchains for me. Xcode can find #include <swift/bridging> just fine, but currently Swiftly toolchains can't. This PR would fix that, but it just got delayed from being part of Swift 6.4. In the meantime, you can use something like export CPLUS_INCLUDE_PATH=$(dirname $(swiftly run which swift +main-snapshot))/../include to add to the include path list that Swift/Clang uses (see also here), but you should also be aware that that workaround broke for swift build invocations (but not xcodebuild invocations) in May: rdar://177175896 (swift build with main-snapshot-2026-05-07 not respecting CPLUS_INCLUDE_PATH; can't #include <swift/bridging> (regression)).

Again though, don't feel like you have to catch every last esoteric Swift-Cxx interop gotcha, if your PRs compile and the tests pass on Swift 6.3 (or 6.4, once that's officially released), that's fine, I can take on the work of fixing the obscure issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants