diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 7482cd07b6..96060b235f 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -30,73 +30,71 @@ jobs:
         # and then use `include` to define their settings.
 
         name: [
-          linux-gcc9,
-          linux-debug-gcc9,
           linux-gcc11,
+          linux-debug-gcc11,
           windows,
-          windows-debug
+          windows-debug,
+          macos-arm64
         ]
 
         include:
 
-          - name: linux-gcc9
+          - name: linux-gcc11
             os: ubuntu-20.04
             buildType: RELEASE
-            containerImage: ghcr.io/gafferhq/build/build:2.1.2
+            containerImage: ghcr.io/gafferhq/build/build:3.0.0
             options: .github/workflows/main/options.posix
-            dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/8.0.1/gafferDependencies-8.0.1-linux-gcc9.tar.gz
+            dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/9.1.0/gafferDependencies-9.1.0-linux-gcc11.tar.gz
             tests: testCore testCorePython testScene testImage testAlembic testUSD testVDB
             publish: true
+            jobs: 4
 
-          - name: linux-debug-gcc9
+          - name: linux-debug-gcc11
             os: ubuntu-20.04
             buildType: DEBUG
-            containerImage: ghcr.io/gafferhq/build/build:2.1.2
-            options: .github/workflows/main/options.posix
-            dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/8.0.1/gafferDependencies-8.0.1-linux-gcc9.tar.gz
-            tests: testCore testCorePython testScene testImage testAlembic testUSD testVDB
-            publish: false
-
-          - name: linux-gcc11
-            os: ubuntu-20.04
-            buildType: RELEASE
             containerImage: ghcr.io/gafferhq/build/build:3.0.0
             options: .github/workflows/main/options.posix
-            dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/8.0.1/gafferDependencies-8.0.1-linux-gcc11.tar.gz
+            dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/9.1.0/gafferDependencies-9.1.0-linux-gcc11.tar.gz
             tests: testCore testCorePython testScene testImage testAlembic testUSD testVDB
-            publish: true
+            publish: false
+            jobs: 4
 
           - name: windows
-            os: windows-2019
+            os: windows-2022
             buildType: RELEASE
             options: .github/workflows/main/options.windows
-            dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/8.0.1/gafferDependencies-8.0.1-windows.zip
+            dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/9.1.0/gafferDependencies-9.1.0-windows.zip
             tests: testCore testCorePython testScene testImage testAlembic testUSD testVDB
             publish: true
+            jobs: 4
 
           - name: windows-debug
-            os: windows-2019
+            os: windows-2022
             buildType: RELWITHDEBINFO
             options: .github/workflows/main/options.windows
-            dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/8.0.1/gafferDependencies-8.0.1-windows.zip
+            dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/9.1.0/gafferDependencies-9.1.0-windows.zip
             tests: testCore testCorePython testScene testImage testAlembic testUSD testVDB
             publish: false
+            jobs: 4
+
+          - name: macos-arm64
+            os: macos-14
+            buildType: RELEASE
+            options: .github/workflows/main/options.posix
+            dependenciesURL: https://github.com/GafferHQ/dependencies/releases/download/9.1.0/gafferDependencies-9.1.0-macos-arm64.tar.gz
+            tests: testCore testCorePython testScene testImage testAlembic testUSD testVDB
+            publish: true
+            jobs: 3
 
     runs-on: ${{ matrix.os }}
 
     container: ${{ matrix.containerImage }}
 
-    env:
-      # GitHub have moved to running actions on Node20, which prevents them from
-      # running on CentOS 7. The below allows actions to continue running on Node16
-      # until October.
-      ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
-
     steps:
 
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
-    - uses: ilammy/msvc-dev-cmd@v1.12.1
+    - uses: ilammy/msvc-dev-cmd@v1.13.0
       with:
         sdk: 10.0.17763.0
 
@@ -108,16 +106,8 @@ jobs:
       if: runner.os == 'Windows'
 
     - name: Install toolchain (MacOS)
-      # Prefer `pip install` because it is faster
-      # than `brew install`.
       run: |
-        sudo pip3 install scons==4.0.1
-        # Brew installs all manner of headers into `/usr/local/include`, including
-        # OpenEXR and Imath versions that conflict with our own. We can't stop Clang
-        # finding them because Clang is hardcoded to look in `/usr/local/include`
-        # _before_ anything we specify with `-isystem`, despite documentation to the
-        # contrary. So we nuke the headers.
-        rm -rf /usr/local/include/*
+        pipx install scons==4.6.0
         echo PACKAGE_COMMAND=tar -czf >> $GITHUB_ENV
         echo PACKAGE_EXTENSION=tar.gz >> $GITHUB_ENV
       if: runner.os == 'macOS'
@@ -161,7 +151,7 @@ jobs:
       shell: bash
 
     - name: Cache
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
         path: sconsCache
         key: ${{ runner.os }}-${{ matrix.containerImage }}-${{env.CORTEX_DEPENDENCIES_HASH}}-${{ matrix.buildType }}-${{ github.sha }}
@@ -170,7 +160,7 @@ jobs:
 
     - name: Build
       run: |
-       scons -j 2 BUILD_TYPE=${{ matrix.buildType }} OPTIONS=${{ matrix.options }} BUILD_CACHEDIR=sconsCache
+       scons -j ${{ matrix.jobs }} BUILD_TYPE=${{ matrix.buildType }} OPTIONS=${{ matrix.options }} BUILD_CACHEDIR=sconsCache
        # Copy the config log for use in the "Debug Failures" step, because it
        # gets clobbered by the `scons test*` call below.
        cp config.log buildConfig.log
@@ -188,10 +178,13 @@ jobs:
        ${{ env.PACKAGE_COMMAND }} ${{ env.CORTEX_BUILD_NAME }}.${{env.PACKAGE_EXTENSION}} ${{ env.CORTEX_BUILD_NAME }}
       if: matrix.publish
 
-    - uses: actions/upload-artifact@v3
+    - uses: actions/upload-artifact@v4
       with:
         name: ${{ env.CORTEX_BUILD_NAME }}
         path: ${{ env.CORTEX_BUILD_NAME }}.${{ env.PACKAGE_EXTENSION }}
+        # Using compression-level 0 avoids compressing our already compressed
+        # package and results in a significantly faster upload.
+        compression-level: 0
       if: matrix.publish
 
     - name: Publish Release
diff --git a/.github/workflows/main/options.windows b/.github/workflows/main/options.windows
index 5a4789bcfe..5a7c3b3010 100644
--- a/.github/workflows/main/options.windows
+++ b/.github/workflows/main/options.windows
@@ -2,7 +2,7 @@ import os
 
 deps = os.environ.get( "CORTEX_BUILD_NAME" )
 includes = os.path.join( deps, "include" )
-libs = os.path.join( deps, "lib" )
+libs = os.path.join( deps, "lib" ) + os.pathsep + os.path.join( deps, "bin" )
 
 build = os.environ.get( "CORTEX_BUILD_NAME" )
 
diff --git a/Changes b/Changes
index 6a3527fc18..1afa9a3aac 100644
--- a/Changes
+++ b/Changes
@@ -1,8 +1,115 @@
-10.5.x.x (relative to 10.5.9.2)
+10.5.x.x (relative to 10.5.13.0)
 ========
 
 
 
+10.5.13.0 (relative to 10.5.12.0)
+=========
+
+Features
+--------
+
+- PointInstancerAlgo : Added support for the env var IECOREUSD_POINTINSTANCER_RELATIVEPROTOTYPES. If this is set to "1", then when USD PointInstancers are loaded as point clouds, if they contain prototype paths beneath themselves in the hierarchy, those prototype paths will be loaded as relative paths, starting with "./". This aligns with how Gaffer will now handle prototype paths, and allows point instancers to be relocated in the hierarchy.
+
+Fixes
+-----
+
+- USDScene :
+  - Fixed timecodes used when writing animated attributes.
+  - Fixed timecodes used when writing animated bounds.
+
+10.5.12.0 (relative to 10.5.11.0)
+========
+
+Features
+--------
+
+- IECoreImage::DisplayDriverServer: Adds option to display server to client driver to write to the same display driver.
+
+Fixes
+-----
+
+- IECore : Fixed bug that was causing imath vectors and colors with values of `inf` / `std::numeric_limits<float>::infinity()` to be serialised in a way that could not be evaluated with `eval()`.
+
+Build
+-----
+
+- CI :
+  - IECoreHoudini.LiveSceneTest : Reverted recent update since #1441 made it unnecessary.
+
+10.5.11.0 (relative to 10.5.10.0)
+=========
+
+Improvements
+------------
+
+- OpenImageIOAlgo::DataView : Added support for Int64Data, UInt64Data, Int64VectorData and UInt64VectorData.
+- IECoreHoudini : Updated to support Houdini 20.0 and 20.5.
+- IECoreMaya : Avoid compilation warnings with new gcc.
+
+Fixes
+-----
+
+- MeshAlgo::MeshSplitter/segment : Fixed so that we now preserve vertex order while splitting. This matches the old behvaviour before 1.4.6.0 when segment was optimized. This doesn't affect the correctness of the result, but is a better convention to match user expectations - when combining meshes followed by splitting, it's better if you get back the same vertex ids you started with.
+
+Build
+-----
+
+- SConstruct :
+  - Added `PYBIND11_INCLUDE_PATH` option.
+  - Added `VDB_PYTHON_PATH` to USD tests.
+  - Added `INSTALL_CREATE_SYMLINKS`, which allows you to disable the creation of version symlinks at the end of the install.
+  - Added `USG_SHIMLIB_PATH` which may be necessary to run nuke tests.
+
+- CI :
+  - IECoreHoudini tests updated to pass on newer environments.
+  - IECoreGL tests updated with relaxed precisions for image comparisons.
+  - Updated to GafferHQ/dependencies 9.0.0.
+  - Updated Windows build to use MSVC 2022.
+
+10.5.10.0 (relative to 10.5.9.5)
+=========
+
+Improvements
+------------
+
+- USDScene : PointInstancers are now loaded with `invisibleIds` and `inactiveIds` as primitive variables.
+- IECoreUSD::DataAlgo :
+  - Made `valueTypeName` argument to `fromUSD( const VtValue & )` optional. This allows VtValue to be converted without having additional type information available.
+  - Added conversions between `VtDictionary` and `CompoundData`.
+- IECoreUSD::ShaderAlgo :
+  - Stopped writing `cortex_autoAdaptor` metadata, which would cause errors in DCCs without the definition registered.
+  - Added round-tripping of all blind data stored on Shaders.
+- IECoreScene::ShaderNetworkAlgo : Added a mechanism for customising the adapter shaders used by `addComponentConnectionAdapters()`.
+
+10.5.9.5 (relative to 10.5.9.4)
+========
+
+Fixes
+-----
+
+- USDScene : Fixed crash loading skinned facevarying normals (bug introduced in 10.5.9.3).
+
+10.5.9.4 (relative to 10.5.9.3)
+========
+
+Fixes
+-----
+
+- USDScene : Fixed crash loading a Volume with an empty field.
+
+10.5.9.3 (relative to 10.5.9.2)
+========
+
+Fixes
+-----
+
+- USDScene :
+  - Fixed crash attempting to write to a file that is already open for reading. An exception is now thrown instead.
+  - Fixed loading of skinned facevarying normals.
+  - `lightLink` and `shadowLink` collections on UsdLuxLightAPI are no longer treated as sets.
+- Version.h : Fixed `*Version()` functions to return the runtime version of the library, not the version that client code was compiled with. Use the `CORTEX_*_VERSION` macros for compile time checks.
+- IECoreUSD : Asset and volume paths now use `/` in the resolved path on all operating systems. This behavior can be disabled by setting the `IECOREUSD_FORCE_ASSET_PATH_FORWARD_SLASH` environment variable to a value of `0`.
 
 10.5.9.2 (relative to 10.5.9.1)
 ========
diff --git a/SConstruct b/SConstruct
index fbc0461ef0..93047695a1 100644
--- a/SConstruct
+++ b/SConstruct
@@ -56,8 +56,8 @@ SConsignFile()
 
 ieCoreMilestoneVersion = 10 # for announcing major milestones - may contain all of the below
 ieCoreMajorVersion = 5 # backwards-incompatible changes
-ieCoreMinorVersion = 9 # new backwards-compatible features
-ieCorePatchVersion = 2 # bug fixes
+ieCoreMinorVersion = 13 # new backwards-compatible features
+ieCorePatchVersion = 0 # bug fixes
 ieCoreVersionSuffix = "" # used for alpha/beta releases. Example: "a1", "b2", etc.
 
 ###########################################################################################
@@ -353,6 +353,13 @@ o.Add(
 	"",
 )
 
+o.Add(
+	"USG_SHIMLIB_PATH",
+	"The path to the FnUsdShim to use for Nuke. This may be necessary to run the tests.",
+	"",
+)
+
+
 # OpenGL options
 
 try :
@@ -557,6 +564,12 @@ o.Add(
 	"",
 )
 
+o.Add(
+	"PYBIND11_INCLUDE_PATH",
+	"The path to the pybind11 include directory.",
+	"",
+)
+
 # Build options
 
 o.Add(
@@ -845,6 +858,11 @@ o.Add(
 	""
 )
 
+o.Add(
+	BoolVariable( "INSTALL_CREATE_SYMLINKS", "Whether to create symlinks post install", True )
+)
+
+
 # Test options
 
 o.Add(
@@ -967,7 +985,7 @@ o.Add(
 ###########################################################################################
 
 env = Environment(
-	MSVC_VERSION = "14.2",
+	MSVC_VERSION = "14.3",
 	options = o
 )
 
@@ -1071,6 +1089,14 @@ if env["PLATFORM"] != "win32" :
 		# deprecation of gluBuild2DMipmaps() in OSX 10.9.
 		if osxVersion[0] == 10 and osxVersion[1] > 7 :
 			env.Append( CXXFLAGS = [ "-Wno-unused-local-typedef", "-Wno-deprecated-declarations" ] )
+		clangVersion = subprocess.check_output( [ env["CXX"], "-dumpversion" ], env=env["ENV"], universal_newlines=True ).strip()
+		clangVersion = [ int( v ) for v in clangVersion.split( "." ) ]
+		# Work around Boost issues with Xcode 15 where `std::unary_function` has been removed.
+		if clangVersion >= [ 15, 0, 0 ] :
+			env.Append( CXXFLAGS = [ "-DBOOST_NO_CXX98_FUNCTION_BASE", "-D_HAS_AUTO_PTR_ETC=0" ] )
+		# Disable FMA on arm64 builds to limit floating point discrepancies with x86_64 builds.
+		if platform.machine() == "arm64" :
+			env.Append( CXXFLAGS = [ "-ffp-contract=off" ] )
 
 	elif env["PLATFORM"]=="posix" :
 		if "g++" in os.path.basename( env["CXX"] ) and not "clang++" in os.path.basename( env["CXX"] ) :
@@ -1286,11 +1312,11 @@ if doConfigure :
 		Exit( 1 )
 
 	for line in open( str( boostVersionHeader ) ) :
-		m = re.compile( "^#define BOOST_LIB_VERSION \"(.*)\"\s*$" ).match( line )
+		m = re.compile( r"^#define BOOST_LIB_VERSION \"(.*)\"\s*$" ).match( line )
 		if m  :
 			boostVersion = m.group( 1 )
 		if boostVersion :
-			m = re.compile( "^([0-9]+)_([0-9]+)(?:_([0-9]+)|)$" ).match( boostVersion )
+			m = re.compile( r"^([0-9]+)_([0-9]+)(?:_([0-9]+)|)$" ).match( boostVersion )
 			boostMajorVersion, boostMinorVersion, boostPatchVersion = m.group( 1, 2, 3 )
 			env["BOOST_MAJOR_VERSION"] = boostMajorVersion
 			env["BOOST_MINOR_VERSION"] = boostMinorVersion
@@ -1476,9 +1502,6 @@ pythonModuleEnv = pythonEnv.Clone()
 pythonModuleEnv["SHLIBPREFIX"] = ""
 pythonModuleEnv["SHLIBSUFFIX"] = ".so" if env["PLATFORM"] != "win32" else ".pyd"
 
-if pythonModuleEnv["PLATFORM"]=="darwin" :
-	pythonModuleEnv.Append( SHLINKFLAGS = "-single_module" )
-
 ###########################################################################################
 # An environment for running tests
 ###########################################################################################
@@ -1724,7 +1747,8 @@ corePythonScripts = glob.glob( "python/IECore/*.py" )
 coreLibrary = coreEnv.SharedLibrary( "lib/" + os.path.basename( coreEnv.subst( "$INSTALL_LIB_NAME" ) ), coreSources )
 coreLibraryInstall = coreEnv.Install( os.path.dirname( coreEnv.subst( "$INSTALL_LIB_NAME" ) ), coreLibrary )
 coreEnv.NoCache( coreLibraryInstall )
-coreEnv.AddPostAction( coreLibraryInstall, lambda target, source, env : makeLibSymLinks( coreEnv ) )
+if env["INSTALL_CREATE_SYMLINKS"] :
+	coreEnv.AddPostAction( coreLibraryInstall, lambda target, source, env : makeLibSymLinks( coreEnv ) )
 coreEnv.Alias( "install", [ coreLibraryInstall ] )
 coreEnv.Alias( "installCore", [ coreLibraryInstall ] )
 coreEnv.Alias( "installLib", [ coreLibraryInstall ] )
@@ -1747,7 +1771,8 @@ versionHeaderInstall = env.Substfile(
 )
 # handle the remaining core headers
 headerInstall = coreEnv.Install( "$INSTALL_HEADER_DIR/IECore", coreHeaders )
-coreEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECore", lambda target, source, env : makeSymLinks( coreEnv, coreEnv["INSTALL_HEADER_DIR"] ) )
+if env["INSTALL_CREATE_SYMLINKS"]:
+	coreEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECore", lambda target, source, env : makeSymLinks( coreEnv, coreEnv["INSTALL_HEADER_DIR"] ) )
 if env["INSTALL_PKG_CONFIG_FILE"]:
 		coreEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECore", lambda target, source, env : writePkgConfig( coreEnv, corePythonEnv ) )
 coreEnv.Alias( "install", [ headerInstall, versionHeaderInstall ] )
@@ -1758,7 +1783,8 @@ corePythonEnv.Append( LIBS = os.path.basename( coreEnv.subst( "$INSTALL_LIB_NAME
 corePythonLibrary = corePythonEnv.SharedLibrary( "lib/" + os.path.basename( corePythonEnv.subst( "$INSTALL_PYTHONLIB_NAME" ) ), corePythonSources )
 corePythonLibraryInstall = corePythonEnv.Install( os.path.dirname( corePythonEnv.subst( "$INSTALL_PYTHONLIB_NAME" ) ), corePythonLibrary )
 corePythonEnv.NoCache( corePythonLibraryInstall )
-corePythonEnv.AddPostAction( corePythonLibraryInstall, lambda target, source, env : makeLibSymLinks( corePythonEnv, libNameVar="INSTALL_PYTHONLIB_NAME" ) )
+if env["INSTALL_CREATE_SYMLINKS"] :
+	corePythonEnv.AddPostAction( corePythonLibraryInstall, lambda target, source, env : makeLibSymLinks( corePythonEnv, libNameVar="INSTALL_PYTHONLIB_NAME" ) )
 corePythonEnv.Alias( "install", [ corePythonLibraryInstall ] )
 corePythonEnv.Alias( "installCore", [ corePythonLibraryInstall ] )
 corePythonEnv.Alias( "installLib", [ corePythonLibraryInstall ] )
@@ -1776,7 +1802,8 @@ corePythonModuleEnv.Depends( corePythonModule, coreLibrary )
 corePythonModuleEnv.Depends( corePythonModule, corePythonLibrary )
 
 corePythonModuleInstall = corePythonModuleEnv.Install( "$INSTALL_PYTHON_DIR/IECore", corePythonScripts + corePythonModule )
-corePythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECore", lambda target, source, env : makeSymLinks( corePythonEnv, corePythonEnv["INSTALL_PYTHON_DIR"] ) )
+if env["INSTALL_CREATE_SYMLINKS"]:
+	corePythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECore", lambda target, source, env : makeSymLinks( corePythonEnv, corePythonEnv["INSTALL_PYTHON_DIR"] ) )
 corePythonModuleEnv.Alias( "install", corePythonModuleInstall )
 corePythonModuleEnv.Alias( "installCore", corePythonModuleInstall )
 
@@ -1785,7 +1812,8 @@ for cls in env['INSTALL_IECORE_OPS'] :
 	stubName = os.path.basename( cls[1] )
 	stubEnv = corePythonModuleEnv.Clone( IECORE_NAME=os.path.join( cls[1], stubName ) )
 	stubInstall = stubEnv.Command( "$INSTALL_IECORE_OP_PATH", None, 'echo "from %s import %s as %s" > $TARGET' % ( cls[0].rpartition( "." )[0], cls[0].rpartition( "." )[-1], stubName ) )
-	stubEnv.AddPostAction( stubInstall, lambda target, source, env : makeSymLinks( env, env["INSTALL_IECORE_OP_PATH"] ) )
+	if env[ "INSTALL_CREATE_SYMLINKS" ] :
+		stubEnv.AddPostAction( stubInstall, lambda target, source, env : makeSymLinks( env, env["INSTALL_IECORE_OP_PATH"] ) )
 	stubEnv.Alias( "install", stubInstall )
 	stubEnv.Alias( "installCore", stubInstall )
 	stubEnv.Alias( "installStubs", stubInstall )
@@ -1894,14 +1922,16 @@ if doConfigure :
 		imageLibrary = imageEnv.SharedLibrary( "lib/" + os.path.basename( imageEnv.subst( "$INSTALL_LIB_NAME" ) ), imageSources )
 		imageLibraryInstall = imageEnv.Install( os.path.dirname( imageEnv.subst( "$INSTALL_LIB_NAME" ) ), imageLibrary )
 		imageEnv.NoCache( imageLibraryInstall )
-		imageEnv.AddPostAction( imageLibraryInstall, lambda target, source, env : makeLibSymLinks( imageEnv ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			imageEnv.AddPostAction( imageLibraryInstall, lambda target, source, env : makeLibSymLinks( imageEnv ) )
 		imageEnv.Alias( "install", imageLibraryInstall )
 		imageEnv.Alias( "installImage", imageLibraryInstall )
 		imageEnv.Alias( "installLib", [ imageLibraryInstall ] )
 
 		# headers
 		imageHeaderInstall = imageEnv.Install( "$INSTALL_HEADER_DIR/IECoreImage", imageHeaders )
-		imageEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreImage", lambda target, source, env : makeSymLinks( imageEnv, imageEnv["INSTALL_HEADER_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			imageEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreImage", lambda target, source, env : makeSymLinks( imageEnv, imageEnv["INSTALL_HEADER_DIR"] ) )
 		imageEnv.Alias( "install", imageHeaderInstall )
 		imageEnv.Alias( "installImage", imageHeaderInstall )
 
@@ -1915,9 +1945,7 @@ if doConfigure :
 		imagePythonModuleEnv.Append( **imageEnvPrepends )
 		imagePythonModuleEnv.Append(
 			LIBS = [
-				os.path.basename( coreEnv.subst( "$INSTALL_LIB_NAME" ) ),
 				os.path.basename( imageEnv.subst( "$INSTALL_LIB_NAME" ) ),
-				os.path.basename( corePythonEnv.subst( "$INSTALL_PYTHONLIB_NAME" ) ),
 			]
 		)
 		imagePythonModule = imagePythonModuleEnv.SharedLibrary( "python/IECoreImage/_IECoreImage", imagePythonSources + imagePythonModuleSources )
@@ -1925,7 +1953,8 @@ if doConfigure :
 
 		# python module install
 		imagePythonModuleInstall = imagePythonModuleEnv.Install( "$INSTALL_PYTHON_DIR/IECoreImage", imagePythonScripts + imagePythonModule )
-		imagePythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreImage", lambda target, source, env : makeSymLinks( imagePythonModuleEnv, imagePythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			imagePythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreImage", lambda target, source, env : makeSymLinks( imagePythonModuleEnv, imagePythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
 		imagePythonModuleEnv.Alias( "install", imagePythonModuleInstall )
 		imagePythonModuleEnv.Alias( "installImage", imagePythonModuleInstall )
 
@@ -1981,14 +2010,16 @@ if doConfigure :
 	sceneLibrary = sceneEnv.SharedLibrary( "lib/" + os.path.basename( sceneEnv.subst( "$INSTALL_LIB_NAME" ) ), sceneSources )
 	sceneLibraryInstall = sceneEnv.Install( os.path.dirname( sceneEnv.subst( "$INSTALL_LIB_NAME" ) ), sceneLibrary )
 	sceneEnv.NoCache( sceneLibraryInstall )
-	sceneEnv.AddPostAction( sceneLibraryInstall, lambda target, source, env : makeLibSymLinks( sceneEnv ) )
+	if env[ "INSTALL_CREATE_SYMLINKS" ] :
+		sceneEnv.AddPostAction( sceneLibraryInstall, lambda target, source, env : makeLibSymLinks( sceneEnv ) )
 	sceneEnv.Alias( "install", [ sceneLibraryInstall ] )
 	sceneEnv.Alias( "installScene", [ sceneLibraryInstall ] )
 	sceneEnv.Alias( "installSceneLib", [ sceneLibraryInstall ] )
 
 	# headers
 	sceneHeaderInstall = sceneEnv.Install( "$INSTALL_HEADER_DIR/IECoreScene", sceneHeaders )
-	sceneEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreScene", lambda target, source, env : makeSymLinks( sceneEnv, sceneEnv["INSTALL_HEADER_DIR"] ) )
+	if env[ "INSTALL_CREATE_SYMLINKS" ] :
+		sceneEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreScene", lambda target, source, env : makeSymLinks( sceneEnv, sceneEnv["INSTALL_HEADER_DIR"] ) )
 	sceneEnv.Alias( "install", sceneHeaderInstall )
 	sceneEnv.Alias( "installScene", sceneHeaderInstall )
 
@@ -2000,7 +2031,8 @@ if doConfigure :
 	scenePythonModuleEnv.Depends( scenePythonModule, sceneLibrary )
 
 	scenePythonModuleInstall = scenePythonModuleEnv.Install( "$INSTALL_PYTHON_DIR/IECoreScene", scenePythonScripts + scenePythonModule )
-	scenePythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreScene", lambda target, source, env : makeSymLinks( scenePythonModuleEnv, scenePythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
+	if env[ "INSTALL_CREATE_SYMLINKS" ] :
+		scenePythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreScene", lambda target, source, env : makeSymLinks( scenePythonModuleEnv, scenePythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
 	scenePythonModuleEnv.Alias( "install", scenePythonModuleInstall )
 	scenePythonModuleEnv.Alias( "installScene", scenePythonModuleInstall )
 
@@ -2036,7 +2068,8 @@ vdbEnvPrepends = {
 	],
 	"LIBS" : ["openvdb$VDB_LIB_SUFFIX"],
 	"CXXFLAGS" : [
-		systemIncludeArgument, "$VDB_INCLUDE_PATH"
+		systemIncludeArgument, "$VDB_INCLUDE_PATH",
+		systemIncludeArgument, "$PYBIND11_INCLUDE_PATH",
 	]
 }
 
@@ -2076,14 +2109,16 @@ if doConfigure :
 		vdbLibrary = vdbEnv.SharedLibrary( "lib/" + os.path.basename( vdbEnv.subst( "$INSTALL_LIB_NAME" ) ), vdbSources )
 		vdbLibraryInstall = vdbEnv.Install( os.path.dirname( vdbEnv.subst( "$INSTALL_LIB_NAME" ) ), vdbLibrary )
 		vdbEnv.NoCache( vdbLibraryInstall )
-		vdbEnv.AddPostAction( vdbLibraryInstall, lambda target, source, env : makeLibSymLinks( vdbEnv ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			vdbEnv.AddPostAction( vdbLibraryInstall, lambda target, source, env : makeLibSymLinks( vdbEnv ) )
 		vdbEnv.Alias( "install", [ vdbLibraryInstall ] )
 		vdbEnv.Alias( "installVDB", [ vdbLibraryInstall ] )
 		vdbEnv.Alias( "installVDBLib", [ vdbLibraryInstall ] )
 
 		# headers
 		vdbHeaderInstall = sceneEnv.Install( "$INSTALL_HEADER_DIR/IECoreVDB", vdbHeaders )
-		sceneEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreVDB", lambda target, source, env : makeSymLinks( vdbEnv, vdbEnv["INSTALL_HEADER_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			sceneEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreVDB", lambda target, source, env : makeSymLinks( vdbEnv, vdbEnv["INSTALL_HEADER_DIR"] ) )
 		sceneEnv.Alias( "install", vdbHeaderInstall )
 		sceneEnv.Alias( "installVDB", vdbHeaderInstall )
 
@@ -2098,7 +2133,8 @@ if doConfigure :
 		vdbPythonModuleEnv.Depends( vdbPythonModule, vdbLibrary )
 
 		vdbPythonModuleInstall = vdbPythonModuleEnv.Install( "$INSTALL_PYTHON_DIR/IECoreVDB", vdbPythonScripts + vdbPythonModule )
-		vdbPythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreVDB", lambda target, source, env : makeSymLinks( vdbPythonModuleEnv, vdbPythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			vdbPythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreVDB", lambda target, source, env : makeSymLinks( vdbPythonModuleEnv, vdbPythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
 		vdbPythonModuleEnv.Alias( "install", vdbPythonModuleInstall )
 		vdbPythonModuleEnv.Alias( "installVDB", vdbPythonModuleInstall )
 
@@ -2213,26 +2249,30 @@ if env["WITH_GL"] and doConfigure :
 		glLibrary = glEnv.SharedLibrary( "lib/" + os.path.basename( glEnv.subst( "$INSTALL_LIB_NAME" ) ), glSources )
 		glLibraryInstall = glEnv.Install( os.path.dirname( glEnv.subst( "$INSTALL_LIB_NAME" ) ), glLibrary )
 		glEnv.NoCache( glLibraryInstall )
-		glEnv.AddPostAction( glLibraryInstall, lambda target, source, env : makeLibSymLinks( glEnv ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			glEnv.AddPostAction( glLibraryInstall, lambda target, source, env : makeLibSymLinks( glEnv ) )
 		glEnv.Alias( "install", glLibraryInstall )
 		glEnv.Alias( "installGL", glLibraryInstall )
 		glEnv.Alias( "installLib", [ glLibraryInstall ] )
 
 		glHeaders = glob.glob( "include/IECoreGL/*.h" ) + glob.glob( "include/IECoreGL/*.inl" )
 		glHeaderInstall = glEnv.Install( "$INSTALL_HEADER_DIR/IECoreGL", glHeaders )
-		glEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreGL", lambda target, source, env : makeSymLinks( glEnv, glEnv["INSTALL_HEADER_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			glEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreGL", lambda target, source, env : makeSymLinks( glEnv, glEnv["INSTALL_HEADER_DIR"] ) )
 		glEnv.Alias( "install", glHeaderInstall )
 		glEnv.Alias( "installGL", glHeaderInstall )
 
 		glslHeaders = glob.glob( "glsl/IECoreGL/*.h" )
 		glslHeaderInstall = glEnv.Install( "$INSTALL_GLSL_HEADER_DIR/IECoreGL", glslHeaders )
-		glEnv.AddPostAction( "$INSTALL_GLSL_HEADER_DIR/IECoreGL", lambda target, source, env : makeSymLinks( glEnv, glEnv["INSTALL_GLSL_HEADER_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			glEnv.AddPostAction( "$INSTALL_GLSL_HEADER_DIR/IECoreGL", lambda target, source, env : makeSymLinks( glEnv, glEnv["INSTALL_GLSL_HEADER_DIR"] ) )
 		glEnv.Alias( "install", glslHeaderInstall )
 		glEnv.Alias( "installGL", glslHeaderInstall )
 
 		glslShaderFiles = glob.glob( "glsl/*.frag" ) + glob.glob( "glsl/*.vert" )
 		glslShaderInstall = glEnv.Install( "$INSTALL_GLSL_SHADER_DIR", glslShaderFiles )
-		glEnv.AddPostAction( "$INSTALL_GLSL_SHADER_DIR", lambda target, source, env : makeSymLinks( glEnv, glEnv["INSTALL_GLSL_SHADER_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			glEnv.AddPostAction( "$INSTALL_GLSL_SHADER_DIR", lambda target, source, env : makeSymLinks( glEnv, glEnv["INSTALL_GLSL_SHADER_DIR"] ) )
 		glEnv.Alias( "install", glslShaderInstall )
 		glEnv.Alias( "installGL", glslShaderInstall )
 
@@ -2240,9 +2280,7 @@ if env["WITH_GL"] and doConfigure :
 		glPythonModuleEnv.Append( **glEnvAppends )
 		glPythonModuleEnv.Append(
 			LIBS = [
-				os.path.basename( coreEnv.subst( "$INSTALL_LIB_NAME" ) ),
 				os.path.basename( glEnv.subst( "$INSTALL_LIB_NAME" ) ),
-				os.path.basename( corePythonEnv.subst( "$INSTALL_PYTHONLIB_NAME" ) ),
 				os.path.basename( imageEnv.subst( "$INSTALL_LIB_NAME" ) ),
 				os.path.basename( sceneEnv.subst( "$INSTALL_LIB_NAME" ) ),
 			]
@@ -2252,7 +2290,8 @@ if env["WITH_GL"] and doConfigure :
 
 		glPythonScripts = glob.glob( "python/IECoreGL/*.py" )
 		glPythonModuleInstall = glPythonModuleEnv.Install( "$INSTALL_PYTHON_DIR/IECoreGL", glPythonScripts + glPythonModule )
-		glPythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreGL", lambda target, source, env : makeSymLinks( glPythonModuleEnv, glPythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			glPythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreGL", lambda target, source, env : makeSymLinks( glPythonModuleEnv, glPythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
 		glPythonModuleEnv.Alias( "install", glPythonModuleInstall )
 		glPythonModuleEnv.Alias( "installGL", glPythonModuleInstall )
 
@@ -2369,7 +2408,8 @@ if doConfigure :
 		mayaLibrary = mayaEnv.SharedLibrary( "lib/" + os.path.basename( mayaEnv.subst( "$INSTALL_MAYALIB_NAME" ) ), mayaSources )
 		mayaLibraryInstall = mayaEnv.Install( os.path.dirname( mayaEnv.subst( "$INSTALL_MAYALIB_NAME" ) ), mayaLibrary )
 		mayaEnv.NoCache( mayaLibraryInstall )
-		mayaEnv.AddPostAction( mayaLibraryInstall, lambda target, source, env : makeLibSymLinks( mayaEnv, "INSTALL_MAYALIB_NAME" ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			mayaEnv.AddPostAction( mayaLibraryInstall, lambda target, source, env : makeLibSymLinks( mayaEnv, "INSTALL_MAYALIB_NAME" ) )
 		mayaEnv.Alias( "install", mayaLibraryInstall )
 		mayaEnv.Alias( "installMaya", mayaLibraryInstall )
 		mayaEnv.Alias( "installLib", [ mayaLibraryInstall ] )
@@ -2377,13 +2417,15 @@ if doConfigure :
 		# maya headers
 		mayaHeaderInstall = mayaEnv.Install( "$INSTALL_HEADER_DIR/IECoreMaya", mayaHeaders )
 		mayaHeaderInstall += mayaEnv.Install( "$INSTALL_HEADER_DIR/IECoreMaya/bindings", mayaBindingHeaders )
-		mayaEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreMaya", lambda target, source, env : makeSymLinks( mayaEnv, mayaEnv["INSTALL_HEADER_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			mayaEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreMaya", lambda target, source, env : makeSymLinks( mayaEnv, mayaEnv["INSTALL_HEADER_DIR"] ) )
 		mayaEnv.Alias( "install", mayaHeaderInstall )
 		mayaEnv.Alias( "installMaya", mayaHeaderInstall )
 
 		# maya mel
 		mayaMelInstall = mayaEnv.Install( "$INSTALL_MEL_DIR", mayaMel )
-		mayaEnv.AddPostAction( "$INSTALL_MEL_DIR", lambda target, source, env : makeSymLinks( mayaEnv, mayaEnv["INSTALL_MEL_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			mayaEnv.AddPostAction( "$INSTALL_MEL_DIR", lambda target, source, env : makeSymLinks( mayaEnv, mayaEnv["INSTALL_MEL_DIR"] ) )
 		mayaEnv.Alias( "install", mayaMelInstall )
 		mayaEnv.Alias( "installMaya", mayaMelInstall )
 
@@ -2419,7 +2461,8 @@ if doConfigure :
 
 			mayaPluginLoader = mayaPluginLoaderEnv.SharedLibrary( mayaPluginTarget, mayaPluginLoaderSources, SHLIBPREFIX="" )
 			mayaPluginLoaderInstall = mayaPluginLoaderEnv.InstallAs( mayaPluginLoaderEnv.subst( "$INSTALL_MAYAPLUGIN_NAME$SHLIBSUFFIX" ), mayaPluginLoader )
-			mayaPluginLoaderEnv.AddPostAction( mayaPluginLoaderInstall, lambda target, source, env : makeSymLinks( mayaPluginLoaderEnv, mayaPluginLoaderEnv["INSTALL_MAYAPLUGIN_NAME"] ) )
+			if env[ "INSTALL_CREATE_SYMLINKS" ] :
+				mayaPluginLoaderEnv.AddPostAction( mayaPluginLoaderInstall, lambda target, source, env : makeSymLinks( mayaPluginLoaderEnv, mayaPluginLoaderEnv["INSTALL_MAYAPLUGIN_NAME"] ) )
 			mayaPluginLoaderEnv.Alias( "install", mayaPluginLoaderInstall )
 			mayaPluginLoaderEnv.Alias( "installMaya", mayaPluginLoaderInstall )
 
@@ -2432,7 +2475,8 @@ if doConfigure :
 		mayaPluginInstall = mayaPluginEnv.Install( os.path.dirname( mayaPluginEnv.subst( "$INSTALL_MAYAPLUGIN_NAME" ) ), mayaPlugin )
 		mayaPluginEnv.Depends( mayaPlugin, corePythonModule )
 
-		mayaPluginEnv.AddPostAction( mayaPluginInstall, lambda target, source, env : makeSymLinks( mayaPluginEnv, mayaPluginEnv["INSTALL_MAYAPLUGIN_NAME"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			mayaPluginEnv.AddPostAction( mayaPluginInstall, lambda target, source, env : makeSymLinks( mayaPluginEnv, mayaPluginEnv["INSTALL_MAYAPLUGIN_NAME"] ) )
 		mayaPluginEnv.Alias( "install", mayaPluginInstall )
 		mayaPluginEnv.Alias( "installMaya", mayaPluginInstall )
 
@@ -2448,7 +2492,8 @@ if doConfigure :
 		mayaPythonModuleEnv.Depends( mayaPythonModule, mayaLibrary )
 
 		mayaPythonModuleInstall = mayaPythonModuleEnv.Install( "$INSTALL_PYTHON_DIR/IECoreMaya", mayaPythonScripts + mayaPythonModule )
-		mayaPythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreMaya", lambda target, source, env : makeSymLinks( mayaPythonModuleEnv, mayaPythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			mayaPythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreMaya", lambda target, source, env : makeSymLinks( mayaPythonModuleEnv, mayaPythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
 		mayaPythonModuleEnv.Alias( "install", mayaPythonModuleInstall )
 		mayaPythonModuleEnv.Alias( "installMaya", mayaPythonModuleInstall )
 
@@ -2552,6 +2597,7 @@ nukeTestEnv["ENV"]["NUKE_PATH"] = "plugins/nuke"
 nukeTestEnv["ENV"]["IECORE_OP_PATHS"] = "test/IECoreNuke/ops:test/IECore/ops"
 # prepend OIIO LIB PATH to library path to support custom OIIO with specific dependencies
 nukeTestEnv["ENV"][libraryPathEnvVar] = "{}:{}".format( nukeTestEnv.subst( "$OIIO_LIB_PATH" ), nukeTestEnv["ENV"][libraryPathEnvVar] )
+nukeTestEnv["ENV"]["USG_SHIMLIB_PATH"] = nukeTestEnv["USG_SHIMLIB_PATH"]
 
 if doConfigure :
 
@@ -2643,7 +2689,8 @@ if doConfigure :
 				# nuke library
 				nukeLibrary = nukeEnv.SharedLibrary( "lib/" + os.path.basename( nukeEnv.subst( "$INSTALL_NUKELIB_NAME" ) ), nukeSources )
 				nukeLibraryInstall = nukeEnv.Install( os.path.dirname( nukeEnv.subst( "$INSTALL_NUKELIB_NAME" ) ), nukeLibrary )
-				nukeEnv.AddPostAction( nukeLibraryInstall, lambda target, source, env : makeLibSymLinks( nukeEnv, "INSTALL_NUKELIB_NAME" ) )
+				if env[ "INSTALL_CREATE_SYMLINKS" ] :
+					nukeEnv.AddPostAction( nukeLibraryInstall, lambda target, source, env : makeLibSymLinks( nukeEnv, "INSTALL_NUKELIB_NAME" ) )
 				nukeEnv.Alias( "install", nukeLibraryInstall )
 				nukeEnv.Alias( "installNuke", nukeLibraryInstall )
 				nukeEnv.Alias( "installLib", [ nukeLibraryInstall ] )
@@ -2651,7 +2698,8 @@ if doConfigure :
 				# nuke headers
 
 				nukeHeaderInstall = nukeEnv.Install( "$INSTALL_HEADER_DIR/IECoreNuke", nukeHeaders )
-				nukeEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreNuke", lambda target, source, env : makeSymLinks( nukeEnv, nukeEnv["INSTALL_HEADER_DIR"] ) )
+				if env[ "INSTALL_CREATE_SYMLINKS" ] :
+					nukeEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreNuke", lambda target, source, env : makeSymLinks( nukeEnv, nukeEnv["INSTALL_HEADER_DIR"] ) )
 				nukeEnv.Alias( "installNuke", nukeHeaderInstall )
 				nukeEnv.Alias( "install", nukeHeaderInstall )
 
@@ -2659,7 +2707,8 @@ if doConfigure :
 
 				nukePythonModule = nukePythonModuleEnv.SharedLibrary( "python/IECoreNuke/_IECoreNuke", nukePythonSources )
 				nukePythonModuleInstall = nukePythonModuleEnv.Install( "$INSTALL_NUKEPYTHON_DIR/IECoreNuke", nukePythonScripts + nukePythonModule )
-				nukePythonModuleEnv.AddPostAction( "$INSTALL_NUKEPYTHON_DIR/IECoreNuke", lambda target, source, env : makeSymLinks( nukePythonModuleEnv, nukePythonModuleEnv["INSTALL_NUKEPYTHON_DIR"] ) )
+				if env[ "INSTALL_CREATE_SYMLINKS" ] :
+					nukePythonModuleEnv.AddPostAction( "$INSTALL_NUKEPYTHON_DIR/IECoreNuke", lambda target, source, env : makeSymLinks( nukePythonModuleEnv, nukePythonModuleEnv["INSTALL_NUKEPYTHON_DIR"] ) )
 				nukePythonModuleEnv.Alias( "install", nukePythonModuleInstall )
 				nukePythonModuleEnv.Alias( "installNuke", nukePythonModuleInstall )
 				nukePythonModuleEnv.Depends( nukePythonModule, corePythonModule )
@@ -2681,7 +2730,8 @@ if doConfigure :
 				nukePlugin = nukePluginEnv.SharedLibrary( nukePluginTarget, nukePluginSources, SHLIBPREFIX="" )
 				nukePluginInstall = nukePluginEnv.Install( os.path.dirname( nukePluginEnv.subst( "$INSTALL_NUKEPLUGIN_NAME" ) ), nukePlugin )
 
-				nukePluginEnv.AddPostAction( nukePluginInstall, lambda target, source, env : makeSymLinks( nukePluginEnv, nukePluginEnv["INSTALL_NUKEPLUGIN_NAME"] ) )
+				if env[ "INSTALL_CREATE_SYMLINKS" ] :
+					nukePluginEnv.AddPostAction( nukePluginInstall, lambda target, source, env : makeSymLinks( nukePluginEnv, nukePluginEnv["INSTALL_NUKEPLUGIN_NAME"] ) )
 				nukePluginEnv.Alias( "install", nukePluginInstall )
 				nukePluginEnv.Alias( "installNuke", nukePluginInstall )
 
@@ -2789,6 +2839,10 @@ houdiniEnv.Append( **houdiniEnvAppends )
 
 houdiniEnv.Append( SHLINKFLAGS = pythonEnv["PYTHON_LINK_FLAGS"].split() )
 houdiniEnv.Prepend( SHLINKFLAGS = "$HOUDINI_LINK_FLAGS" )
+# Prepend OIIO path for houdini to allow for non-namespaced includes to be used
+# Houdini ships with a namespaced OpenImageIO (HOIIO) as part of $HOUDINI_INCLUDE_PATH.
+# If you do want to use it, set OIIO_INCLUDE_PATH to that.
+houdiniEnv.Prepend( CXXFLAGS = [ systemIncludeArgument, "$OIIO_INCLUDE_PATH"] )
 
 houdiniPythonModuleEnv = pythonModuleEnv.Clone( **houdiniEnvSets )
 houdiniPythonModuleEnv.Append( **houdiniEnvAppends )
@@ -2854,7 +2908,8 @@ if doConfigure :
 		houdiniLib = houdiniEnv.SharedLibrary( "lib/" + os.path.basename( houdiniEnv.subst( "$INSTALL_HOUDINILIB_NAME" ) ), houdiniSources )
 		houdiniLibInstall = houdiniEnv.Install( os.path.dirname( houdiniEnv.subst( "$INSTALL_HOUDINILIB_NAME" ) ), houdiniLib )
 		houdiniEnv.NoCache( houdiniLibInstall )
-		houdiniEnv.AddPostAction( houdiniLibInstall, lambda target, source, env : makeLibSymLinks( houdiniEnv, "INSTALL_HOUDINILIB_NAME" ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			houdiniEnv.AddPostAction( houdiniLibInstall, lambda target, source, env : makeLibSymLinks( houdiniEnv, "INSTALL_HOUDINILIB_NAME" ) )
 		houdiniEnv.Alias( "install", houdiniLibInstall )
 		houdiniEnv.Alias( "installHoudini", houdiniLibInstall )
 		houdiniEnv.Alias( "installLib", [ houdiniLibInstall ] )
@@ -2864,7 +2919,8 @@ if doConfigure :
 		#=====
 		houdiniHeaderInstall = houdiniEnv.Install( "$INSTALL_HEADER_DIR/IECoreHoudini", houdiniHeaders )
 		houdiniHeaderInstall += houdiniEnv.Install( "$INSTALL_HEADER_DIR/IECoreHoudini/bindings", houdiniBindingHeaders )
-		houdiniEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreHoudini", lambda target, source, env : makeSymLinks( houdiniEnv, houdiniEnv["INSTALL_HEADER_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			houdiniEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreHoudini", lambda target, source, env : makeSymLinks( houdiniEnv, houdiniEnv["INSTALL_HEADER_DIR"] ) )
 		houdiniEnv.Alias( "install", houdiniHeaderInstall )
 		houdiniEnv.Alias( "installHoudini", houdiniHeaderInstall )
 
@@ -2880,7 +2936,8 @@ if doConfigure :
 		houdiniPlugin = houdiniPluginEnv.SharedLibrary( houdiniPluginTarget, houdiniPluginSources, SHLIBPREFIX="" )
 		houdiniPluginInstall = houdiniPluginEnv.Install( os.path.dirname( houdiniPluginEnv.subst( "$INSTALL_HOUDINIPLUGIN_NAME" ) ), houdiniPlugin )
 		houdiniPluginEnv.Depends( houdiniPlugin, corePythonModule )
-		houdiniPluginEnv.AddPostAction( houdiniPluginInstall, lambda target, source, env : makeSymLinks( houdiniPluginEnv, houdiniPluginEnv["INSTALL_HOUDINIPLUGIN_NAME"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			houdiniPluginEnv.AddPostAction( houdiniPluginInstall, lambda target, source, env : makeSymLinks( houdiniPluginEnv, houdiniPluginEnv["INSTALL_HOUDINIPLUGIN_NAME"] ) )
 		houdiniPluginEnv.Alias( "install", houdiniPluginInstall )
 		houdiniPluginEnv.Alias( "installHoudini", houdiniPluginInstall )
 
@@ -2898,7 +2955,8 @@ if doConfigure :
 		houdiniPythonModule = houdiniPythonModuleEnv.SharedLibrary( "python/IECoreHoudini/_IECoreHoudini", houdiniPythonSources )
 		houdiniPythonModuleEnv.Depends( houdiniPythonModule, houdiniLib )
 		houdiniPythonModuleInstall = houdiniPythonModuleEnv.Install( "$INSTALL_PYTHON_DIR/IECoreHoudini", houdiniPythonScripts + houdiniPythonModule )
-		houdiniPythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreHoudini", lambda target, source, env : makeSymLinks( houdiniPythonModuleEnv, houdiniPythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			houdiniPythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreHoudini", lambda target, source, env : makeSymLinks( houdiniPythonModuleEnv, houdiniPythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
 		houdiniPythonModuleEnv.Alias( "install", houdiniPythonModuleInstall )
 		houdiniPythonModuleEnv.Alias( "installHoudini", houdiniPythonModuleInstall )
 
@@ -3076,14 +3134,16 @@ if doConfigure :
 		usdLibrary = usdEnv.SharedLibrary( "lib/" + os.path.basename( usdEnv.subst( "$INSTALL_USDLIB_NAME" ) ), usdSources )
 		usdLibraryInstall = usdEnv.Install( os.path.dirname( usdEnv.subst( "$INSTALL_USDLIB_NAME" ) ), usdLibrary )
 		usdEnv.NoCache( usdLibraryInstall )
-		usdEnv.AddPostAction( usdLibraryInstall, lambda target, source, env : makeLibSymLinks( usdEnv ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			usdEnv.AddPostAction( usdLibraryInstall, lambda target, source, env : makeLibSymLinks( usdEnv ) )
 		usdEnv.Alias( "install", usdLibraryInstall )
 		usdEnv.Alias( "installUSD", usdLibraryInstall )
 		usdEnv.Alias( "installLib", [ usdLibraryInstall ] )
 
 		# headers
 		usdHeaderInstall = usdEnv.Install( "$INSTALL_HEADER_DIR/IECoreUSD", usdHeaders )
-		usdEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreUSD", lambda target, source, env : makeSymLinks( usdEnv, usdEnv["INSTALL_HEADER_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			usdEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreUSD", lambda target, source, env : makeSymLinks( usdEnv, usdEnv["INSTALL_HEADER_DIR"] ) )
 		usdEnv.Alias( "install", usdHeaderInstall )
 		usdEnv.Alias( "installUSD", usdHeaderInstall )
 
@@ -3098,7 +3158,8 @@ if doConfigure :
 				).replace( "\\", "\\\\" ),
 			}
 		)
-		usdEnv.AddPostAction( "$INSTALL_USD_RESOURCE_DIR/IECoreUSD", lambda target, source, env : makeSymLinks( usdEnv, usdEnv["INSTALL_USD_RESOURCE_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			usdEnv.AddPostAction( "$INSTALL_USD_RESOURCE_DIR/IECoreUSD", lambda target, source, env : makeSymLinks( usdEnv, usdEnv["INSTALL_USD_RESOURCE_DIR"] ) )
 		usdEnv.Alias( "install", usdResourceInstall )
 		usdEnv.Alias( "installUSD", usdResourceInstall )
 
@@ -3114,7 +3175,8 @@ if doConfigure :
 		usdPythonModuleEnv.Depends( usdPythonModule, usdLibrary )
 
 		usdPythonModuleInstall = usdPythonModuleEnv.Install( "$INSTALL_PYTHON_DIR/IECoreUSD", usdPythonScripts + usdPythonModule )
-		usdPythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreUSD", lambda target, source, env : makeSymLinks( usdPythonModuleEnv, usdPythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			usdPythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreUSD", lambda target, source, env : makeSymLinks( usdPythonModuleEnv, usdPythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
 		usdPythonModuleEnv.Alias( "install", usdPythonModuleInstall )
 		usdPythonModuleEnv.Alias( "installUSD", usdPythonModuleInstall )
 
@@ -3122,7 +3184,7 @@ if doConfigure :
 
 		# tests
 		usdTestEnv = testEnv.Clone()
-		usdTestEnv["ENV"]["PYTHONPATH"] += os.pathsep + "./contrib/IECoreUSD/python"
+		usdTestEnv["ENV"]["PYTHONPATH"] += os.pathsep + "./contrib/IECoreUSD/python" + os.pathsep + usdTestEnv["VDB_PYTHON_PATH"]
 
 		usdLibPath = coreEnv.subst("$USD_LIB_PATH")
 		usdPythonPath = os.path.join(usdLibPath, "python")
@@ -3224,14 +3286,16 @@ if doConfigure :
 		alembicLibrary = alembicEnv.SharedLibrary( "lib/" + os.path.basename( alembicEnv.subst( "$INSTALL_ALEMBICLIB_NAME" ) ), alembicSources )
 		alembicLibraryInstall = alembicEnv.Install( os.path.dirname( alembicEnv.subst( "$INSTALL_ALEMBICLIB_NAME" ) ), alembicLibrary )
 		alembicEnv.NoCache( alembicLibraryInstall )
-		alembicEnv.AddPostAction( alembicLibraryInstall, lambda target, source, env : makeLibSymLinks( alembicEnv ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			alembicEnv.AddPostAction( alembicLibraryInstall, lambda target, source, env : makeLibSymLinks( alembicEnv ) )
 		alembicEnv.Alias( "install", alembicLibraryInstall )
 		alembicEnv.Alias( "installAlembic", alembicLibraryInstall )
 		alembicEnv.Alias( "installLib", [ alembicLibraryInstall ] )
 
 		# headers
 		alembicHeaderInstall = alembicEnv.Install( "$INSTALL_HEADER_DIR/IECoreAlembic", alembicHeaders )
-		alembicEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreAlembic", lambda target, source, env : makeSymLinks( alembicEnv, alembicEnv["INSTALL_HEADER_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			alembicEnv.AddPostAction( "$INSTALL_HEADER_DIR/IECoreAlembic", lambda target, source, env : makeSymLinks( alembicEnv, alembicEnv["INSTALL_HEADER_DIR"] ) )
 		alembicEnv.Alias( "install", alembicHeaderInstall )
 		alembicEnv.Alias( "installAlembic", alembicHeaderInstall )
 
@@ -3249,7 +3313,8 @@ if doConfigure :
 		alembicPythonModuleEnv.Depends( alembicPythonModule, scenePythonModule )
 
 		alembicPythonModuleInstall = alembicPythonModuleEnv.Install( "$INSTALL_PYTHON_DIR/IECoreAlembic", alembicPythonScripts + alembicPythonModule )
-		alembicPythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreAlembic", lambda target, source, env : makeSymLinks( alembicPythonModuleEnv, alembicPythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
+		if env[ "INSTALL_CREATE_SYMLINKS" ] :
+			alembicPythonModuleEnv.AddPostAction( "$INSTALL_PYTHON_DIR/IECoreAlembic", lambda target, source, env : makeSymLinks( alembicPythonModuleEnv, alembicPythonModuleEnv["INSTALL_PYTHON_DIR"] ) )
 		alembicPythonModuleEnv.Alias( "install", alembicPythonModuleInstall )
 		alembicPythonModuleEnv.Alias( "installAlembic", alembicPythonModuleInstall )
 
diff --git a/config/ie/buildAll b/config/ie/buildAll
index 50128aa9f6..30c6622bc6 100755
--- a/config/ie/buildAll
+++ b/config/ie/buildAll
@@ -5,9 +5,9 @@ import subprocess
 import sys
 import os
 import os.path
-import shutil
 import VersionControl
-VersionControl.setVersion( "IEBuild" )
+
+VersionControl.setVersion("IEBuild")
 import IEBuild
 
 ##########################################################################
@@ -15,124 +15,139 @@ import IEBuild
 ##########################################################################
 
 ## \todo: this is duplicated from ./options but can we centralize it instead?
-def cortexRegistryVersion() :
-
-	import re
-	varsFound = {}
-	varsToFind = [ "ieCoreMilestoneVersion", "ieCoreMajorVersion" ]
-
-	with open( "SConstruct", "r" ) as f :
-		for line in f :
-			for varName in varsToFind :
-				match = re.match( "^\s*%s\s*=\s*(?P<value>\d+).*$" % varName, line )
-				if match :
-					varsFound[varName] = match.groupdict()["value"]
-					varsToFind.remove( varName )
-					break
-			if not varsToFind:
-				break
-
-	if varsToFind :
-		raise Exception( "Could not find the Cortex version in the SConstruct file. Please review the parsing rules." )
-
-	return varsFound["ieCoreMilestoneVersion"] + "." + varsFound["ieCoreMajorVersion"]
+def cortexVersion():
+    import re
+
+    varsFound = {}
+    varNames = [
+        "ieCoreMilestoneVersion", "ieCoreMajorVersion", "ieCoreMinorVersion", "ieCorePatchVersion"
+    ]
+    varsToFind = list(varNames)
+
+    with open("SConstruct", "r") as f:
+        for line in f:
+            for varName in varsToFind:
+                match = re.match("^\s*%s\s*=\s*(?P<value>\d+).*$" % varName, line)
+                if match:
+                    varsFound[varName] = match.groupdict()["value"]
+                    varsToFind.remove(varName)
+                    break
+            if not varsToFind:
+                break
+
+    if varsToFind:
+        raise Exception(
+            "Could not find the Cortex version in the SConstruct file. Please review the parsing"
+            " rules."
+        )
+
+    return ".".join([varsFound[k] for k in varNames])
+
+
+currentCortexVersion = cortexVersion()
+cortexReg = IEEnv.findRegistryRoot("cortex", currentCortexVersion)
 
 platform = IEEnv.platform()
-cortexCompatibilityVersion = cortexRegistryVersion()
-cortexReg = IEEnv.registry["libraries"]["cortex"][cortexCompatibilityVersion][platform]
 
 ##########################################################################
 # Run a single build
 ##########################################################################
 
-def build( extraArgs = [] ) :
 
-	sysArgs = sys.argv[1:]
+def build(extraArgs=[]):
+    sysArgs = sys.argv[1:]
 
-	install = False
-	if "install" in sysArgs :
-		install = True
-		sysArgs.remove( "install" )
+    install = False
+    if "install" in sysArgs:
+        install = True
+        sysArgs.remove("install")
 
-	release = False
-	if "RELEASE=1" in sysArgs :
-		release = True
+    release = False
+    if "RELEASE=1" in sysArgs:
+        release = True
 
-	if "J=" in " ".join( sysArgs ) :
-		sysArgs = " ".join( sysArgs ).replace( "J=", "-j " ).split( " " )
+    if "J=" in " ".join(sysArgs):
+        sysArgs = " ".join(sysArgs).replace("J=", "-j ").split(" ")
 
-	installPrefix = "/software" if release else os.path.expanduser( "~" )
-	buildArgs = [ "INSTALL_PREFIX=" + installPrefix ]
-	buildArgs.extend( extraArgs )
-	buildArgs.extend( sysArgs )
+    installPrefix = "/software" if release else os.path.expanduser("~")
+    buildArgs = ["INSTALL_PREFIX=" + installPrefix]
+    buildArgs.extend(extraArgs)
+    buildArgs.extend(sysArgs)
 
-	argsToValidate = [ "CORTEX_VERSION={}".format( cortexCompatibilityVersion ) ] + extraArgs
-	if not IEEnv.Registry.validateVariation( argsToValidate ) :
-		print( "Skipped invalid variation combination: " + str(argsToValidate) + "\n" )
-		return
+    argsToValidate = ["CORTEX_VERSION={}".format(currentCortexVersion)] + extraArgs
+    if not IEEnv.Registry.validateVariation(argsToValidate):
+        print("Skipped invalid variation combination: " + str(argsToValidate) + "\n")
+        return
 
-	cmd = [ "scons", "install" ] if install or release else [ "scons" ]
+    cmd = ["scons", "install"] if install or release else ["scons"]
 
-	print( " ".join( cmd + buildArgs ) )
-	if "DRYRUN=1" in sysArgs :
-		return
+    print(" ".join(cmd + buildArgs))
+    if "DRYRUN=1" in sysArgs:
+        return
+
+    if subprocess.call(cmd + buildArgs) != 0:
+        raise RuntimeError("Error : " + str(" ".join(cmd + buildArgs)))
+    print("Build succeeded: " + " ".join(cmd + buildArgs) + "\n")
 
-	if subprocess.call( cmd + buildArgs ) != 0 :
-		raise RuntimeError( "Error : " + str( " ".join( cmd + buildArgs ) ) )
-	print( "Build succeeded: " + " ".join( cmd + buildArgs ) + "\n" )
 
 ##########################################################################
 # Build docs only
 ##########################################################################
 
-def installDocs() :
 
-	sysArgs = sys.argv[1:]
+def installDocs():
+    sysArgs = sys.argv[1:]
 
-	if "RELEASE=1" not in sysArgs :
-		return
+    if "RELEASE=1" not in sysArgs:
+        return
 
-	buildArgs = [ "INSTALL_PREFIX=/software" ]
-	buildArgs.extend( sysArgs )
+    buildArgs = ["INSTALL_PREFIX=/software"]
+    buildArgs.extend(sysArgs)
 
-	cmd = [ "scons", "installDoc" ]
-	print( " ".join( cmd + buildArgs ) )
-	if "DRYRUN=1" in sysArgs :
-		return
+    cmd = ["scons", "installDoc"]
+    print(" ".join(cmd + buildArgs))
+    if "DRYRUN=1" in sysArgs:
+        return
 
-	if subprocess.call( cmd + buildArgs ) != 0 :
+    if subprocess.call(cmd + buildArgs) != 0:
+        raise RuntimeError("Error : scons installDoc " + str(" ".join(buildArgs)))
 
-		raise RuntimeError("Error : scons installDoc " + str( " ".join( buildArgs ) ) )
 
 ##########################################################################
 # Loop over all builds
 ##########################################################################
 
-compilerVersions = IEBuild.utils.versionsToInstall( "gcc" )
-pythonVersions = IEBuild.utils.versionsToInstall( "python" )
-mayaVersions = IEBuild.utils.versionsToInstall( "maya" )
-nukeVersions = IEBuild.utils.versionsToInstall( "nuke" )
-houdiniVersions = IEBuild.utils.versionsToInstall( "houdini" )
-rvVersions = IEBuild.utils.versionsToInstall( "rv" )
+compilerVersions = IEBuild.utils.versionsToInstall("gcc")
+pythonVersions = IEBuild.utils.versionsToInstall("python")
+mayaVersions = IEBuild.utils.versionsToInstall("maya")
+nukeVersions = IEBuild.utils.versionsToInstall("nuke")
+houdiniVersions = IEBuild.utils.versionsToInstall("houdini")
+rvVersions = IEBuild.utils.versionsToInstall("rv")
 
 
 for compilerVersion in compilerVersions:
-	for pythonVersion in pythonVersions :
-		build( [ "COMPILER_VERSION="+compilerVersion, "PYTHON_VERSION="+pythonVersion, "DL_VERSION=UNDEFINED" ] )
-
-for mayaVersion in mayaVersions :
-	compilerVersion = IEEnv.registry["apps"]["maya"][mayaVersion][platform]["compilerVersion"]
-	build( [ "APP=maya", "APP_VERSION="+mayaVersion ] )
-
-for nukeVersion in nukeVersions :
-	compilerVersion = IEEnv.registry["apps"]["nuke"][nukeVersion][platform]["compilerVersion"]
-	build( [ "APP=nuke", "APP_VERSION="+nukeVersion ] )
-
-for houdiniVersion in houdiniVersions :
-	compilerVersion = IEEnv.registry["apps"]["houdini"][houdiniVersion][platform]["compilerVersion"]
-	build( [ "APP=houdini", "APP_VERSION="+houdiniVersion ] )
-
-for rvVersion in rvVersions :
-	build( [ "APP=rv", "APP_VERSION="+rvVersion, "DL_VERSION=UNDEFINED" ] )
+    for pythonVersion in pythonVersions:
+        build(
+            [
+                "COMPILER_VERSION=" + compilerVersion,
+                "PYTHON_VERSION=" + pythonVersion,
+                "DL_VERSION=UNDEFINED",
+            ]
+        )
+
+for mayaVersion in mayaVersions:
+    compilerVersion = IEEnv.registry["apps"]["maya"][mayaVersion][platform]["compilerVersion"]
+    build(["APP=maya", "APP_VERSION=" + mayaVersion])
+
+for nukeVersion in nukeVersions:
+    compilerVersion = IEEnv.registry["apps"]["nuke"][nukeVersion][platform]["compilerVersion"]
+    build(["APP=nuke", "APP_VERSION=" + nukeVersion])
+
+for houdiniVersion in houdiniVersions:
+    compilerVersion = IEEnv.registry["apps"]["houdini"][houdiniVersion][platform]["compilerVersion"]
+    build(["APP=houdini", "APP_VERSION=" + houdiniVersion])
+
+for rvVersion in rvVersions:
+    build(["APP=rv", "APP_VERSION=" + rvVersion, "DL_VERSION=UNDEFINED"])
 
 installDocs()
diff --git a/config/ie/options b/config/ie/options
index e650468f64..f345f6d331 100644
--- a/config/ie/options
+++ b/config/ie/options
@@ -33,24 +33,23 @@
 ##########################################################################
 
 import distutils
+from distutils.version import LooseVersion
 import os
-import sys
-import os.path
 import IEEnv
 import re
-import copy
 import getpass
-import string
 
-def getOption( name, default ) :
 
-	import sys
-	result = default
-	for a in sys.argv:
-		if a[:len(name)+1]==name+"=" :
-			result = a[len(name)+1:]
+def getOption(name, default):
+    import sys
+
+    result = default
+    for a in sys.argv:
+        if a[: len(name) + 1] == name + "=":
+            result = a[len(name) + 1 :]
+
+    return result
 
-	return result
 
 # decide what compiler/app we're targeting. we either pass COMPILER_VERSION, COMPILER and PYTHON_VERSION
 # to do a straight install in /software/apps/cortex/<cortexVersion>/$PLATFORM/base/ or we pass an APP and APP_VERSION pair
@@ -59,10 +58,10 @@ def getOption( name, default ) :
 
 platform = IEEnv.platform()
 
-compiler = getOption( "COMPILER", None )
-compilerVersion = getOption( "COMPILER_VERSION", None )
-pythonVersion = getOption( "PYTHON_VERSION", None )
-targetApp = getOption( "APP", None )
+compiler = getOption("COMPILER", None)
+compilerVersion = getOption("COMPILER_VERSION", None)
+pythonVersion = getOption("PYTHON_VERSION", None)
+targetApp = getOption("APP", None)
 
 
 # get cortex config information from the registry. if we have setting specific to this platform then use them, otherwise
@@ -72,420 +71,638 @@ targetApp = getOption( "APP", None )
 # parse SConstruct file for the cortex version
 ##########################################################################
 
-def cortexRegistryVersion() :
-
-	import re
-	varsFound = {}
-	varsToFind = [ "ieCoreMilestoneVersion", "ieCoreMajorVersion" ]
-
-	with open( "SConstruct", "r" ) as f :
-		for line in f :
-			for varName in varsToFind :
-				match = re.match( "^\s*%s\s*=\s*(?P<value>\d+).*$" % varName, line )
-				if match :
-					varsFound[varName] = match.groupdict()["value"]
-					varsToFind.remove( varName )
-					break
-			if not varsToFind:
-				break
-
-	if varsToFind :
-		raise Exception( "Could not find the Cortex version in the SConstruct file. Please review the parsing rules." )
-
-	return varsFound["ieCoreMilestoneVersion"] + "." + varsFound["ieCoreMajorVersion"]
-
-cortexReg = IEEnv.registry["libraries"]["cortex"][cortexRegistryVersion()][platform]
-
-if targetApp :
-
-	if targetApp not in ( "nuke", "maya", "houdini", "rv" ) :
-		raise RuntimeError( "Check config logic applies to the new app and remove this exception." )
-
-	if (compiler or compilerVersion) :
-		raise RuntimeError( "Must specify only one of COMPILER or APP" )
-
-	if pythonVersion :
-		raise RuntimeError( "Must specify only one of PYTHON_VERSION or APP" )
-
-	targetAppVersion = getOption( "APP_VERSION", os.environ.get( targetApp.upper() + "_VERSION" ) )
-
-	targetAppReg = IEEnv.registry["apps"][targetApp][targetAppVersion][platform]
-	compiler = targetAppReg["compiler"]
-	compilerVersion = targetAppReg["compilerVersion"]
-	cxxStd = targetAppReg.get( "cxxStd", cortexReg.get( "cxxStd", "c++11" ) )
-	openEXRVersion = targetAppReg.get( "OpenEXRVersion", cortexReg["OpenEXRVersion"] )
-	alembicVersion = targetAppReg.get( "AlembicVersion", cortexReg["AlembicVersion"] )
-	vdbVersion = targetAppReg.get( "OpenVDBVersion", cortexReg["OpenVDBVersion"] )
-	bloscVersion = targetAppReg.get( "bloscVersion", cortexReg["bloscVersion"] )
-	hdf5Version = targetAppReg.get( "hdf5Version", cortexReg["hdf5Version"] )
-	glewVersion = targetAppReg.get( "glewVersion", cortexReg["glewVersion"] )
-	pythonVersion = targetAppReg["pythonVersion"]
-	boostVersion = targetAppReg.get( "boostVersion", cortexReg["boostVersion"] )
-	oiioVersion = targetAppReg.get( "OpenImageIOVersion", cortexReg["OpenImageIOVersion"] )
-	oiioLibSuffix = targetAppReg.get( "OpenImageIOLibSuffix", oiioVersion )
-	tbbVersion = targetAppReg.get( "tbbVersion", cortexReg["tbbVersion"] )
-	usdVersion = targetAppReg.get( "usdVersion", cortexReg.get("usdVersion", None) )
-	targetAppMajorVersion = targetAppReg.get( "majorVersion", targetAppVersion )
-	compatibilityVersion = targetAppReg.get( "compatibilityVersion", targetAppMajorVersion )
-else :
-	platformReg = IEEnv.registry["platformDefaults"][IEEnv.platform()]
-	if not compiler :
-		compiler = cortexReg.get( "compiler", platformReg["compiler"] )
-	if not compilerVersion :
-		compilerVersion = cortexReg.get( "compilerVersion", platformReg["compilerVersion"] )
-	if not pythonVersion :
-		pythonVersion = cortexReg["preferredPythonVersion"]
-	cxxStd = cortexReg.get( "cxxStd", "c++11" )
-	openEXRVersion = cortexReg["OpenEXRVersion"]
-	alembicVersion = cortexReg["AlembicVersion"]
-	vdbVersion = cortexReg.get("OpenVDBVersion", "4.0.2")
-	bloscVersion = cortexReg.get("bloscVersion" )
-	hdf5Version = cortexReg["hdf5Version"]
-	glewVersion = cortexReg["glewVersion"]
-	tbbVersion = cortexReg["tbbVersion"]
-	usdVersion = cortexReg.get("usdVersion", None)
-	boostVersion = cortexReg["boostVersion"]
-	oiioVersion = cortexReg["OpenImageIOVersion"]
-	oiioLibSuffix = oiioVersion
+
+def cortexVersion():
+    import re
+
+    varsFound = {}
+    varNames = [
+        "ieCoreMilestoneVersion",
+        "ieCoreMajorVersion",
+        "ieCoreMinorVersion",
+        "ieCorePatchVersion",
+    ]
+    varsToFind = list(varNames)
+
+    with open("SConstruct", "r") as f:
+        for line in f:
+            for varName in varsToFind:
+                match = re.match("^\s*%s\s*=\s*(?P<value>\d+).*$" % varName, line)
+                if match:
+                    varsFound[varName] = match.groupdict()["value"]
+                    varsToFind.remove(varName)
+                    break
+            if not varsToFind:
+                break
+
+    if varsToFind:
+        raise Exception(
+            "Could not find the Cortex version in the SConstruct file. Please review the parsing"
+            " rules."
+        )
+
+    return ".".join([varsFound[k] for k in varNames])
+
+
+currentCortexVersion = cortexVersion()
+cortexReg = IEEnv.findRegistryRoot("cortex", currentCortexVersion)
+
+if LooseVersion(currentCortexVersion) >= LooseVersion("10.5.9"):
+    INSTALL_CREATE_SYMLINKS = False
+
+if targetApp:
+    if targetApp not in ("nuke", "maya", "houdini", "rv"):
+        raise RuntimeError("Check config logic applies to the new app and remove this exception.")
+
+    if compiler or compilerVersion:
+        raise RuntimeError("Must specify only one of COMPILER or APP")
+
+    if pythonVersion:
+        raise RuntimeError("Must specify only one of PYTHON_VERSION or APP")
+
+    targetAppVersion = getOption("APP_VERSION", os.environ.get(targetApp.upper() + "_VERSION"))
+
+    targetAppReg = IEEnv.registry["apps"][targetApp][targetAppVersion][platform]
+    compiler = targetAppReg["compiler"]
+    compilerVersion = targetAppReg["compilerVersion"]
+    cxxStd = targetAppReg.get("cxxStd", cortexReg.get("cxxStd", "c++11"))
+    openEXRVersion = targetAppReg.get("OpenEXRVersion", cortexReg["OpenEXRVersion"])
+    alembicVersion = targetAppReg.get("AlembicVersion", cortexReg["AlembicVersion"])
+    vdbVersion = targetAppReg.get("OpenVDBVersion", cortexReg["OpenVDBVersion"])
+    pybind11Version = targetAppReg.get("pybind11", cortexReg.get("pybind11", "2.10.4"))
+    bloscVersion = targetAppReg.get("bloscVersion", cortexReg["bloscVersion"])
+    hdf5Version = targetAppReg.get("hdf5Version", cortexReg["hdf5Version"])
+    glewVersion = targetAppReg.get("glewVersion", cortexReg["glewVersion"])
+    pythonVersion = targetAppReg["pythonVersion"]
+    boostVersion = targetAppReg.get("boostVersion", cortexReg["boostVersion"])
+    oiioVersion = targetAppReg.get("OpenImageIOVersion", cortexReg["OpenImageIOVersion"])
+    oiioLibSuffix = targetAppReg.get("OpenImageIOLibSuffix", oiioVersion)
+    tbbVersion = targetAppReg.get("tbbVersion", cortexReg["tbbVersion"])
+    usdVersion = targetAppReg.get("usdVersion", cortexReg.get("usdVersion", None))
+    targetAppMajorVersion = targetAppReg.get("majorVersion", targetAppVersion)
+    compatibilityVersion = targetAppReg.get("compatibilityVersion", targetAppMajorVersion)
+else:
+    platformReg = IEEnv.registry["platformDefaults"][IEEnv.platform()]
+    if not compiler:
+        compiler = cortexReg.get("compiler", platformReg["compiler"])
+    if not compilerVersion:
+        compilerVersion = cortexReg.get("compilerVersion", platformReg["compilerVersion"])
+    if not pythonVersion:
+        pythonVersion = cortexReg["preferredPythonVersion"]
+    cxxStd = cortexReg.get("cxxStd", "c++11")
+    openEXRVersion = cortexReg["OpenEXRVersion"]
+    alembicVersion = cortexReg["AlembicVersion"]
+    vdbVersion = cortexReg.get("OpenVDBVersion", "4.0.2")
+    pybind11Version = cortexReg.get("pybind11", "2.10.4")
+    bloscVersion = cortexReg.get("bloscVersion")
+    hdf5Version = cortexReg["hdf5Version"]
+    glewVersion = cortexReg["glewVersion"]
+    tbbVersion = cortexReg["tbbVersion"]
+    usdVersion = cortexReg.get("usdVersion", None)
+    boostVersion = cortexReg["boostVersion"]
+    oiioVersion = cortexReg["OpenImageIOVersion"]
+    oiioLibSuffix = oiioVersion
 
 # get the compiler location using the registry
 compilerReg = IEEnv.registry["compilers"][compiler][compilerVersion][platform]
 
-CXX = os.path.join( compilerReg["location"], compilerReg["bin"] )
-CXXSTD = getOption( "CXXSTD", cxxStd )
+CXX = os.path.join(compilerReg["location"], compilerReg["bin"])
+CXXSTD = getOption("CXXSTD", cxxStd)
 
-m = re.compile( "^([0-9]+)\.([0-9]+)\.([0-9]+)$" ).match( compilerVersion )
-if m :
-	compilerMajorVersion, compilerMinorVersion, compilerPatchVersion = m.group( 1, 2, 3 )
-	compilerVersionInt = int(compilerMajorVersion) * 100 + int(compilerMinorVersion) * 10 + int(compilerPatchVersion)
-else :
-	m = re.compile( "^([0-9]+)\.([0-9]+)$" ).match( compilerVersion )
-	compilerMajorVersion, compilerMinorVersion = m.group( 1, 2 )
-	compilerVersionInt = int(compilerMajorVersion) * 100 + int(compilerMinorVersion) * 10 + 9
+m = re.compile("^([0-9]+)\.([0-9]+)\.([0-9]+)$").match(compilerVersion)
+if m:
+    compilerMajorVersion, compilerMinorVersion, compilerPatchVersion = m.group(1, 2, 3)
+    compilerVersionInt = (
+        int(compilerMajorVersion) * 100 + int(compilerMinorVersion) * 10 + int(compilerPatchVersion)
+    )
+else:
+    m = re.compile("^([0-9]+)\.([0-9]+)$").match(compilerVersion)
+    compilerMajorVersion, compilerMinorVersion = m.group(1, 2)
+    compilerVersionInt = int(compilerMajorVersion) * 100 + int(compilerMinorVersion) * 10 + 9
 
-if not m :
-	raise RuntimeError( "Cannot determine compiler version (%s)" % compilerVersion )
+if not m:
+    raise RuntimeError("Cannot determine compiler version (%s)" % compilerVersion)
 
-CXXFLAGS = [ "-pipe", "-Wall", "-Wextra", "-pthread" ]
+# TODO: TBB_SUPPRESS_DEPRECATED_MESSAGES is required because tbb/mutex.h and tbb/recursive_mutex.h
+# are deprecated by tbb. The recommendation is to use std::mutex and std:recursive_mutex
+CXXFLAGS = ["-pipe", "-Wall", "-Wextra", "-pthread", "-DTBB_SUPPRESS_DEPRECATED_MESSAGES"]
 
 LINKFLAGS = []
 
 # set the dependency paths
-TBB_INCLUDE_PATH = os.path.join( "/software/apps/tbb", tbbVersion, platform, compiler, compilerVersion, "include" )
-TBB_LIB_PATH = os.path.join( "/software/apps/tbb", tbbVersion, platform, compiler, compilerVersion, "lib" )
-TBB_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix( "tbb", tbbVersion )
+TBB_INCLUDE_PATH = os.path.join(
+    "/software/apps/tbb", tbbVersion, platform, compiler, compilerVersion, "include"
+)
+TBB_LIB_PATH = os.path.join(
+    "/software/apps/tbb", tbbVersion, platform, compiler, compilerVersion, "lib"
+)
+TBB_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix("tbb", tbbVersion)
 
-BOOST_INCLUDE_PATH = os.path.join( "/software/tools/include", platform, "boost", boostVersion )
-BOOST_LIB_PATH = os.path.join( "/software", "tools", "lib", platform, compiler, compilerVersion )
-BOOST_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix( "boost", boostVersion, { "compiler" : compiler, "compilerVersion" : compilerVersion } )
+BOOST_INCLUDE_PATH = os.path.join("/software/tools/include", platform, "boost", boostVersion)
+BOOST_LIB_PATH = os.path.join("/software", "tools", "lib", platform, compiler, compilerVersion)
+BOOST_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix(
+    "boost", boostVersion, {"compiler": compiler, "compilerVersion": compilerVersion}
+)
 BOOST_PYTHON_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix(
-	"boost_python", boostVersion, { "compiler" : compiler, "compilerVersion" : compilerVersion, "pythonVersion" : pythonVersion }
+    "boost_python",
+    boostVersion,
+    {"compiler": compiler, "compilerVersion": compilerVersion, "pythonVersion": pythonVersion},
 )
 
 OPENEXR_INCLUDE_PATH = "/software/tools/include/" + platform + "/OpenEXR/" + openEXRVersion
-OPENEXR_LIB_PATH = os.path.join( "/software", "tools", "lib", platform, compiler, compilerVersion )
-ILMBASE_LIB_PATH = os.path.join( "/software", "tools", "lib", platform, compiler, compilerVersion )
-OPENEXR_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix( "OpenEXR", openEXRVersion )
+OPENEXR_LIB_PATH = os.path.join("/software", "tools", "lib", platform, compiler, compilerVersion)
+ILMBASE_LIB_PATH = os.path.join("/software", "tools", "lib", platform, compiler, compilerVersion)
+OPENEXR_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix("OpenEXR", openEXRVersion)
 
 GLEW_INCLUDE_PATH = "/software/tools/include/" + platform + "/glew/" + glewVersion
-GLEW_LIB_PATH = os.path.join( "/software", "tools", "lib", platform, compiler, compilerVersion )
-GLEW_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix( "glew", glewVersion )
+GLEW_LIB_PATH = os.path.join("/software", "tools", "lib", platform, compiler, compilerVersion)
+GLEW_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix("glew", glewVersion)
 
-oiioRoot = os.path.join( "/software", "apps", "OpenImageIO", oiioVersion, platform, compiler, compilerVersion )
-OIIO_INCLUDE_PATH = os.path.join( oiioRoot, "include" )
-OIIO_LIB_PATH = os.path.join( oiioRoot, "lib64" )
-OIIO_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix( "OpenImageIO", oiioLibSuffix )
+oiioRoot = os.path.join(
+    "/software", "apps", "OpenImageIO", oiioVersion, platform, compiler, compilerVersion
+)
+OIIO_INCLUDE_PATH = os.path.join(oiioRoot, "include")
+OIIO_LIB_PATH = os.path.join(oiioRoot, "lib64")
+OIIO_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix("OpenImageIO", oiioLibSuffix)
 WITH_OIIO_UTIL = "true"
 
-FREETYPE_LIB_PATH = os.path.join( "/software", "tools", "lib", platform, compiler, compilerVersion )
+FREETYPE_LIB_PATH = os.path.join("/software", "tools", "lib", platform, compiler, compilerVersion)
 FREETYPE_INCLUDE_PATH = "/usr/include/freetype2"
 
 # find the right libraries based on compiler and platform
-LIBPATH = ":".join( [
-	os.path.join( "/software", "tools", "lib", platform, compiler, compilerVersion ),
-	os.path.join( "/software", "apps", compiler, compilerVersion, platform, "lib64" ),
-] )
-if targetApp :
-	libPaths = []
-	for libPath in targetAppReg.get( "libPaths", [] ):
-		libPaths.append( os.path.join( targetAppReg["location"], libPath ) )
-	libPaths.append( LIBPATH )
-	LIBPATH = ":".join( libPaths )
+LIBPATH = ":".join(
+    [
+        os.path.join("/software", "tools", "lib", platform, compiler, compilerVersion),
+        os.path.join("/software", "apps", compiler, compilerVersion, platform, "lib64"),
+    ]
+)
+if targetApp:
+    libPaths = []
+    for libPath in targetAppReg.get("libPaths", []):
+        libPaths.append(os.path.join(targetAppReg["location"], libPath))
+    libPaths.append(LIBPATH)
+    LIBPATH = ":".join(libPaths)
 
 # ignore python-config stuff and specify it all explicitly ourselves
-pythonReg = IEEnv.registry['apps']['python'][pythonVersion][platform]
+pythonReg = IEEnv.registry["apps"]["python"][pythonVersion][platform]
 
-pythonRoot = os.path.join( pythonReg["location"], compiler, compilerVersion )
+pythonRoot = os.path.join(pythonReg["location"], compiler, compilerVersion)
 
 PYTHON = "/software/tools/wrappers/iePython%s" % pythonVersion
-PYTHON_INCLUDE_PATH = pythonReg["location"] + "/" + compiler + "/" + compilerVersion + "/" + pythonReg["include"] + "/python" + pythonVersion
+PYTHON_INCLUDE_PATH = (
+    pythonReg["location"]
+    + "/"
+    + compiler
+    + "/"
+    + compilerVersion
+    + "/"
+    + pythonReg["include"]
+    + "/python"
+    + pythonVersion
+)
 PYTHON_LINK_FLAGS = pythonReg["moduleLinkFlags"]
 
-if PYTHON_LINK_FLAGS=="" :
-	PYTHON_LINK_FLAGS = "-L" + pythonReg["location"] + "/" + compiler + "/" + compilerVersion + "/lib -lpython" + pythonVersion
-
-if distutils.version.LooseVersion( openEXRVersion ) < distutils.version.LooseVersion( "2.4.1" ) :
-	openEXRPythonModuleDir = os.path.join( "/software", "apps", "openexr", openEXRVersion, platform, compiler, compilerVersion, "python", pythonVersion, "boost", boostVersion, "lib64", "python" + pythonVersion, "site-packages" )
-	openEXRPythonLibDir = os.path.join( "/software", "apps", "openexr", openEXRVersion, platform, compiler, compilerVersion, "python", pythonVersion, "boost", boostVersion, "lib" )
-else :
-	openEXRPythonModuleDir = os.path.join( "/software", "apps", "openexr", openEXRVersion, platform, compiler, compilerVersion, "python", pythonVersion, "boost", boostVersion, "lib", "python" + pythonVersion, "site-packages" )
-	openEXRPythonLibDir = os.path.join( "/software", "apps", "openexr", openEXRVersion, platform, compiler, compilerVersion, "python", pythonVersion, "boost", boostVersion, "lib64" )
+if PYTHON_LINK_FLAGS == "":
+    PYTHON_LINK_FLAGS = (
+        "-L"
+        + pythonReg["location"]
+        + "/"
+        + compiler
+        + "/"
+        + compilerVersion
+        + "/lib -lpython"
+        + pythonVersion
+    )
+
+if distutils.version.LooseVersion(openEXRVersion) < distutils.version.LooseVersion("2.4.1"):
+    openEXRPythonModuleDir = os.path.join(
+        "/software",
+        "apps",
+        "openexr",
+        openEXRVersion,
+        platform,
+        compiler,
+        compilerVersion,
+        "python",
+        pythonVersion,
+        "boost",
+        boostVersion,
+        "lib64",
+        "python" + pythonVersion,
+        "site-packages",
+    )
+    openEXRPythonLibDir = os.path.join(
+        "/software",
+        "apps",
+        "openexr",
+        openEXRVersion,
+        platform,
+        compiler,
+        compilerVersion,
+        "python",
+        pythonVersion,
+        "boost",
+        boostVersion,
+        "lib",
+    )
+else:
+    openEXRPythonModuleDir = os.path.join(
+        "/software",
+        "apps",
+        "openexr",
+        openEXRVersion,
+        platform,
+        compiler,
+        compilerVersion,
+        "lib",
+        "python" + pythonVersion,
+        "site-packages",
+    )
+    openEXRPythonLibDir = os.path.join(
+        "/software",
+        "apps",
+        "openexr",
+        openEXRVersion,
+        platform,
+        compiler,
+        compilerVersion,
+        "lib",
+    )
 
 PYTHONPATH = openEXRPythonModuleDir
 
 # get the installation locations right
-INSTALL_PREFIX = getOption( "INSTALL_PREFIX", os.path.expanduser( "~" ) )
-installPrefix = os.path.join( "$INSTALL_PREFIX", "apps", "cortex", "${IECORE_VERSION}", platform )
+INSTALL_PREFIX = getOption("INSTALL_PREFIX", os.path.expanduser("~"))
+installPrefix = os.path.join("$INSTALL_PREFIX", "apps", "cortex", "${IECORE_VERSION}", platform)
 
 # add the dev suffix to local builds
-if getOption( "RELEASE", "0" )!="1" :
-	installPrefix = os.path.join( "$INSTALL_PREFIX", "apps", "cortex", "${IECORE_VERSION}dev", platform )
-
-basePrefix = os.path.join( installPrefix, "base" )
-if targetApp :
-	if targetApp in ( "nuke", ):
-		appPrefix = os.path.join( installPrefix, targetApp, compatibilityVersion )
-	else:
-		appPrefix = os.path.join( installPrefix, targetApp, targetAppMajorVersion )
+if getOption("RELEASE", "0") != "1":
+    installPrefix = os.path.join(
+        "$INSTALL_PREFIX", "apps", "cortex", "${IECORE_VERSION}dev", platform
+    )
+
+basePrefix = os.path.join(installPrefix, "base")
+if targetApp:
+    if targetApp in ("nuke",):
+        appPrefix = os.path.join(installPrefix, targetApp, compatibilityVersion)
+    else:
+        appPrefix = os.path.join(installPrefix, targetApp, targetAppMajorVersion)
 
 # ask for opengl support
-WITH_GL=1
+WITH_GL = 1
 
 # find alembic:
-ALEMBIC_INCLUDE_PATH = os.path.join( "/software", "apps", "Alembic", alembicVersion, platform, compiler, compilerVersion, "include" )
-ALEMBIC_LIB_PATH = os.path.join( "/software", "apps", "Alembic", alembicVersion, platform, compiler, compilerVersion, "lib" )
-ALEMBIC_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix( "Alembic", alembicVersion )
+ALEMBIC_INCLUDE_PATH = os.path.join(
+    "/software", "apps", "Alembic", alembicVersion, platform, compiler, compilerVersion, "include"
+)
+ALEMBIC_LIB_PATH = os.path.join(
+    "/software", "apps", "Alembic", alembicVersion, platform, compiler, compilerVersion, "lib"
+)
+ALEMBIC_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix("Alembic", alembicVersion)
 
-VDB_INCLUDE_PATH = os.path.join( "/software", "apps", "OpenVDB", vdbVersion, platform, compiler, compilerVersion, "include" )
-VDB_LIB_PATH = os.path.join( "/software", "apps", "OpenVDB", vdbVersion, platform, compiler, compilerVersion, "lib" )
-VDB_PYTHON_PATH = os.path.join( "/software", "apps", "OpenVDB", vdbVersion, platform, compiler, compilerVersion, "python", "lib", "python"+pythonVersion )
+VDB_INCLUDE_PATH = os.path.join(
+    "/software", "apps", "OpenVDB", vdbVersion, platform, compiler, compilerVersion, "include"
+)
+VDB_LIB_PATH = os.path.join(
+    "/software", "apps", "OpenVDB", vdbVersion, platform, compiler, compilerVersion, "lib"
+)
+VDB_PYTHON_PATH = os.path.join(
+    "/software",
+    "apps",
+    "OpenVDB",
+    vdbVersion,
+    platform,
+    compiler,
+    compilerVersion,
+    "python",
+    "lib",
+    "python" + pythonVersion,
+)
 
-BLOSC_INCLUDE_PATH = os.path.join( "/software/tools/include/", platform, "blosc", bloscVersion )
-BLOSC_LIB_PATH = os.path.join("/software/tools/lib", platform, compiler, compilerVersion )
-BLOSC_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix( "blosc", bloscVersion )
+
+PYBIND11_INCLUDE_PATH = os.path.join(
+    "/software", "tools", "include", platform, "pybind11", pybind11Version
+)
+BLOSC_INCLUDE_PATH = os.path.join("/software/tools/include/", platform, "blosc", bloscVersion)
+BLOSC_LIB_PATH = os.path.join("/software/tools/lib", platform, compiler, compilerVersion)
+BLOSC_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix("blosc", bloscVersion)
 
 # find USD:
 
-usdReg = IEEnv.registry["apps"]["usd"].get( usdVersion, {} ).get( platform )
-if usdReg :
-	if targetApp :
-		USD_INCLUDE_PATH = os.path.join( usdReg["location"], targetApp, compatibilityVersion, "include" )
-		USD_LIB_PATH = os.path.join( usdReg["location"], targetApp, compatibilityVersion, "lib" )
-		USD_LIB_PREFIX = "usd_"
-	else:
-		USD_INCLUDE_PATH = os.path.join( usdReg["location"], compiler, compilerVersion, "python", pythonVersion, "cortex", "$IECORE_COMPATIBILITY_VERSION", "include" )
-		USD_LIB_PATH = os.path.join( usdReg["location"], compiler, compilerVersion, "python", pythonVersion, "cortex", "$IECORE_COMPATIBILITY_VERSION", "lib" )
-		USD_LIB_PREFIX = "usd_"
+usdReg = IEEnv.registry["apps"]["usd"].get(usdVersion, {}).get(platform)
+if usdReg:
+    if LooseVersion(currentCortexVersion) < LooseVersion("10.5.9") and targetApp:
+        USD_INCLUDE_PATH = os.path.join(
+            usdReg["location"], targetApp, compatibilityVersion, "include"
+        )
+        USD_LIB_PATH = os.path.join(usdReg["location"], targetApp, compatibilityVersion, "lib")
+        USD_LIB_PREFIX = "usd_"
+    else:
+        USD_INCLUDE_PATH = os.path.join(
+            usdReg["location"],
+            compiler,
+            compilerVersion,
+            "python",
+            pythonVersion,
+            "cortex",
+            "$IECORE_COMPATIBILITY_VERSION",
+            "include",
+        )
+        USD_LIB_PATH = os.path.join(
+            usdReg["location"],
+            compiler,
+            compilerVersion,
+            "python",
+            pythonVersion,
+            "cortex",
+            "$IECORE_COMPATIBILITY_VERSION",
+            "lib",
+        )
+        USD_LIB_PREFIX = "usd_"
 
 # find hdf5:
-HDF5_INCLUDE_PATH = os.path.join( "/software/apps/hdf5", hdf5Version, platform, compiler, compilerVersion, "include" )
-HDF5_LIB_PATH = os.path.join( "/software/apps/hdf5", hdf5Version, platform, compiler, compilerVersion, "lib" )
+HDF5_INCLUDE_PATH = os.path.join(
+    "/software/apps/hdf5", hdf5Version, platform, compiler, compilerVersion, "include"
+)
+HDF5_LIB_PATH = os.path.join(
+    "/software/apps/hdf5", hdf5Version, platform, compiler, compilerVersion, "lib"
+)
+
+# options file location (so we can refer to post install files later)
+optionsFile = os.getenv("CORTEX_OPTIONS_FILE", "config/ie/options")
+optionsDir = os.path.dirname(optionsFile)
+
+ieCoreLibSuffix = ""
+ieCorePythonLibSuffix = "-python$PYTHON_VERSION"
+if LooseVersion(currentCortexVersion) < LooseVersion("10.5.9") and targetApp:
+    ieCoreLibSuffix = "-$IECORE_COMPATIBILITY_VERSION"
 
 # find maya if we're building for maya
-if targetApp=="maya" :
-
-	mayaVersion = targetAppVersion
-
-	mayaReg = IEEnv.registry["apps"]["maya"][mayaVersion][platform]
-	MAYA_ROOT = mayaReg["location"]
-	INSTALL_MAYALIB_NAME = os.path.join( appPrefix, "lib", "$IECORE_NAME-$IECORE_COMPATIBILITY_VERSION" )
-	INSTALL_MEL_DIR = os.path.join( appPrefix, "mel", "$IECORE_NAME" )
-	INSTALL_MAYAPLUGIN_NAME = os.path.join( appPrefix, "plugins", "$IECORE_NAME" )
-	INSTALL_MAYAICON_DIR = os.path.join( appPrefix, "icons" )
-	INSTALL_COREMAYA_POST_COMMAND="ieEnvExec {workingPath} scons -i -f config/ie/postCoreMayaInstall MAYA_VERSION='{mayaVersion}' INSTALLPREFIX={appPrefix} install".format(
-		workingPath = os.environ["IEENV_WORKING_PATH"],
-		mayaVersion = mayaVersion,
-		appPrefix = appPrefix
-	)
-	WITH_MAYA_PLUGIN_LOADER = 1
-
-	mayaUsdVersion = mayaReg.get( "mayaUsdVersion" )
-	mayaUsdReg = IEEnv.registry["apps"]["mayaUsd"].get( mayaUsdVersion, {} ).get( platform )
-	# If the Maya usd plugin is not in the registry we build against our standalone USD version
-	if mayaUsdReg :
-		pluginUsdVersion = mayaUsdReg["usdVersion"]
-		# Maya ships the USD libraries with the installation, but not the header files... we use the one that are installed by standalone usd
-		usdReg = IEEnv.registry["apps"]["usd"].get( pluginUsdVersion, {} ).get( platform )
-		if usdReg :
-			USD_INCLUDE_PATH = os.path.join( usdReg["location"], targetApp, compatibilityVersion, "include" )
-
-			mayaMajorVersion = mayaVersion.split(".")[0]
-			mayaLooseVersion = distutils.version.LooseVersion(mayaVersion)
-			if mayaLooseVersion >= "2022" and mayaLooseVersion < "2023":
-				# Maya 2022 installs the USD libs and the maya plugin itself for python 2 and 3. This is not the case for the 2020 version
-				# We make the assumption, that the python version suffix is for Maya 2022 only, because Maya 2023 will be python 3 exclusively.
-				mayaPythonMajorVersion = mayaReg["pythonVersion"].split(".")[0]
-				USD_LIB_PATH = os.path.join( mayaUsdReg["location"], mayaMajorVersion, "mayausd/USD{}/lib".format( mayaPythonMajorVersion ) )
-			else:
-				USD_LIB_PATH = os.path.join( mayaUsdReg["location"], mayaMajorVersion, "mayausd/USD/lib" )
-
-			# Pixar introduced a library prefix `usd_` in USD v21.11, which Autodesk does not use yet, so we have to reset the prefix.
-			# See https://github.com/Autodesk/maya-usd/issues/2108 for reference
-			USD_LIB_PREFIX = "" if mayaUsdReg.get( "usdLibPrefix" ) == "" else mayaUsdReg.get( "usdLibPrefix" ) or USD_LIB_PREFIX
+if targetApp == "maya":
+    mayaVersion = targetAppVersion
+
+    mayaReg = IEEnv.registry["apps"]["maya"][mayaVersion][platform]
+    MAYA_ROOT = mayaReg["location"]
+    INSTALL_MAYALIB_NAME = os.path.join(appPrefix, "lib", "$IECORE_NAME{}".format(ieCoreLibSuffix))
+    INSTALL_MEL_DIR = os.path.join(appPrefix, "mel", "$IECORE_NAME")
+    INSTALL_MAYAPLUGIN_NAME = os.path.join(appPrefix, "plugins", "$IECORE_NAME")
+    INSTALL_MAYAICON_DIR = os.path.join(appPrefix, "icons")
+    INSTALL_COREMAYA_POST_COMMAND = (
+        "ieEnvExec {workingPath} scons -i -f {optionsDir}/postCoreMayaInstall"
+        " MAYA_VERSION='{mayaVersion}' INSTALLPREFIX={appPrefix} install".format(
+            optionsDir=optionsDir,
+            workingPath=os.environ["IEENV_WORKING_PATH"],
+            mayaVersion=mayaVersion,
+            appPrefix=appPrefix,
+        )
+    )
+    WITH_MAYA_PLUGIN_LOADER = 1
+
+    mayaUsdVersion = mayaReg.get("mayaUsdVersion")
+    mayaUsdReg = IEEnv.registry["apps"]["mayaUsd"].get(mayaUsdVersion, {}).get(platform)
+    # If the Maya usd plugin is not in the registry we build against our standalone USD version
+    if mayaUsdReg:
+        pluginUsdVersion = mayaUsdReg["usdVersion"]
+        # Maya ships the USD libraries with the installation, but not the header files... we use the one that are installed by standalone usd
+        usdReg = IEEnv.registry["apps"]["usd"].get(pluginUsdVersion, {}).get(platform)
+        if usdReg:
+            USD_INCLUDE_PATH = os.path.join(
+                usdReg["location"], targetApp, compatibilityVersion, "include"
+            )
+
+            mayaMajorVersion = mayaVersion.split(".")[0]
+            mayaLooseVersion = distutils.version.LooseVersion(mayaVersion)
+            if mayaLooseVersion >= "2022" and mayaLooseVersion < "2023":
+                # Maya 2022 installs the USD libs and the maya plugin itself for python 2 and 3. This is not the case for the 2020 version
+                # We make the assumption, that the python version suffix is for Maya 2022 only, because Maya 2023 will be python 3 exclusively.
+                mayaPythonMajorVersion = mayaReg["pythonVersion"].split(".")[0]
+                USD_LIB_PATH = os.path.join(
+                    mayaUsdReg["location"],
+                    mayaMajorVersion,
+                    "mayausd/USD{}/lib".format(mayaPythonMajorVersion),
+                )
+            else:
+                USD_LIB_PATH = os.path.join(
+                    mayaUsdReg["location"], mayaMajorVersion, "mayausd/USD/lib"
+                )
+
+            # Pixar introduced a library prefix `usd_` in USD v21.11, which Autodesk does not use yet, so we have to reset the prefix.
+            # See https://github.com/Autodesk/maya-usd/issues/2108 for reference
+            USD_LIB_PREFIX = (
+                ""
+                if mayaUsdReg.get("usdLibPrefix") == ""
+                else mayaUsdReg.get("usdLibPrefix") or USD_LIB_PREFIX
+            )
 
 # find nuke if we're building for nuke
-if targetApp=="nuke" :
-
-	nukeVersion = targetAppVersion
-	nukeReg = IEEnv.registry["apps"]["nuke"][nukeVersion][platform]
-	NUKE_ROOT = nukeReg["location"]
-	NUKE_LICENSE_FILE = nukeReg["wrapperEnvVars"]["foundry_LICENSE"]
-	INSTALL_NUKELIB_NAME = os.path.join( appPrefix, "lib", "$IECORE_NAME-$IECORE_COMPATIBILITY_VERSION" )
-	INSTALL_NUKEPYTHON_DIR = os.path.join( appPrefix, "python" )
-	INSTALL_NUKEICON_DIR = os.path.join( appPrefix, "icons" )
-	INSTALL_NUKEPLUGIN_NAME = os.path.join( appPrefix, "plugins", "$IECORE_NAME" )
+if targetApp == "nuke":
+    nukeVersion = targetAppVersion
+    nukeReg = IEEnv.registry["apps"]["nuke"][nukeVersion][platform]
+    NUKE_ROOT = nukeReg["location"]
+    NUKE_LICENSE_FILE = nukeReg["wrapperEnvVars"]["foundry_LICENSE"]
+    USG_SHIMLIB_PATH = os.path.join(
+        "/software",
+        "apps",
+        "FnUsdShim",
+        nukeReg["compatibilityVersion"],
+        platform,
+        compiler,
+        compilerVersion,
+    )
+    INSTALL_NUKELIB_NAME = os.path.join(appPrefix, "lib", "$IECORE_NAME{}".format(ieCoreLibSuffix))
+    INSTALL_NUKEPYTHON_DIR = os.path.join(appPrefix, "python")
+    INSTALL_NUKEICON_DIR = os.path.join(appPrefix, "icons")
+    INSTALL_NUKEPLUGIN_NAME = os.path.join(appPrefix, "plugins", "$IECORE_NAME")
+    # Note:
+    # This is the recommended method for disabling the deprecation warnings for the legacy 3D
+    # system.
+    # From the foundry:
+    # ...it should be fine to continue using the classic 3D system, and these plugins, in Nuke 15.1
+    # and, while we might eventually remove the classic 3D system in the future, there are currently
+    # no plans to do this. So with that said. it should be safe to ignore the deprecation warnings
+    # in the meantime.
+    CXXFLAGS.append("-DNUKE_DISABLE_DEPRECATIONS")
 
 # find houdini if we're building for houdini
-if targetApp=="houdini" :
-
-	houdiniVersion = targetAppVersion
-
-	houdiniReg = IEEnv.registry["apps"]["houdini"][houdiniVersion][platform]
-	HOUDINI_ROOT = houdiniReg['location']
-
-	# we use out normal boost includes (and libs) because Houdini has mangled their
-	# boost install with an "h" prefix on the includes, libs, and even macros like HBOOST_VERSION.
-	BOOST_INCLUDE_PATH = "/software/tools/include/" + platform + "/boost/" + boostVersion
-
-	# houdini 17 ships its own USD so we link against that
-	if distutils.version.LooseVersion(houdiniVersion) >= distutils.version.LooseVersion("17.0") :
-		TBB_INCLUDE_PATH = "$HOUDINI_INCLUDE_PATH"
-		TBB_LIB_PATH = "$HOUDINI_LIB_PATH"
-		TBB_LIB_SUFFIX = ""
-		GLEW_LIB_SUFFIX = ""
-		VDB_LIB_SUFFIX = "_sesi"
-		USD_INCLUDE_PATH = "$HOUDINI_INCLUDE_PATH"
-		USD_LIB_PATH = "$HOUDINI_LIB_PATH"
-		USD_LIB_PREFIX = "libpxr_"
-		WITH_USD_MONOLITHIC = True
-	if distutils.version.LooseVersion(houdiniVersion) >= distutils.version.LooseVersion("18.0") :
-		# SideFx is non building USD monolithically anymore
-		WITH_USD_MONOLITHIC = False
-		# we stop using SideFx namespaced library as much as possible.
-		VDB_LIB_SUFFIX = ""
-
-	HOUDINI_CXX_FLAGS = " ".join( houdiniReg['compilerFlags'] )
-
-	INSTALL_HOUDINILIB_NAME = os.path.join( appPrefix, "lib", "$IECORE_NAME-$IECORE_COMPATIBILITY_VERSION" )
-	INSTALL_HOUDINIPLUGIN_NAME = os.path.join( appPrefix, "plugins", "$IECORE_NAME" )
-	INSTALL_HOUDINIOTL_DIR = os.path.join( appPrefix, "otls" )
-	INSTALL_HOUDINIICON_DIR = os.path.join( appPrefix, "icons" )
-	INSTALL_HOUDINITOOLBAR_DIR = os.path.join( appPrefix, "toolbar" )
-	INSTALL_HOUDINIMENU_DIR = os.path.join( appPrefix, "generic" )
-
-	WITH_MANTRA = True
-	INSTALL_MANTRALIB_NAME = os.path.join( appPrefix, "lib", "$IECORE_NAME-$IECORE_COMPATIBILITY_VERSION" )
-	INSTALL_MANTRAPROCEDURAL_NAME = os.path.join( appPrefix, "plugins", "mantra", "$IECORE_NAME" )
-
-	# Temporarily disable IECoreGL until we sort out our dependency trauma
-	#	Houdini 16.0 requires Boost 1.55.0
-	#	IECoreGL requires IECoreImage requires OpenImageIO 1.8.4dev requires Boost 1.61.0
-	# We need to sort this out before Cortex 10 is officially released,
-	# but chances are by that points Houdini 16.5 will be released.
-	if distutils.version.LooseVersion(houdiniVersion) >= distutils.version.LooseVersion("16.5"):
-		WITH_GL = 1
-	else:
-		WITH_GL = False
-
-if targetApp=="rv" :
-
-	rvVersion = targetAppVersion
-	rvReg = IEEnv.registry["apps"]["rv"][rvVersion][platform]
-	rvRoot = rvReg["location"]
-	rvIncludes = os.path.join( rvRoot, "include" )
-	rvLibs = os.path.join( rvRoot, "lib" )
-
-	WITH_GL = False
-
-	if distutils.version.LooseVersion(rvVersion) >= distutils.version.LooseVersion("7.8.0"):
-		if "boostVersion" in rvReg:
-			BOOST_INCLUDE_PATH = rvIncludes
-			BOOST_LIB_PATH = rvLibs
-			BOOST_LIB_SUFFIX = rvReg.get( "boostLibSuffix", BOOST_LIB_SUFFIX )
-
-		if "OpenImageIOVersion" in rvReg:
-			# NOTE: At the moment RV doesn't provide OIIO headers, so we rely on
-			# the default `OIIO_INCLUDE_PATH` value.
-			OIIO_LIB_PATH = rvLibs
-			OIIO_LIB_SUFFIX = rvReg.get( "OpenImageIOLibSuffix", OIIO_LIB_SUFFIX )
-			# current version of OIIO used by RV doesn't include the Util library
-			# this variable will tell the build process to not require it
-			WITH_OIIO_UTIL = rvReg.get( "WithOpenImageIOUtil", WITH_OIIO_UTIL )
+if targetApp == "houdini":
+    houdiniVersion = targetAppVersion
+
+    houdiniReg = IEEnv.registry["apps"]["houdini"][houdiniVersion][platform]
+    HOUDINI_ROOT = houdiniReg["location"]
+
+    # houdini 17 ships its own USD so we link against that
+    if distutils.version.LooseVersion(houdiniVersion) >= distutils.version.LooseVersion("17.0"):
+        TBB_INCLUDE_PATH = "$HOUDINI_INCLUDE_PATH"
+        TBB_LIB_PATH = "$HOUDINI_LIB_PATH"
+        TBB_LIB_SUFFIX = ""
+        GLEW_LIB_SUFFIX = ""
+        VDB_LIB_SUFFIX = "_sesi"
+        USD_INCLUDE_PATH = "$HOUDINI_INCLUDE_PATH"
+        USD_LIB_PATH = "$HOUDINI_LIB_PATH"
+        USD_LIB_PREFIX = "libpxr_"
+        WITH_USD_MONOLITHIC = True
+    if distutils.version.LooseVersion(houdiniVersion) >= distutils.version.LooseVersion("18.0"):
+        # SideFx is non building USD monolithically anymore
+        WITH_USD_MONOLITHIC = False
+        # we stop using SideFx namespaced library as much as possible.
+        VDB_LIB_SUFFIX = ""
+
+    HOUDINI_CXX_FLAGS = " ".join(houdiniReg["compilerFlags"])
+
+    INSTALL_HOUDINILIB_NAME = os.path.join(
+        appPrefix, "lib", "$IECORE_NAME{}".format(ieCoreLibSuffix)
+    )
+    INSTALL_HOUDINIPLUGIN_NAME = os.path.join(appPrefix, "plugins", "$IECORE_NAME")
+    INSTALL_HOUDINIOTL_DIR = os.path.join(appPrefix, "otls")
+    INSTALL_HOUDINIICON_DIR = os.path.join(appPrefix, "icons")
+    INSTALL_HOUDINITOOLBAR_DIR = os.path.join(appPrefix, "toolbar")
+    INSTALL_HOUDINIMENU_DIR = os.path.join(appPrefix, "generic")
+
+    WITH_MANTRA = True
+    INSTALL_MANTRALIB_NAME = os.path.join(
+        appPrefix, "lib", "$IECORE_NAME{}".format(ieCoreLibSuffix)
+    )
+    INSTALL_MANTRAPROCEDURAL_NAME = os.path.join(appPrefix, "plugins", "mantra", "$IECORE_NAME")
+
+    # Temporarily disable IECoreGL until we sort out our dependency trauma
+    #    Houdini 16.0 requires Boost 1.55.0
+    #    IECoreGL requires IECoreImage requires OpenImageIO 1.8.4dev requires Boost 1.61.0
+    # We need to sort this out before Cortex 10 is officially released,
+    # but chances are by that points Houdini 16.5 will be released.
+    if distutils.version.LooseVersion(houdiniVersion) >= distutils.version.LooseVersion("16.5"):
+        WITH_GL = 1
+    else:
+        WITH_GL = False
+
+if targetApp == "rv":
+    # DEPRECATED: We no longer need to build cortex for rv
+    rvVersion = targetAppVersion
+    rvReg = IEEnv.registry["apps"]["rv"][rvVersion][platform]
+    rvRoot = rvReg["location"]
+    rvIncludes = os.path.join(rvRoot, "include")
+    rvLibs = os.path.join(rvRoot, "lib")
+
+    WITH_GL = False
+
+    if distutils.version.LooseVersion(rvVersion) >= distutils.version.LooseVersion("7.8.0"):
+        if "boostVersion" in rvReg:
+            BOOST_INCLUDE_PATH = rvIncludes
+            BOOST_LIB_PATH = rvLibs
+            BOOST_LIB_SUFFIX = rvReg.get("boostLibSuffix", BOOST_LIB_SUFFIX)
+
+        if "OpenImageIOVersion" in rvReg:
+            # NOTE: At the moment RV doesn't provide OIIO headers, so we rely on
+            # the default `OIIO_INCLUDE_PATH` value.
+            OIIO_LIB_PATH = rvLibs
+            OIIO_LIB_SUFFIX = rvReg.get("OpenImageIOLibSuffix", OIIO_LIB_SUFFIX)
+            # current version of OIIO used by RV doesn't include the Util library
+            # this variable will tell the build process to not require it
+            WITH_OIIO_UTIL = rvReg.get("WithOpenImageIOUtil", WITH_OIIO_UTIL)
 
 # find doxygen
-DOXYGEN = os.path.join( "/software/apps/doxygen", os.environ["DOXYGEN_VERSION"], platform, "bin", "doxygen" )
+DOXYGEN = os.path.join(
+    "/software/apps/doxygen", os.environ["DOXYGEN_VERSION"], platform, "bin", "doxygen"
+)
 
 # import vars we need to get our doxygen and python wrappers working
 envVarsToImport = [
-	"PATH",
-	"PYTHONPATH",
-	"IEENV_ROOT",
-	"IEENV_WORKING_PATH",
-	"IEENV_LIBRARY_PREFIX_PATH",
-	"DOXYGEN_VERSION",
-	"IEENV_DEBUG",
-	"IEENV_DEBUG_PYTHON",
-	"IEENV_DEBUGGER",
-	"IEENV_DEBUGGER_ARGS",
-	"DELIGHT_CONF",
-	"SCONS_VERSION",
-	"DL_VERSION",
-	"DL_SHADERS_PATH",
-	"DL_DISPLAYS_PATH",
-	"solidangle_LICENSE",
-	"CORTEX_POINTDISTRIBUTION_TILESET",
-	"OCIO",
-	"IECORE_DEBUG_WAIT",
-	"CORTEX_PERFORMANCE_TEST",
-	"IECORE_RTLD_GLOBAL",
+    "PATH",
+    "PYTHONPATH",
+    "IEENV_ROOT",
+    "IEENV_WORKING_PATH",
+    "IEENV_LIBRARY_PREFIX_PATH",
+    "DOXYGEN_VERSION",
+    "IEENV_DEBUG",
+    "IEENV_DEBUG_PYTHON",
+    "IEENV_DEBUGGER",
+    "IEENV_DEBUGGER_ARGS",
+    "DELIGHT_CONF",
+    "SCONS_VERSION",
+    "DL_VERSION",
+    "DL_SHADERS_PATH",
+    "DL_DISPLAYS_PATH",
+    "solidangle_LICENSE",
+    "CORTEX_POINTDISTRIBUTION_TILESET",
+    "OCIO",
+    "IECORE_DEBUG_WAIT",
+    "CORTEX_PERFORMANCE_TEST",
+    "IECORE_RTLD_GLOBAL",
 ]
 
-ENV_VARS_TO_IMPORT= " ".join(envVarsToImport)
+ENV_VARS_TO_IMPORT = " ".join(envVarsToImport)
 
 # make sure the tests can run
 testLibs = [
-	openEXRPythonLibDir,
-	os.path.join( pythonReg["location"], compiler, compilerVersion, "lib" ),
-	os.path.join( compilerReg["location"], "lib" ),
+    openEXRPythonLibDir,
+    os.path.join(pythonReg["location"], compiler, compilerVersion, "lib"),
+    os.path.join(compilerReg["location"], "lib"),
 ]
 
-TEST_LIBPATH = ":".join( testLibs )
+TEST_LIBPATH = ":".join(testLibs)
 TEST_LIBRARY_PATH_ENV_VAR = "IEENV_LIBRARY_PREFIX_PATH"
 
 # install the op stubs and procedural stubs
-INSTALL_IECORE_PROCEDURAL_PATH = os.path.join( basePrefix, "procedurals", "$IECORE_NAME-${IECORE_COMPATIBILITY_VERSION}.py" )
+INSTALL_IECORE_PROCEDURAL_PATH = os.path.join(
+    basePrefix, "procedurals", "$IECORE_NAME-${IECORE_COMPATIBILITY_VERSION}.py"
+)
 # ClassLoader requires integer versioning in the op filenames, so we use milestone version over compatibility version
-INSTALL_IECORE_OP_PATH = os.path.join( basePrefix, "ops", "$IECORE_NAME-${IECORE_MILESTONE_VERSION}.py" )
-
-if targetApp :
-	INSTALL_HEADER_DIR = os.path.join( appPrefix, "include" )
-	INSTALL_USD_RESOURCE_DIR = os.path.join( appPrefix, "resource" )
-	INSTALL_LIB_NAME = os.path.join( appPrefix, "lib", "$IECORE_NAME-$IECORE_COMPATIBILITY_VERSION" )
-	INSTALL_PYTHONLIB_NAME = os.path.join( appPrefix, "lib", "$IECORE_NAME-$IECORE_COMPATIBILITY_VERSION-python$PYTHON_VERSION" )
-	INSTALL_PYTHON_DIR = os.path.join( appPrefix, "python" )
-	INSTALL_ALEMBICLIB_NAME = os.path.join( appPrefix, "lib", "$IECORE_NAME-$IECORE_COMPATIBILITY_VERSION" )
-	INSTALL_USDLIB_NAME = os.path.join( appPrefix, "lib", "$IECORE_NAME-$IECORE_COMPATIBILITY_VERSION" )
-else :
-	INSTALL_HEADER_DIR = os.path.join( basePrefix, "include" )
-	INSTALL_USD_RESOURCE_DIR = os.path.join( basePrefix, "resource" )
-	INSTALL_LIB_NAME = os.path.join( basePrefix, "lib", compiler, compilerVersion, "$IECORE_NAME-$IECORE_COMPATIBILITY_VERSION" )
-	INSTALL_PYTHONLIB_NAME = os.path.join( basePrefix, "lib", compiler, compilerVersion, "$IECORE_NAME-$IECORE_COMPATIBILITY_VERSION-python$PYTHON_VERSION" )
-	INSTALL_PYTHON_DIR = os.path.join( basePrefix, "python", pythonVersion, compiler, compilerVersion )
-	INSTALL_ALEMBICLIB_NAME = os.path.join( basePrefix, "lib", compiler, compilerVersion, "$IECORE_NAME-$IECORE_COMPATIBILITY_VERSION" )
-	INSTALL_USDLIB_NAME = os.path.join( basePrefix, "lib", compiler, compilerVersion, "$IECORE_NAME-$IECORE_COMPATIBILITY_VERSION" )
-
-	# only installing for the base installation as the CORTEX_STARTUP_PATHS will load these within target apps as well
-	INSTALL_CORESCENE_POST_COMMAND = "ieEnvExec {workingPath} scons -i -f config/ie/postCoreSceneInstall INSTALLPREFIX={prefix} install".format(
-		workingPath = os.environ["IEENV_WORKING_PATH"], prefix = basePrefix
-	)
-	INSTALL_COREIMAGE_POST_COMMAND = "ieEnvExec {workingPath} scons -i -f config/ie/postCoreImageInstall INSTALLPREFIX={prefix} install".format(
-		workingPath = os.environ["IEENV_WORKING_PATH"], prefix = basePrefix
-	)
-
-INSTALL_GLSL_HEADER_DIR =  os.path.join( basePrefix, "glsl" )
-INSTALL_GLSL_SHADER_DIR =  os.path.join( basePrefix, "glsl" )
-INSTALL_RSL_HEADER_DIR =  os.path.join( basePrefix, "rsl" )
-INSTALL_DOC_DIR = os.path.join( installPrefix, "doc" )
+INSTALL_IECORE_OP_PATH = os.path.join(
+    basePrefix, "ops", "$IECORE_NAME-${IECORE_MILESTONE_VERSION}.py"
+)
+
+if targetApp:
+    INSTALL_HEADER_DIR = os.path.join(appPrefix, "include")
+    INSTALL_USD_RESOURCE_DIR = os.path.join(appPrefix, "resource")
+    INSTALL_LIB_NAME = os.path.join(appPrefix, "lib", "$IECORE_NAME{}".format(ieCoreLibSuffix))
+    INSTALL_PYTHONLIB_NAME = os.path.join(
+        appPrefix, "lib", "$IECORE_NAME{}{}".format(ieCoreLibSuffix, ieCorePythonLibSuffix)
+    )
+    INSTALL_PYTHON_DIR = os.path.join(appPrefix, "python")
+    INSTALL_ALEMBICLIB_NAME = os.path.join(
+        appPrefix, "lib", "$IECORE_NAME{}".format(ieCoreLibSuffix)
+    )
+    INSTALL_USDLIB_NAME = os.path.join(appPrefix, "lib", "$IECORE_NAME{}".format(ieCoreLibSuffix))
+else:
+    INSTALL_HEADER_DIR = os.path.join(basePrefix, "include")
+    INSTALL_USD_RESOURCE_DIR = os.path.join(basePrefix, "resource")
+    INSTALL_LIB_NAME = os.path.join(
+        basePrefix, "lib", compiler, compilerVersion, "$IECORE_NAME{}".format(ieCoreLibSuffix)
+    )
+    INSTALL_PYTHONLIB_NAME = os.path.join(
+        basePrefix,
+        "lib",
+        compiler,
+        compilerVersion,
+        "$IECORE_NAME{}{}".format(ieCoreLibSuffix, ieCorePythonLibSuffix),
+    )
+    INSTALL_PYTHON_DIR = os.path.join(
+        basePrefix, "python", pythonVersion, compiler, compilerVersion
+    )
+    INSTALL_ALEMBICLIB_NAME = os.path.join(
+        basePrefix, "lib", compiler, compilerVersion, "$IECORE_NAME{}".format(ieCoreLibSuffix)
+    )
+    INSTALL_USDLIB_NAME = os.path.join(
+        basePrefix, "lib", compiler, compilerVersion, "$IECORE_NAME{}".format(ieCoreLibSuffix)
+    )
+
+    # only installing for the base installation as the CORTEX_STARTUP_PATHS will load these within target apps as well
+    INSTALL_CORESCENE_POST_COMMAND = (
+        "ieEnvExec {workingPath} scons -i -f {optionsDir}/postCoreSceneInstall"
+        " INSTALLPREFIX={prefix} install".format(
+            optionsDir=optionsDir, workingPath=os.environ["IEENV_WORKING_PATH"], prefix=basePrefix
+        )
+    )
+    INSTALL_COREIMAGE_POST_COMMAND = (
+        "ieEnvExec {workingPath} scons -i -f {optionsDir}/postCoreImageInstall"
+        " INSTALLPREFIX={prefix} install".format(
+            optionsDir=optionsDir, workingPath=os.environ["IEENV_WORKING_PATH"], prefix=basePrefix
+        )
+    )
+
+INSTALL_GLSL_HEADER_DIR = os.path.join(basePrefix, "glsl")
+INSTALL_GLSL_SHADER_DIR = os.path.join(basePrefix, "glsl")
+INSTALL_RSL_HEADER_DIR = os.path.join(basePrefix, "rsl")
+INSTALL_DOC_DIR = os.path.join(installPrefix, "doc")
 INSTALL_PKG_CONFIG_FILE = "0"
 
 # speed up the build a bit hopefully.
@@ -494,5 +711,5 @@ BUILD_CACHEDIR = os.environ["IEBUILD_CACHEDIR"]
 # build build will embed full paths to source code paths in the dwarf information
 # Disabling the build cache to ensure our debug builds are correct rather than fast.
 
-if getOption( "DEBUG", False ) and os.path.exists("/disk1") :
-	BUILD_CACHEDIR = '/disk1/{0}/scons-cache'.format( getpass.getuser() )
+if getOption("DEBUG", False) and os.path.exists("/disk1"):
+    BUILD_CACHEDIR = "/disk1/{0}/scons-cache".format(getpass.getuser())
diff --git a/contrib/IECoreUSD/include/IECoreUSD/DataAlgo.h b/contrib/IECoreUSD/include/IECoreUSD/DataAlgo.h
index 9f8d172188..1e6c8bd611 100644
--- a/contrib/IECoreUSD/include/IECoreUSD/DataAlgo.h
+++ b/contrib/IECoreUSD/include/IECoreUSD/DataAlgo.h
@@ -66,13 +66,12 @@ typename USDTypeTraits<T>::CortexType fromUSD( const T &value );
 template<typename T>
 boost::intrusive_ptr< typename USDTypeTraits<T>::CortexVectorDataType > fromUSD( const pxr::VtArray<T> &array );
 
-/// Converts USD `value` to Cortex Data, applying any additional
-/// geometric interpretation implied by `valueTypeName`. If
-/// `arrayAccepted` is false, then converts single element arrays
-/// to simple data and emits a warning and returns nullptr for
-/// all other arrays. Returns nullptr if no appropriate conversion
-/// exists.
-IECOREUSD_API IECore::DataPtr fromUSD( const pxr::VtValue &value, const pxr::SdfValueTypeName &valueTypeName, bool arrayAccepted = true );
+/// Converts USD `value` to Cortex Data, applying any additional geometric
+/// interpretation implied by `valueTypeName` if it is provided. If
+/// `arrayAccepted` is false, then converts single element arrays to simple data
+/// and emits a warning and returns nullptr for all other arrays. Returns
+/// nullptr if no appropriate conversion exists.
+IECOREUSD_API IECore::DataPtr fromUSD( const pxr::VtValue &value, const pxr::SdfValueTypeName &valueTypeName = pxr::SdfValueTypeName(), bool arrayAccepted = true );
 
 /// Converts the value of `attribute` at the specified time, using the attribute's
 /// type name to apply geometric interpretation. The meaning of `arrayAccepted` is
diff --git a/contrib/IECoreUSD/resources/plugInfo.json b/contrib/IECoreUSD/resources/plugInfo.json
index fc186e3f82..ae59cfc19d 100644
--- a/contrib/IECoreUSD/resources/plugInfo.json
+++ b/contrib/IECoreUSD/resources/plugInfo.json
@@ -17,6 +17,10 @@
 						"target": "usd"
 					}
 				},
+				## \todo This metadata causes problems because other DCCs are
+				# unlikely to have our `plugInfo.json` available. Replace it
+				# with custom data instead (`UsdObject::SetCustomData()`), since
+				# that doesn't require central registration.
 				"SdfMetadata": {
 					"cortex_isConstantPrimitiveVariable": {
 						"type": "bool",
@@ -37,9 +41,7 @@
 						"type": "bool",
 						"appliesTo": "prims"
 					},
-					# Label a shader that was created automatically to hold
-					# a connection that USD can't support directly - if we
-					# want to round trip, we have to remove these on input
+					# Legacy metadata once used by ShaderAlgo.
 					"cortex_autoAdapter": {
 						"type": "bool",
 						"appliesTo": "prims"
diff --git a/contrib/IECoreUSD/src/IECoreUSD/DataAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/DataAlgo.cpp
index 6b4eca217e..0703d7b799 100644
--- a/contrib/IECoreUSD/src/IECoreUSD/DataAlgo.cpp
+++ b/contrib/IECoreUSD/src/IECoreUSD/DataAlgo.cpp
@@ -34,6 +34,7 @@
 
 #include "IECoreUSD/DataAlgo.h"
 
+#include "IECore/CompoundData.h"
 #include "IECore/DataAlgo.h"
 #include "IECore/MessageHandler.h"
 
@@ -49,6 +50,10 @@ IECORE_POP_DEFAULT_VISIBILITY
 
 #include "boost/unordered_map.hpp"
 
+#ifdef _MSC_VER
+#include <filesystem>
+#endif
+
 using namespace std;
 using namespace pxr;
 using namespace IECore;
@@ -92,6 +97,19 @@ GeometricData::Interpretation interpretation( TfToken role )
 	return GeometricData::None;
 }
 
+#ifdef _MSC_VER
+
+static const bool g_forceAssetPathForwardSlash = []() -> bool {
+	const char *c = getenv( "IECOREUSD_FORCE_ASSET_PATH_FORWARD_SLASH" );
+	if( !c )
+	{
+		return true;
+	}
+	return strcmp( c, "0" );
+}();
+
+#endif
+
 } // namespace
 
 
@@ -142,9 +160,16 @@ IECore::DataPtr dataFromArray( const pxr::VtValue &value, GeometricData::Interpr
 
 IECore::DataPtr dataFromSdfAssetPath( const SdfAssetPath &assetPath, const pxr::UsdAttribute *attribute = nullptr )
 {
-	if( assetPath.GetResolvedPath().size() || !assetPath.GetAssetPath().size() || !attribute )
+
+#ifdef _MSC_VER
+	const std::string p = g_forceAssetPathForwardSlash ? std::filesystem::path( assetPath.GetResolvedPath() ).generic_string() : assetPath.GetResolvedPath();
+#else
+	const std::string p = assetPath.GetResolvedPath();
+#endif
+
+	if( p.size() || !assetPath.GetAssetPath().size() || !attribute )
 	{
-		return new StringData( assetPath.GetResolvedPath() );
+		return new StringData( p );
 	}
 
 	// Path resolution failed, for a couple of possible reasons :
@@ -168,9 +193,16 @@ IECore::DataPtr dataFromSdfAssetPath( const SdfAssetPath &assetPath, const pxr::
 			spec->GetLayer()->GetNumTimeSamplesForPath( spec->GetPath() )
 		)
 		{
+#ifdef _MSC_VER
+			const std::string result = SdfComputeAssetPathRelativeToLayer( spec->GetLayer(), assetPath.GetAssetPath() );
+			return new StringData(
+				g_forceAssetPathForwardSlash ? std::filesystem::path( result ).generic_string() : result
+			);
+#else
 			return new StringData(
 				SdfComputeAssetPathRelativeToLayer( spec->GetLayer(), assetPath.GetAssetPath() )
 			);
+#endif
 		}
 	}
 
@@ -182,6 +214,19 @@ IECore::DataPtr dataFromSdfAssetPath( const pxr::VtValue &value, GeometricData::
 	return dataFromSdfAssetPath( value.UncheckedGet<SdfAssetPath>() );
 }
 
+IECore::DataPtr dataFromDictionary( const pxr::VtValue &value, GeometricData::Interpretation interpretation, bool arrayAccepted )
+{
+	CompoundDataPtr result = new CompoundData;
+	for( const auto &[name, v] : value.Get<VtDictionary>() )
+	{
+		if( IECore::DataPtr d = IECoreUSD::DataAlgo::fromUSD( v, pxr::SdfValueTypeName() ) )
+		{
+			result->writable()[name] = d;
+		}
+	}
+	return result;
+}
+
 static const std::map<pxr::TfType, IECore::DataPtr (*)( const pxr::VtValue &, GeometricData::Interpretation, bool )> g_fromVtValueConverters = {
 
 	// Numeric types
@@ -248,7 +293,11 @@ static const std::map<pxr::TfType, IECore::DataPtr (*)( const pxr::VtValue &, Ge
 	{ TfType::Find<VtArray<string>>(), &dataFromArray<string> },
 	{ TfType::Find<TfToken>(), &dataFromValue<TfToken> },
 	{ TfType::Find<VtArray<TfToken>>(), &dataFromArray<TfToken> },
-	{ TfType::Find<SdfAssetPath>(), &dataFromSdfAssetPath }
+	{ TfType::Find<SdfAssetPath>(), &dataFromSdfAssetPath },
+
+	// Dictionary
+
+	{ TfType::Find<VtDictionary>(), &dataFromDictionary }
 
 };
 
@@ -292,12 +341,24 @@ static const std::map<pxr::TfType, std::function<IECore::DataPtr ( const pxr::Vt
 
 IECore::DataPtr IECoreUSD::DataAlgo::fromUSD( const pxr::VtValue &value, const pxr::SdfValueTypeName &valueTypeName, bool arrayAccepted )
 {
-	const GeometricData::Interpretation i = interpretation( valueTypeName.GetRole() );
+	GeometricData::Interpretation i;
+	TfType type;
+	if( valueTypeName )
+	{
+		i = interpretation( valueTypeName.GetRole() );
+		type = valueTypeName.GetType();
+	}
+	else
+	{
+		i = GeometricData::Interpretation::None;
+		type = value.GetType();
+	}
+
 	if( i == GeometricData::Color )
 	{
 		// Colors can not be identified by TfType because they borrow GfVec3,
 		// so they require their own dispatch table.
-		const auto it = g_fromVtValueColorConverters.find( valueTypeName.GetType() );
+		const auto it = g_fromVtValueColorConverters.find( type );
 		if( it == g_fromVtValueColorConverters.end() )
 		{
 			return nullptr;
@@ -305,7 +366,7 @@ IECore::DataPtr IECoreUSD::DataAlgo::fromUSD( const pxr::VtValue &value, const p
 		return it->second( value, arrayAccepted );
 	}
 
-	const auto it = g_fromVtValueConverters.find( valueTypeName.GetType() );
+	const auto it = g_fromVtValueConverters.find( type );
 	if( it == g_fromVtValueConverters.end() )
 	{
 		return nullptr;
@@ -389,6 +450,26 @@ struct VtValueFromData
 		return VtValue( DataAlgo::toUSD( data->readable() ) );
 	}
 
+	VtValue operator()( const IECore::CompoundData *data, bool arrayRequired )
+	{
+		if( arrayRequired )
+		{
+			return VtValue();
+		}
+
+		VtDictionary result;
+		for( const auto &[name, value] : data->readable() )
+		{
+			VtValue v = DataAlgo::toUSD( value.get() );
+			if( !v.IsEmpty() )
+			{
+				result[name] = v;
+			}
+		}
+
+		return VtValue( result );
+	}
+
 	VtValue operator()( const IECore::Data *data, bool arrayRequired ) const
 	{
 		return VtValue();
@@ -402,7 +483,13 @@ pxr::VtValue IECoreUSD::DataAlgo::toUSD( const IECore::Data *data, bool arrayReq
 {
 	try
 	{
-		return IECore::dispatch( data, VtValueFromData(), arrayRequired );
+		VtValueFromData valueFromData;
+		if( auto cd = runTimeCast<const CompoundData>( data ) )
+		{
+			// Manual dispatch since CompoundData not handled by `dispatch()`.
+			return valueFromData( cd, arrayRequired );
+		}
+		return IECore::dispatch( data, valueFromData, arrayRequired );
 	}
 	catch( const IECore::Exception & )
 	{
diff --git a/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp
index ea2096e94f..de9f01c88b 100644
--- a/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp
+++ b/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp
@@ -53,6 +53,19 @@ using namespace IECoreUSD;
 namespace
 {
 
+bool checkEnvFlag( const char *envVar, bool def )
+{
+	const char *value = getenv( envVar );
+	if( value )
+	{
+		return std::string( value ) != "0";
+	}
+	else
+	{
+		return def;
+	}
+}
+
 IECore::ObjectPtr readPointInstancer( pxr::UsdGeomPointInstancer &pointInstancer, pxr::UsdTimeCode time, const Canceller *canceller )
 {
 	pxr::VtVec3fArray pointsData;
@@ -86,18 +99,53 @@ IECore::ObjectPtr readPointInstancer( pxr::UsdGeomPointInstancer &pointInstancer
 	Canceller::check( canceller );
 	PrimitiveAlgo::readPrimitiveVariable( pointInstancer.GetAngularVelocitiesAttr(), time, newPoints.get(), "angularVelocity" );
 
+	if( pointInstancer.GetInvisibleIdsAttr().HasAuthoredValue() )
+	{
+		DataPtr cortexInvisIds = DataAlgo::fromUSD( pointInstancer.GetInvisibleIdsAttr(), time, true );
+		if( cortexInvisIds )
+		{
+			newPoints->variables["invisibleIds"] = IECoreScene::PrimitiveVariable(
+				PrimitiveVariable::Constant, cortexInvisIds
+			);
+		}
+	}
+
+	pxr::SdfInt64ListOp inactiveIdsListOp;
+	if( pointInstancer.GetPrim().GetMetadata( pxr::UsdGeomTokens->inactiveIds, &inactiveIdsListOp ) )
+	{
+		newPoints->variables["inactiveIds"] = IECoreScene::PrimitiveVariable(
+			PrimitiveVariable::Constant,
+			new IECore::Int64VectorData( inactiveIdsListOp.GetExplicitItems() )
+		);
+	}
+
 	// Prototype paths
 
+	const static bool g_relativePrototypes = checkEnvFlag( "IECOREUSD_POINTINSTANCER_RELATIVE_PROTOTYPES", false );
+
 	pxr::SdfPathVector targets;
 	Canceller::check( canceller );
 	pointInstancer.GetPrototypesRel().GetForwardedTargets( &targets );
 
+	const pxr::SdfPath &primPath = pointInstancer.GetPath();
+
 	IECore::StringVectorDataPtr prototypeRootsData = new IECore::StringVectorData();
 	auto &prototypeRoots = prototypeRootsData->writable();
 	prototypeRoots.reserve( targets.size() );
 	for( const auto &t : targets )
 	{
-		prototypeRoots.push_back( t.GetString() );
+		if( !g_relativePrototypes || !t.HasPrefix( primPath ) )
+		{
+			prototypeRoots.push_back( t.GetString() );
+		}
+		else
+		{
+			// The ./ prefix shouldn't be necessary - we want to just use the absence of a leading
+			// slash to indicate relative paths. We can remove the prefix here once we deprecate the
+			// GAFFERSCENE_INSTANCER_EXPLICIT_ABSOLUTE_PATHS env var and have Gaffer always require a leading
+			// slash for absolute paths.
+			prototypeRoots.push_back( "./" + t.MakeRelativePath( primPath ).GetString() );
+		}
 	}
 
 	newPoints->variables["prototypeRoots"] = IECoreScene::PrimitiveVariable( IECoreScene::PrimitiveVariable::Constant, prototypeRootsData );
@@ -120,6 +168,7 @@ bool pointInstancerMightBeTimeVarying( pxr::UsdGeomPointInstancer &instancer )
 		instancer.GetVelocitiesAttr().ValueMightBeTimeVarying() ||
 		instancer.GetAccelerationsAttr().ValueMightBeTimeVarying() ||
 		instancer.GetAngularVelocitiesAttr().ValueMightBeTimeVarying() ||
+		instancer.GetInvisibleIdsAttr().ValueMightBeTimeVarying() ||
 		PrimitiveAlgo::primitiveVariablesMightBeTimeVarying(
 			pxr::UsdGeomPrimvarsAPI( instancer )
 		)
diff --git a/contrib/IECoreUSD/src/IECoreUSD/PrimitiveAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/PrimitiveAlgo.cpp
index 9add88c8eb..c142d882b7 100644
--- a/contrib/IECoreUSD/src/IECoreUSD/PrimitiveAlgo.cpp
+++ b/contrib/IECoreUSD/src/IECoreUSD/PrimitiveAlgo.cpp
@@ -45,6 +45,7 @@ IECORE_PUSH_DEFAULT_VISIBILITY
 #include "pxr/base/gf/matrix3d.h"
 #include "pxr/base/gf/matrix4f.h"
 #include "pxr/base/gf/matrix4d.h"
+#include "pxr/usd/usdGeom/mesh.h"
 #include "pxr/usd/usdSkel/animQuery.h"
 #include "pxr/usd/usdSkel/bindingAPI.h"
 #include "pxr/usd/usdSkel/blendShapeQuery.h"
@@ -52,6 +53,7 @@ IECORE_PUSH_DEFAULT_VISIBILITY
 #include "pxr/usd/usdSkel/skeletonQuery.h"
 #include "pxr/usd/usdSkel/skinningQuery.h"
 #include "pxr/usd/usdSkel/root.h"
+#include "pxr/usd/usdSkel/utils.h"
 IECORE_POP_DEFAULT_VISIBILITY
 
 using namespace std;
@@ -317,6 +319,53 @@ void applyBlendShapes( const pxr::UsdGeomPointBased &pointBased, pxr::UsdTimeCod
 	);
 }
 
+bool computeFaceVaryingSkinnedNormals( pxr::UsdSkelSkinningQuery &skinningQuery, const pxr::VtArray<pxr::GfMatrix4d> &xforms, pxr::VtVec3fArray *normals, pxr::UsdTimeCode time, const Canceller *canceller )
+{
+	const pxr::UsdGeomMesh mesh( skinningQuery.GetPrim() );
+	if( !mesh )
+	{
+		return false;
+	}
+
+	Canceller::check( canceller );
+	pxr::VtIntArray faceVertexIndices;
+	mesh.GetFaceVertexIndicesAttr().Get( &faceVertexIndices, time );
+
+	Canceller::check( canceller );
+	pxr::VtIntArray jointIndices;
+	pxr::VtFloatArray jointWeights;
+	if( !skinningQuery.ComputeJointInfluences( &jointIndices, &jointWeights, time ) )
+	{
+		return false;
+	}
+
+	Canceller::check( canceller );
+	pxr::VtArray<pxr::GfMatrix4d> orderedXforms = xforms;
+	if( auto jointMapper = skinningQuery.GetJointMapper() )
+	{
+		if( !jointMapper->RemapTransforms( xforms, &orderedXforms ) )
+		{
+			return false;
+		}
+	}
+
+	Canceller::check( canceller );
+	pxr::VtArray<pxr::GfMatrix3d> invTransposeXforms( orderedXforms.size() );
+	for( size_t i = 0; i < orderedXforms.size(); ++i )
+	{
+		invTransposeXforms[i] = orderedXforms[i].ExtractRotationMatrix().GetInverse().GetTranspose();
+	}
+
+	Canceller::check( canceller );
+	return pxr::UsdSkelSkinFaceVaryingNormals(
+		skinningQuery.GetSkinningMethod(),
+		skinningQuery.GetGeomBindTransform( time ).ExtractRotationMatrix().GetInverse().GetTranspose(),
+		invTransposeXforms, jointIndices, jointWeights,
+		skinningQuery.GetNumInfluencesPerComponent(),
+		faceVertexIndices, *normals
+	);
+}
+
 bool readPrimitiveVariables( const pxr::UsdSkelRoot &skelRoot, const pxr::UsdGeomPointBased &pointBased, pxr::UsdTimeCode time, IECoreScene::Primitive *primitive, const Canceller *canceller )
 {
 	Canceller::check( canceller );
@@ -358,10 +407,6 @@ bool readPrimitiveVariables( const pxr::UsdSkelRoot &skelRoot, const pxr::UsdGeo
 	Canceller::check( canceller );
 	applyBlendShapes( pointBased, time, skelQuery, skinningQuery, points );
 
-	// The UsdSkelBakeSkinning example code uses skinningQuery.GetJointMapper() to remap
-	// xforms based on a per-prim joint order. However, doing this seems to scramble data
-	// for UsdSkel crowds exported from Houdini. We don't have any example data that requires
-	// the joint remapping, so for now we're omiting it in favor of more seamless DCC support.
 	Canceller::check( canceller );
 	if( !skinningQuery.ComputeSkinnedPoints( skinningXforms, &points, time ) )
 	{
@@ -398,19 +443,41 @@ bool readPrimitiveVariables( const pxr::UsdSkelRoot &skelRoot, const pxr::UsdGeo
 		pointBased.GetPointsAttr()
 	);
 
-	// we'll consider normals optional and return true regardless of whether normals were skinned successfully
+	// Normals
+
+	Canceller::check( canceller );
 	pxr::VtVec3fArray normals;
-	if( pointBased.GetNormalsAttr().Get( &normals, time ) && skinningQuery.ComputeSkinnedNormals( skinningXforms, &normals, time ) )
+	if( !pointBased.GetNormalsAttr().Get( &normals, time ) )
 	{
-		Canceller::check( canceller );
-		if( auto n = boost::static_pointer_cast<V3fVectorData>( DataAlgo::fromUSD( normals ) ) )
-		{
-			n->setInterpretation( GeometricData::Normal );
-			addPrimitiveVariableIfValid(
-				primitive, "N", IECoreScene::PrimitiveVariable( PrimitiveAlgo::fromUSD( pointBased.GetNormalsInterpolation() ), n ),
-				pointBased.GetNormalsAttr()
-			);
-		}
+		// Now that we've skinned "P", we'll always return true, regardless of
+		// whether or not we can skin "N".
+		return true;
+	}
+
+	const TfToken normalsInterpolation = pointBased.GetNormalsInterpolation();
+
+	Canceller::check( canceller );
+	bool normalsValid = false;
+	if( normalsInterpolation == UsdGeomTokens->faceVarying )
+	{
+		// UsdGeomSkinningQuery doesn't support facevarying normals. But
+		// there are lower-level functions we can use manually, so do that.
+		normalsValid = computeFaceVaryingSkinnedNormals( skinningQuery, skinningXforms, &normals, time, canceller );
+	}
+	else
+	{
+		// UsdGeomSkinningQuery will do it all for us.
+		normalsValid = skinningQuery.ComputeSkinnedNormals( skinningXforms, &normals, time );
+	}
+
+	if( normalsValid )
+	{
+		auto n = boost::static_pointer_cast<V3fVectorData>( DataAlgo::fromUSD( normals ) );
+		n->setInterpretation( GeometricData::Normal );
+		addPrimitiveVariableIfValid(
+			primitive, "N", IECoreScene::PrimitiveVariable( PrimitiveAlgo::fromUSD( normalsInterpolation ), n ),
+			pointBased.GetNormalsAttr()
+		);
 	}
 
 	return true;
diff --git a/contrib/IECoreUSD/src/IECoreUSD/ShaderAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/ShaderAlgo.cpp
index e80fa58ab8..9f2891ead9 100644
--- a/contrib/IECoreUSD/src/IECoreUSD/ShaderAlgo.cpp
+++ b/contrib/IECoreUSD/src/IECoreUSD/ShaderAlgo.cpp
@@ -64,7 +64,8 @@
 namespace
 {
 
-pxr::TfToken g_adapterLabelToken( IECoreScene::ShaderNetworkAlgo::componentConnectionAdapterLabel().string() );
+const pxr::TfToken g_blindDataToken( "cortex:blindData" );
+pxr::TfToken g_legacyAdapterLabelToken( IECoreScene::ShaderNetworkAlgo::componentConnectionAdapterLabel().string() );
 
 std::pair<pxr::TfToken, std::string> shaderIdAndType( const pxr::UsdShadeConnectableAPI &connectable )
 {
@@ -249,11 +250,26 @@ IECore::InternedString readShaderNetworkWalk( const pxr::SdfPath &anchorPath, co
 	readNonStandardLightParameters( usdShader.GetPrim(), parameters );
 
 	IECoreScene::ShaderPtr newShader = new IECoreScene::Shader( shaderName, shaderType, parametersData );
+
+	// General purpose support for any Cortex blind data.
+
+	const pxr::VtValue blindDataValue = usdShader.GetPrim().GetCustomDataByKey( g_blindDataToken );
+	if( !blindDataValue.IsEmpty() )
+	{
+		if( auto blindData = IECore::runTimeCast<IECore::CompoundData>( IECoreUSD::DataAlgo::fromUSD( blindDataValue ) ) )
+		{
+			newShader->blindData()->writable() = blindData->readable();
+		}
+	}
+
+	// Legacy support for `cortex_autoAdaptor` metadata.
+
 	pxr::VtValue metadataValue;
-	if( usdShader.GetPrim().GetMetadata( g_adapterLabelToken, &metadataValue ) && metadataValue.Get<bool>() )
+	if( usdShader.GetPrim().GetMetadata( g_legacyAdapterLabelToken, &metadataValue ) && metadataValue.Get<bool>() )
 	{
 		newShader->blindData()->writable()[ IECoreScene::ShaderNetworkAlgo::componentConnectionAdapterLabel() ] = new IECore::BoolData( true );
 	}
+
 	shaderNetwork.addShader( handle, std::move( newShader ) );
 
 	// Can only add connections after we've added the shader.
@@ -350,10 +366,12 @@ void writeShaderParameterValues( const IECoreScene::Shader *shader, pxr::UsdShad
 		input.Set( IECoreUSD::DataAlgo::toUSD( p.second.get() ) );
 	}
 
-	const IECore::BoolData *adapterMeta = shader->blindData()->member<IECore::BoolData>( IECoreScene::ShaderNetworkAlgo::componentConnectionAdapterLabel() );
-	if( adapterMeta && adapterMeta->readable() )
+	if( shader->blindData()->readable().size() )
 	{
-		usdShader.GetPrim().SetMetadata( g_adapterLabelToken, true );
+		usdShader.GetPrim().SetCustomDataByKey(
+			g_blindDataToken,
+			IECoreUSD::DataAlgo::toUSD( shader->blindData() )
+		);
 	}
 }
 
diff --git a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp
index dbd1b624a3..40dec351ae 100644
--- a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp
+++ b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp
@@ -283,6 +283,33 @@ boost::container::flat_map<pxr::TfToken, PrimPredicate> g_schemaTypeSetPredicate
 	{ pxr::TfToken( "usd:pointInstancers" ), &pxr::UsdPrim::IsA<pxr::UsdGeomPointInstancer> }
 };
 
+bool collectionIsSet( const pxr::UsdCollectionAPI &collection )
+{
+	if(
+		collection.GetPrim().HasAPI<pxr::UsdLuxLightAPI>() &&
+		( collection.GetName() == pxr::UsdLuxTokens->lightLink || collection.GetName() == pxr::UsdLuxTokens->shadowLink )
+	)
+	{
+		// These collections are problematic :
+		//
+		// - They define the objects that _this_ light is linked to. So it makes
+		//   no sense to combine them from multiple prims into a single set as
+		//   we do when loading recursively.
+		// - The UsdLuxLightAPI defaults the collection to have
+		//   `includeRoot=true`, which in conjunction with the default expansion
+		//   rule means that it will include every single prim in the scene.
+		//   That's not only a lot of prims, but most of them won't be below the
+		//   collection's prim, and every single one of those will trigger a
+		//   warning.
+		// - Gaffer has a different light-linking mechanism anyway.
+		//
+		// For these reasons, we do not treat them as sets.
+		return false;
+	}
+
+	return true;
+}
+
 // If `predicate` is non-null then it is called to determine if _this_ prim is in the set. If null,
 // then the set is loaded from a UsdCollection called `name`.
 IECore::PathMatcher localSet( const pxr::UsdPrim &prim, const pxr::TfToken &name, PrimPredicate predicate, const Canceller *canceller )
@@ -298,7 +325,9 @@ IECore::PathMatcher localSet( const pxr::UsdPrim &prim, const pxr::TfToken &name
 	}
 
 	const size_t prefixSize = prim.GetPath().GetPathElementCount();
-	if( auto collection = pxr::UsdCollectionAPI( prim, name ) )
+
+	auto collection = pxr::UsdCollectionAPI( prim, name );
+	if( collection && collectionIsSet( collection ) )
 	{
 		Canceller::check( canceller );
 		pxr::UsdCollectionAPI::MembershipQuery membershipQuery = collection.ComputeMembershipQuery();
@@ -406,7 +435,10 @@ SceneInterface::NameList localSetNames( const pxr::UsdPrim &prim )
 		result.reserve( allCollections.size() );
 		for( const pxr::UsdCollectionAPI &collection : allCollections )
 		{
-			result.push_back( collection.GetName().GetString() );
+			if( collectionIsSet( collection ) )
+			{
+				result.push_back( collection.GetName().GetString() );
+			}
 		}
 	}
 	else
@@ -679,7 +711,7 @@ class USDScene::IO : public RefCounted
 			return m_stage;
 		}
 
-		pxr::UsdTimeCode getTime( double timeSeconds ) const
+		pxr::UsdTimeCode timeCode( double timeSeconds ) const
 		{
 			return timeSeconds * m_timeCodesPerSecond;
 		}
@@ -761,11 +793,11 @@ class USDScene::IO : public RefCounted
 
 		static pxr::UsdStageRefPtr makeStage( const std::string &fileName, IndexedIO::OpenMode openMode )
 		{
+			pxr::UsdStageRefPtr stage;
 			switch( openMode )
 			{
 				case IndexedIO::Read : {
 					static const std::string g_stageCachePrefix( "stageCache:" );
-					pxr::UsdStageRefPtr stage;
 					if( boost::starts_with( fileName, g_stageCachePrefix ) )
 					{
 						// Get Id from filename of form "stageCache:{id}.usd"
@@ -779,17 +811,20 @@ class USDScene::IO : public RefCounted
 					{
 						stage = pxr::UsdStage::Open( fileName );
 					}
-					if( !stage )
-					{
-						throw IECore::Exception( boost::str( boost::format( "USDScene : Failed to open USD stage : '%1%'" ) % fileName ) );
-					}
-					return stage;
+					break;
 				}
 				case IndexedIO::Write :
-					return pxr::UsdStage::CreateNew( fileName );
+					stage = pxr::UsdStage::CreateNew( fileName );
+					break;
 				default:
 					throw Exception( "Unsupported OpenMode" );
 			}
+
+			if( !stage )
+			{
+				throw IECore::Exception( boost::str( boost::format( "USDScene : Failed to open USD stage : '%1%'" ) % fileName ) );
+			}
+			return stage;
 		}
 
 		std::string m_fileName;
@@ -902,7 +937,7 @@ Imath::Box3d USDScene::readBound( double time ) const
 	}
 
 	pxr::VtArray<pxr::GfVec3f> extents;
-	attr.Get( &extents, m_root->getTime( time ) );
+	attr.Get( &extents, m_root->timeCode( time ) );
 
 	// When coming from UsdGeomModelAPI, `extents` may contain several bounds,
 	// on a per-purpose basis. Take the union, since the SceneInterface API only
@@ -930,12 +965,12 @@ ConstDataPtr USDScene::readTransform( double time ) const
 
 Imath::M44d USDScene::readTransformAsMatrix( double time ) const
 {
-	return localTransform( m_location->prim, m_root->getTime( time ) );
+	return localTransform( m_location->prim, m_root->timeCode( time ) );
 }
 
 ConstObjectPtr USDScene::readObject( double time, const Canceller *canceller ) const
 {
-	return ObjectAlgo::readObject( m_location->prim, m_root->getTime( time ), canceller );
+	return ObjectAlgo::readObject( m_location->prim, m_root->timeCode( time ), canceller );
 }
 
 SceneInterface::Name USDScene::name() const
@@ -973,7 +1008,7 @@ void USDScene::writeBound( const Imath::Box3d &bound, double time )
 	extent.push_back( DataAlgo::toUSD( Imath::V3f( bound.max ) ) );
 
 	pxr::UsdAttribute extentAttr = boundable.CreateExtentAttr();
-	extentAttr.Set( pxr::VtValue( extent ) );
+	extentAttr.Set( pxr::VtValue( extent ), m_root->timeCode( time ) );
 }
 
 void USDScene::writeTransform( const Data *transform, double time )
@@ -988,7 +1023,7 @@ void USDScene::writeTransform( const Data *transform, double time )
 	if( xformable )
 	{
 		pxr::UsdGeomXformOp transformOp = xformable.MakeMatrixXform();
-		const pxr::UsdTimeCode timeCode = m_root->getTime( time );
+		const pxr::UsdTimeCode timeCode = m_root->timeCode( time );
 		transformOp.Set( DataAlgo::toUSD( m44->readable() ), timeCode );
 	}
 }
@@ -1147,7 +1182,7 @@ ConstObjectPtr USDScene::readAttribute( const SceneInterface::Name &name, double
 		{
 			return nullptr;
 		}
-		pxr::TfToken value; attr.Get( &value, m_root->getTime( time ) );
+		pxr::TfToken value; attr.Get( &value, m_root->timeCode( time ) );
 		if( value == pxr::UsdGeomTokens->inherited )
 		{
 			return new BoolData( true );
@@ -1192,7 +1227,7 @@ ConstObjectPtr USDScene::readAttribute( const SceneInterface::Name &name, double
 	{
 		pxr::UsdAttribute attr = pxr::UsdGeomGprim( m_location->prim ).GetDoubleSidedAttr();
 		bool doubleSided;
-		if( attr.HasAuthoredValue() && attr.Get( &doubleSided, m_root->getTime( time ) ) )
+		if( attr.HasAuthoredValue() && attr.Get( &doubleSided, m_root->timeCode( time ) ) )
 		{
 			return new BoolData( doubleSided );
 		}
@@ -1200,7 +1235,7 @@ ConstObjectPtr USDScene::readAttribute( const SceneInterface::Name &name, double
 	}
 	else if( pxr::UsdAttribute attribute = AttributeAlgo::findUSDAttribute( m_location->prim, name.string() ) )
 	{
-		return DataAlgo::fromUSD( attribute, m_root->getTime( time ) );
+		return DataAlgo::fromUSD( attribute, m_root->timeCode( time ) );
 	}
 	else
 	{
@@ -1225,7 +1260,7 @@ void USDScene::writeAttribute( const SceneInterface::Name &name, const Object *a
 			pxr::UsdGeomImageable imageable( m_location->prim );
 			imageable.GetVisibilityAttr().Set(
 				data->readable() ? pxr::UsdGeomTokens->inherited : pxr::UsdGeomTokens->invisible,
-				m_root->getTime( time )
+				m_root->timeCode( time )
 			);
 		}
 	}
@@ -1258,7 +1293,7 @@ void USDScene::writeAttribute( const SceneInterface::Name &name, const Object *a
 			pxr::UsdGeomGprim gprim( m_location->prim );
 			if( gprim )
 			{
-				gprim.GetDoubleSidedAttr().Set( data->readable(), m_root->getTime( time ) );
+				gprim.GetDoubleSidedAttr().Set( data->readable(), m_root->timeCode( time ) );
 			}
 			else
 			{
@@ -1306,7 +1341,7 @@ void USDScene::writeAttribute( const SceneInterface::Name &name, const Object *a
 						pxr::TfToken( g.first.string().substr( 10 ) ),
 						DataAlgo::valueTypeName( d )
 					);
-					globalAttribute.Set( DataAlgo::toUSD( d ) );
+					globalAttribute.Set( DataAlgo::toUSD( d ), m_root->timeCode( time ) );
 				}
 			}
 		}
@@ -1322,14 +1357,14 @@ void USDScene::writeAttribute( const SceneInterface::Name &name, const Object *a
 				pxr::UsdGeomPrimvar usdPrimvar = primvarsAPI.CreatePrimvar(
 					usdName.name, DataAlgo::valueTypeName( data ), pxr::UsdGeomTokens->constant
 				);
-				usdPrimvar.Set( DataAlgo::toUSD( data ), time );
+				usdPrimvar.Set( DataAlgo::toUSD( data ), m_root->timeCode( time ) );
 			}
 			else
 			{
 				pxr::UsdAttribute newAttribute = m_location->prim.CreateAttribute(
 					usdName.name, DataAlgo::valueTypeName( data )
 				);
-				newAttribute.Set( DataAlgo::toUSD( data ), time );
+				newAttribute.Set( DataAlgo::toUSD( data ), m_root->timeCode( time ) );
 			}
 		}
 	}
@@ -1453,7 +1488,7 @@ PrimitiveVariableMap USDScene::readObjectPrimitiveVariables( const std::vector<I
 
 void USDScene::writeObject( const Object *object, double time )
 {
-	if( !ObjectAlgo::writeObject( object, m_root->getStage(), m_location->prim.GetPath(), m_root->getTime( time ) ) )
+	if( !ObjectAlgo::writeObject( object, m_root->getStage(), m_location->prim.GetPath(), m_root->timeCode( time ) ) )
 	{
 		IECore::msg(
 			IECore::Msg::Warning, "USDScene::writeObject",
diff --git a/contrib/IECoreUSD/src/IECoreUSD/VolumeAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/VolumeAlgo.cpp
index 4a58cc6bc7..02d6c9155e 100644
--- a/contrib/IECoreUSD/src/IECoreUSD/VolumeAlgo.cpp
+++ b/contrib/IECoreUSD/src/IECoreUSD/VolumeAlgo.cpp
@@ -32,11 +32,13 @@
 //
 //////////////////////////////////////////////////////////////////////////
 
+#include "IECoreUSD/DataAlgo.h"
 #include "IECoreUSD/ObjectAlgo.h"
 
 #include "IECoreVDB/VDBObject.h"
 
 #include "IECore/MessageHandler.h"
+#include "IECore/SimpleTypedData.h"
 
 IECORE_PUSH_DEFAULT_VISIBILITY
 #include "pxr/usd/usdVol/volume.h"
@@ -96,9 +98,11 @@ IECore::ObjectPtr readVolume( pxr::UsdVolVolume &volume, pxr::UsdTimeCode time,
 			continue;
 		}
 
-		SdfAssetPath fieldAssetPath;
-		fieldAsset.GetFilePathAttr().Get( &fieldAssetPath, time );
-		const std::string fieldFileName = fieldAssetPath.GetResolvedPath();
+		std::string fieldFileName;
+		if( auto fieldFileNameData = runTimeCast<const StringData>( DataAlgo::fromUSD( fieldAsset.GetFilePathAttr(), time ) ) )
+		{
+			fieldFileName = fieldFileNameData->readable();
+		}
 
 		if( fileName.empty() )
 		{
diff --git a/contrib/IECoreUSD/test/IECoreUSD/DataAlgoTest.py b/contrib/IECoreUSD/test/IECoreUSD/DataAlgoTest.py
index f594cc9086..d425851567 100644
--- a/contrib/IECoreUSD/test/IECoreUSD/DataAlgoTest.py
+++ b/contrib/IECoreUSD/test/IECoreUSD/DataAlgoTest.py
@@ -83,7 +83,7 @@ def testToUSDBinding( self ) :
 			( IECore.FloatData( 2.5 ), 2.5 ),
 			( IECore.IntVectorData( [ 1, 2, 3 ] ), [ 1, 2, 3 ] ),
 			( IECore.PathMatcherData(), None ),
-			( IECore.CompoundData(), None ),
+			( IECore.CompoundData(), {} ),
 		] :
 			self.assertEqual( IECoreUSD.DataAlgo.toUSD( data ), value )
 
diff --git a/contrib/IECoreUSD/test/IECoreUSD/SceneCacheFileFormatTest.py b/contrib/IECoreUSD/test/IECoreUSD/SceneCacheFileFormatTest.py
index 7a4a6778fc..f943a2706d 100644
--- a/contrib/IECoreUSD/test/IECoreUSD/SceneCacheFileFormatTest.py
+++ b/contrib/IECoreUSD/test/IECoreUSD/SceneCacheFileFormatTest.py
@@ -47,7 +47,6 @@
 import IECoreScene
 import IECoreUSD
 
-@unittest.skipIf( sys.platform == 'darwin', "plugInfo.json fails to register on GitHub Actions Macos container." )
 class SceneCacheFileFormatTest( unittest.TestCase ) :
 
 	def setUp( self ) :
diff --git a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py
index 11fe8a4abf..c9a52115cf 100644
--- a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py
+++ b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py
@@ -32,6 +32,8 @@
 #
 ##########################################################################
 
+import IECore
+
 import importlib
 import os
 import math
@@ -40,6 +42,8 @@
 import shutil
 import tempfile
 import imath
+import subprocess
+import sys
 import threading
 import time
 
@@ -2569,6 +2573,21 @@ def testSkelBlendShapes( self ) :
 			self.assertAlmostEqual( arm_10["P"].data[i].y, expected_10[i].y, 5 )
 			self.assertAlmostEqual( arm_10["P"].data[i].z, expected_10[i].z, 5 )
 
+	def testSkinnedFaceVaryingNormals( self ) :
+
+		root = IECoreScene.SceneInterface.create( os.path.dirname( __file__ ) + "/data/skinnedFaceVaryingNormals.usda", IECore.IndexedIO.OpenMode.Read )
+		cubeLocation = root.scene( [ "main", "pCube1" ] )
+		for timeSample in range( 1, 25 ) :
+
+			cubeMesh = cubeLocation.readObject( timeSample / 24.0 )
+			self.assertIn( "N", cubeMesh )
+			self.assertTrue( cubeMesh.isPrimitiveVariableValid( cubeMesh["N"] ) )
+			self.assertEqual( cubeMesh["N"].interpolation, IECoreScene.PrimitiveVariable.Interpolation.FaceVarying )
+
+			referenceNormals = IECoreScene.MeshAlgo.calculateFaceVaryingNormals( cubeMesh, thresholdAngle = 5 )
+			for referenceNormal, normal in zip( referenceNormals.data, cubeMesh["N"].data ) :
+				self.assertTrue( normal.equalWithAbsError( referenceNormal, 0.000001 ) )
+
 	@unittest.skipIf( ( IECore.TestUtil.inMacCI() or IECore.TestUtil.inWindowsCI() ), "Mac and Windows CI are too slow for reliable timing" )
 	def testCancel ( self ) :
 
@@ -2792,6 +2811,41 @@ def testAttributeBadPrefix( self ):
 		self.assertEqual( set( root.child( "loc" ).attributeNames() ), set( ['ai:testAttribute' ] ) )
 		self.assertEqual( root.child( "loc" ).readAttribute( 'ai:testAttribute', 0 ), IECore.FloatData( 9 ) )
 
+	def testWriteAnimatedAttribute( self ) :
+
+		fileName = os.path.join( self.temporaryDirectory(), "test.usda" )
+		root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write )
+		child = root.createChild( "child" )
+
+		for t in ( 0.0, 0.5, 1.0 ) :
+			child.writeAttribute( "user:test", IECore.FloatData( t ), t )
+
+		del child, root
+
+		root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
+		child = root.child( "child" )
+
+		for t in ( 0.0, 0.5, 1.0 ) :
+			self.assertEqual( child.readAttribute( "user:test", t ), IECore.FloatData( t ) )
+
+	def testWriteAnimatedBound( self ) :
+
+		fileName = os.path.join( self.temporaryDirectory(), "test.usda" )
+		root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write )
+		child = root.createChild( "child" )
+
+		for t in ( 1.0, 1.5, 2.0 ) :
+			child.writeObject( IECoreScene.SpherePrimitive( t ), t )
+			child.writeBound( imath.Box3d( imath.V3d( -t ), imath.V3d( t ) ), t )
+
+		del child, root
+
+		root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
+		child = root.child( "child" )
+
+		for t in ( 1.0, 1.5, 2.0 ) :
+			self.assertEqual( child.readBound( t ), imath.Box3d( imath.V3d( -t ), imath.V3d( t ) ) )
+
 	def testShaders( self ) :
 
 		# Write shaders
@@ -3347,6 +3401,37 @@ def testLightAttribute( self ) :
 			} )
 		)
 
+	@unittest.skipIf( pxr.Usd.GetVersion() < ( 0, 21, 11 ), "UsdLuxLightAPI not available" )
+	def testLightAndShadowLinkCollections( self ) :
+
+		# We ignore `lightLink` and `shadowLink` on lights, because they have
+		# a specific meaning in USD that doesn't translate to our definition of a set.
+
+		root = IECoreScene.SceneInterface.create(
+			os.path.join( os.path.dirname( __file__ ), "data", "sphereLight.usda" ),
+			IECore.IndexedIO.OpenMode.Read
+		)
+		self.assertNotIn( "lightLink", root.setNames() )
+		self.assertNotIn( "shadowLink", root.setNames() )
+		self.assertEqual( root.readSet( "lightLink" ), IECore.PathMatcher() )
+		self.assertEqual( root.readSet( "shadowLink" ), IECore.PathMatcher() )
+
+		# But that doesn't mean folks can't use those names for sets elsewhere if they
+		# want to.
+
+		fileName = os.path.join( self.temporaryDirectory(), "test.usda" )
+		root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write )
+
+		root.writeSet( "lightLink", IECore.PathMatcher( [ "/test1" ] ) )
+		root.writeSet( "shadowLink", IECore.PathMatcher( [ "/test2" ] ) )
+		del root
+
+		root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
+		self.assertIn( "lightLink", root.setNames() )
+		self.assertIn( "shadowLink", root.setNames() )
+		self.assertEqual( root.readSet( "lightLink" ), IECore.PathMatcher( [ "/test1" ] ) )
+		self.assertEqual( root.readSet( "shadowLink" ), IECore.PathMatcher( [ "/test2" ] ) )
+
 	def testReadDoubleSidedAttribute( self ) :
 
 		root = IECoreScene.SceneInterface.create(
@@ -3486,6 +3571,55 @@ def testColor4fShaderParameterComponentConnections( self ) :
 		root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
 		self.assertEqual( root.child( "object" ).readAttribute( "ai:surface", 0 ), network )
 
+	def testLegacyComponentConnections( self ) :
+
+		expectedNetwork = IECoreScene.ShaderNetwork(
+			shaders = {
+				"source" : IECoreScene.Shader( "noise" ),
+				"output" : IECoreScene.Shader(
+					"color_correct",
+					parameters = {
+						"input" : imath.Color4f( 1 ),
+					}
+				),
+			},
+			connections = [
+				( ( "source", "r" ), ( "output", "input.g" ) ),
+				( ( "source", "g" ), ( "output", "input.b" ) ),
+				( ( "source", "b" ), ( "output", "input.r" ) ),
+				( ( "source", "r" ), ( "output", "input.a" ) ),
+			],
+			output = "output",
+		)
+
+		root = IECoreScene.SceneInterface.create( os.path.join( os.path.dirname( __file__ ), "data", "legacyComponentConnections.usda" ), IECore.IndexedIO.OpenMode.Read )
+		self.assertEqual( root.child( "object" ).readAttribute( "ai:surface", 0 ), expectedNetwork )
+
+	def testShaderBlindData( self ) :
+
+		shader = IECoreScene.Shader( "test" )
+		shader.blindData()["testInt"] = IECore.IntData( 10 )
+		shader.blindData()["testFloatVector"] = IECore.FloatVectorData( [ 1, 2, 3, ] )
+		shader.blindData()["test:colon"] = IECore.BoolData( True )
+		shader.blindData()["testCompound"] = IECore.CompoundData( {
+			"testString" : "test",
+			"testStringVector" : IECore.StringVectorData( [ "one", "two" ] )
+		} )
+
+		network = IECoreScene.ShaderNetwork(
+			shaders = { "test" : shader },
+			output = ( "test", "out" )
+		)
+
+		fileName = os.path.join( self.temporaryDirectory(), "testShaderBlindData.usda" )
+		root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write )
+		object = root.createChild( "object" )
+		object.writeAttribute( "surface", network, 0.0 )
+		del object, root
+
+		root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
+		self.assertEqual( root.child( "object" ).readAttribute( "surface", 0 ), network )
+
 	def testMaterialPurpose( self ) :
 
 		def assertExpected( root ) :
@@ -3656,10 +3790,10 @@ def testPointInstancerPrimvars( self ) :
 
 		fileName = os.path.join( self.temporaryDirectory(), "pointInstancePrimvars.usda" )
 		stage = pxr.Usd.Stage.CreateNew( fileName )
-		points = pxr.UsdGeom.PointInstancer.Define( stage, "/points" )
-		points.CreatePositionsAttr( [ ( v, v, v ) for v in range( 0, 5 ) ] )
+		pointInstancer = pxr.UsdGeom.PointInstancer.Define( stage, "/points" )
+		pointInstancer.CreatePositionsAttr( [ ( v, v, v ) for v in range( 0, 5 ) ] )
 
-		primvars = pxr.UsdGeom.PrimvarsAPI( points )
+		primvars = pxr.UsdGeom.PrimvarsAPI( pointInstancer )
 		primvar = primvars.CreatePrimvar( "myColor", pxr.Sdf.ValueTypeNames.Color3fArray, "vertex" )
 		primvar.Set(
 			[ ( c, c, c ) for c in range( 1, 6 ) ]
@@ -3672,6 +3806,8 @@ def testPointInstancerPrimvars( self ) :
 		root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
 		points = root.child( "points" ).readObject( 0 )
 
+		self.assertEqual( points.keys(), ['P', 'myColor', 'prototypeRoots'] )
+
 		self.assertIsInstance( points, IECoreScene.PointsPrimitive )
 		self.assertIn( "myColor", points )
 		self.assertEqual(
@@ -3681,6 +3817,21 @@ def testPointInstancerPrimvars( self ) :
 		self.assertEqual( points["myColor"].interpolation, IECoreScene.PrimitiveVariable.Interpolation.Vertex )
 		self.assertEqual( points["myColor"].indices, None )
 
+		# Now try deactivating some ids
+
+		pointInstancer.DeactivateIds( [ 0, 2 ] )
+		pointInstancer.InvisIds( [ 1, 4 ], 0 )
+
+		stage.GetRootLayer().Save()
+
+		root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
+		points = root.child( "points" ).readObject( 0 )
+
+		self.assertEqual( points.keys(), ['P', 'inactiveIds', 'invisibleIds', 'myColor', 'prototypeRoots'] )
+
+		self.assertEqual( points["inactiveIds"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.Int64VectorData( [ 0, 2 ] ) ) )
+		self.assertEqual( points["invisibleIds"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.Int64VectorData( [ 1, 4 ] ) ) )
+
 	def testArnoldArrayInputs( self ) :
 
 		def assertExpectedArrayInputs( network ) :
@@ -4313,5 +4464,83 @@ def testSetNameValidation( self ) :
 			with self.subTest( setName = setName ) :
 				self.assertEqual( root.readSet( setName ), IECore.PathMatcher( [ f"/set{setIndex}Member" ] ) )
 
+	def testWriteToOpenScene( self ) :
+
+		# Using posix-format filename, because Windows backslashes don't play nicely
+		# with `assertRaisesRegex()`.
+		fileName = ( pathlib.Path( self.temporaryDirectory() ) / "test.usda" ).as_posix()
+		IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write )
+
+		reader = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
+
+		with self.assertRaisesRegex( RuntimeError, f"USDScene : Failed to open USD stage : '{fileName}'" ) :
+			IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write )
+
+	def testAssetPathSlashes ( self ) :
+
+		root = IECoreScene.SceneInterface.create(
+			os.path.join( os.path.dirname( __file__ ), "data", "assetPathAttribute.usda" ),
+			IECore.IndexedIO.OpenMode.Read
+		)
+		xform = root.child( "xform" )
+
+		self.assertEqual( xform.attributeNames(), [ "render:testAsset" ] )
+		self.assertNotIn( "\\", xform.readAttribute( "render:testAsset", 0 ).value )
+		self.assertTrue( pathlib.Path( xform.readAttribute( "render:testAsset", 0 ).value ).is_file() )
+
+	def _testPointInstancerRelativePrototypes( self ) :
+
+		root = IECoreScene.SceneInterface.create(
+			os.path.join( os.path.dirname( __file__ ), "data", "pointInstancerWeirdPrototypes.usda" ),
+			IECore.IndexedIO.OpenMode.Read
+		)
+		pointInstancer = root.child( "inst" )
+		obj = pointInstancer.readObject(0.0)
+
+		if os.environ.get( "IECOREUSD_POINTINSTANCER_RELATIVE_PROTOTYPES", "0" ) != "0" :
+			self.assertEqual( obj["prototypeRoots"].data, IECore.StringVectorData( [ './Prototypes/sphere', '/cube' ] ) )
+		else :
+			self.assertEqual( obj["prototypeRoots"].data, IECore.StringVectorData( [ '/inst/Prototypes/sphere', '/cube' ] ) )
+
+	def testPointInstancerRelativePrototypes( self ) :
+
+		for relative in [ "0", "1", None ] :
+
+			with self.subTest( relative = relative ) :
+
+				env = os.environ.copy()
+				if relative is not None :
+					env["IECOREUSD_POINTINSTANCER_RELATIVE_PROTOTYPES"] = relative
+				else :
+					env.pop( "IECOREUSD_POINTINSTANCER_RELATIVE_PROTOTYPES", None )
+
+				try :
+					subprocess.check_output(
+						[ sys.executable, __file__, "USDSceneTest._testPointInstancerRelativePrototypes" ],
+						env = env, stderr = subprocess.STDOUT
+					)
+				except subprocess.CalledProcessError as e :
+					self.fail( e.output )
+
+	@unittest.skipIf( not haveVDB, "No IECoreVDB" )
+	def testUsdVolVolumeSlashes( self ) :
+
+		import IECoreVDB
+
+		fileName = os.path.dirname( __file__ ) + "/data/volume.usda"
+		root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
+		child = root.child( "volume" )
+
+		vdbObject = child.readObject( 0 )
+		self.assertNotIn( "\\", vdbObject.fileName() )
+		self.assertTrue( pathlib.Path( vdbObject.fileName() ).is_file() )
+
+	@unittest.skipIf( not haveVDB, "No IECoreVDB" )
+	def testUsdVolVolumeWithEmptyField( self ) :
+
+		fileName = os.path.dirname( __file__ ) + "/data/volumeWithEmptyField.usda"
+		root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
+		self.assertIsNone( root.child( "volume" ).readObject( 0 ) )
+
 if __name__ == "__main__":
 	unittest.main()
diff --git a/contrib/IECoreUSD/test/IECoreUSD/data/legacyComponentConnections.usda b/contrib/IECoreUSD/test/IECoreUSD/data/legacyComponentConnections.usda
new file mode 100644
index 0000000000..2d2674594c
--- /dev/null
+++ b/contrib/IECoreUSD/test/IECoreUSD/data/legacyComponentConnections.usda
@@ -0,0 +1,54 @@
+#usda 1.0
+
+def Xform "object" (
+    prepend apiSchemas = ["MaterialBindingAPI"]
+)
+{
+    rel material:binding = </object/materials/material_aaad0a40dfc4f67f90ca42cbc732dec4>
+
+    def Scope "materials" (
+        cortex_autoMaterials = true
+    )
+    {
+        def Material "material_aaad0a40dfc4f67f90ca42cbc732dec4"
+        {
+            token outputs:arnold:surface.connect = </object/materials/material_aaad0a40dfc4f67f90ca42cbc732dec4/arnold_surface_shaders/output.outputs:DEFAULT_OUTPUT>
+
+            def Scope "arnold_surface_shaders"
+            {
+                def Shader "output"
+                {
+                    uniform token info:id = "color_correct"
+                    color4f inputs:input = (1, 1, 1, 1)
+                    color4f inputs:input.connect = </object/materials/material_aaad0a40dfc4f67f90ca42cbc732dec4/arnold_surface_shaders/pack.outputs:out>
+                    token outputs:DEFAULT_OUTPUT
+                }
+
+                def Shader "pack" (
+                    cortex_autoAdapter = true
+                )
+                {
+                    uniform token info:id = "osl:MaterialX/mx_pack_color"
+                    float inputs:in1 = 1
+                    float inputs:in1.connect = </object/materials/material_aaad0a40dfc4f67f90ca42cbc732dec4/arnold_surface_shaders/source.outputs:b>
+                    float inputs:in2 = 1
+                    float inputs:in2.connect = </object/materials/material_aaad0a40dfc4f67f90ca42cbc732dec4/arnold_surface_shaders/source.outputs:r>
+                    float inputs:in3 = 1
+                    float inputs:in3.connect = </object/materials/material_aaad0a40dfc4f67f90ca42cbc732dec4/arnold_surface_shaders/source.outputs:g>
+                    float inputs:in4 = 1
+                    float inputs:in4.connect = </object/materials/material_aaad0a40dfc4f67f90ca42cbc732dec4/arnold_surface_shaders/source.outputs:r>
+                    color4f outputs:out
+                }
+
+                def Shader "source"
+                {
+                    uniform token info:id = "noise"
+                    float outputs:b
+                    float outputs:g
+                    float outputs:r
+                }
+            }
+        }
+    }
+}
+
diff --git a/contrib/IECoreUSD/test/IECoreUSD/data/pointInstancerWeirdPrototypes.usda b/contrib/IECoreUSD/test/IECoreUSD/data/pointInstancerWeirdPrototypes.usda
new file mode 100644
index 0000000000..4845af62d6
--- /dev/null
+++ b/contrib/IECoreUSD/test/IECoreUSD/data/pointInstancerWeirdPrototypes.usda
@@ -0,0 +1,26 @@
+#usda 1.0
+(
+)
+
+def PointInstancer "inst" (
+	kind = "group"
+)
+{
+	point3f[] positions = [(0, 0, -20), (0, 0, -16), (0, 0, -12), (0, 0, -8), (0, 0, -4), (0, 0, 0), (0, 0, 4), (0, 0, 8), (0, 0, 12), (0, 0, 16)]
+	int[] protoIndices = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
+	rel prototypes = [ </inst/Prototypes/sphere>, </cube> ]
+
+	def Scope "Prototypes" (
+		kind = "group"
+	)
+	{
+		def Sphere "sphere"
+		{
+			double radius = 1
+		}
+	}
+}
+
+def Cube "cube"
+{
+}
diff --git a/contrib/IECoreUSD/test/IECoreUSD/data/skinnedFaceVaryingNormals.usda b/contrib/IECoreUSD/test/IECoreUSD/data/skinnedFaceVaryingNormals.usda
new file mode 100644
index 0000000000..acb58b5444
--- /dev/null
+++ b/contrib/IECoreUSD/test/IECoreUSD/data/skinnedFaceVaryingNormals.usda
@@ -0,0 +1,68 @@
+#usda 1.0
+(
+    defaultPrim = "main"
+    metersPerUnit = 0.01
+    startTimeCode = 1
+    endTimeCode = 24
+    upAxis = "Y"
+)
+
+def SkelRoot "main" (
+    kind = "component"
+)
+{
+    float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)]
+
+    def Mesh "pCube1" (
+        prepend apiSchemas = ["SkelBindingAPI"]
+    )
+    {
+        uniform bool doubleSided = 1
+        float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)]
+        int[] faceVertexCounts = [4, 4, 4, 4, 4, 4]
+        int[] faceVertexIndices = [0, 1, 3, 2, 2, 3, 5, 4, 4, 5, 7, 6, 6, 7, 1, 0, 1, 7, 5, 3, 6, 0, 2, 4]
+        normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0)] (
+            interpolation = "faceVarying"
+        )
+        point3f[] points = [(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (-0.5, 0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5)]
+        matrix4d primvars:skel:geomBindTransform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )
+        int[] primvars:skel:jointIndices = [0, 0, 0, 0, 0, 0, 0, 0] (
+            elementSize = 1
+            interpolation = "vertex"
+        )
+        float[] primvars:skel:jointWeights = [1, 1, 1, 1, 1, 1, 1, 1] (
+            elementSize = 1
+            interpolation = "vertex"
+        )
+        uniform token[] skel:joints = ["joint1"]
+        rel skel:skeleton = </main/skel>
+        uniform token subdivisionScheme = "none"
+    }
+
+    def Skeleton "skel" (
+        prepend apiSchemas = ["SkelBindingAPI"]
+        customData = {
+            dictionary Maya = {
+                bool generated = 1
+            }
+        }
+    )
+    {
+        uniform matrix4d[] bindTransforms = [( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )]
+        uniform token[] joints = ["joint1", "joint2"]
+        uniform matrix4d[] restTransforms = [( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )]
+        rel skel:animationSource = </main/skel/anim>
+
+        def SkelAnimation "anim"
+        {
+            uniform token[] joints = ["joint1"]
+            quatf[] rotations.timeSamples = {
+                1: [(1, 0, 0, 0)],
+                24: [(0.7071, 0.7071, 0, 0)],
+            }
+            half3[] scales = [(1, 1, 1)]
+            float3[] translations = [(0, 0, 0)]
+        }
+    }
+}
+
diff --git a/contrib/IECoreUSD/test/IECoreUSD/data/volumeWithEmptyField.usda b/contrib/IECoreUSD/test/IECoreUSD/data/volumeWithEmptyField.usda
new file mode 100644
index 0000000000..173bdfbd56
--- /dev/null
+++ b/contrib/IECoreUSD/test/IECoreUSD/data/volumeWithEmptyField.usda
@@ -0,0 +1,11 @@
+#usda 1.0
+
+def Volume "volume"
+{
+    custom rel field:density = </volume/density>
+
+    def OpenVDBAsset "density"
+    {
+    }
+}
+
diff --git a/include/IECore/MatrixInterpolator.inl b/include/IECore/MatrixInterpolator.inl
index 26dcd0ca8f..ff15aa4722 100644
--- a/include/IECore/MatrixInterpolator.inl
+++ b/include/IECore/MatrixInterpolator.inl
@@ -130,7 +130,7 @@ struct CubicInterpolator< Imath::Matrix44< T > >
 
 		Imath::Vec3<T> s0( 1 ), s1( 1 ), s2( 1 ), s3( 1 ), sx( 1 );
 		Imath::Vec3<T> h0( 0 ), h1( 0 ), h2( 0 ), h3( 0 ), hx( 0 );
-		Imath::Vec3<T> r0( 0 ), r1( 0 ), r2( 0 ), r3( 0 ), rx( 0 );
+		Imath::Vec3<T> r0( 0 ), r1( 0 ), r2( 0 ), r3( 0 );
 		Imath::Vec3<T> t0( 0 ), t1( 0 ), t2( 0 ), t3( 0 ), tx( 0 );
 
 		extractSHRT(y0, s0, h0, r0, t0);
diff --git a/include/IECore/Version.h b/include/IECore/Version.h
index 273bd8a686..765919bca3 100644
--- a/include/IECore/Version.h
+++ b/include/IECore/Version.h
@@ -55,48 +55,25 @@ namespace IECore
 {
 
 /// Returns the milestone version for the IECore library
-IECORE_API inline int milestoneVersion()
-{
-	return CORTEX_MILESTONE_VERSION;
-}
+IECORE_API int milestoneVersion();
 
 /// Returns the major version for the IECore library
-IECORE_API inline int majorVersion()
-{
-	return CORTEX_MAJOR_VERSION;
-}
+IECORE_API int majorVersion();
 
 /// Returns the minor version for the IECore library
-IECORE_API inline int minorVersion()
-{
-	return CORTEX_MINOR_VERSION;
-}
+IECORE_API int minorVersion();
 
 /// Returns the patch version for the IECore library
-IECORE_API inline int patchVersion()
-{
-	return CORTEX_PATCH_VERSION;
-}
+IECORE_API int patchVersion();
 
 /// Returns an integer representation of the compatibility version for the IECore library
-IECORE_API inline int compatibilityVersion()
-{
-	return CORTEX_COMPATIBILITY_VERSION;
-}
+IECORE_API int compatibilityVersion();
 
 /// Returns a string representation of the compatibility version for the IECore library (eg "milestone.major")
-IECORE_API inline const std::string &compatibilityVersionString()
-{
-	static std::string v = boost::str( boost::format( "%d.%d" ) % milestoneVersion() % majorVersion() );
-	return v;
-}
+IECORE_API const std::string &compatibilityVersionString();
 
 /// Returns a string of the form "milestone.major.minor.patch"
-IECORE_API inline const std::string &versionString()
-{
-	static std::string v = boost::str( boost::format( "%d.%d.%d.%d" ) % milestoneVersion() % majorVersion() % minorVersion() % patchVersion() );
-	return v;
-}
+IECORE_API const std::string &versionString();
 
 } // namespace IECore
 
diff --git a/include/IECoreHoudini/GEO_CortexPrimitive.h b/include/IECoreHoudini/GEO_CortexPrimitive.h
index ca16b08004..87446f1635 100644
--- a/include/IECoreHoudini/GEO_CortexPrimitive.h
+++ b/include/IECoreHoudini/GEO_CortexPrimitive.h
@@ -109,9 +109,16 @@ class IECOREHOUDINI_API GEO_CortexPrimitive : public GEO_Primitive
 
 		virtual GEO_Primitive * copy( int preserve_shared_pts ) const;
 		virtual void copyPrimitive( const GEO_Primitive *src, GEO_Point **ptredirect );
+#if MIN_HOU_VERSION( 19, 5, 0 )
+		virtual bool getBBox( UT_BoundingBox *bbox ) const;
+#else
 		virtual int getBBox( UT_BoundingBox *bbox ) const;
+#endif
 		virtual void enlargePointBounds( UT_BoundingBox &box ) const;
 		virtual UT_Vector3 computeNormal() const;
+#if MIN_HOU_VERSION( 19, 5, 0 )
+		virtual UT_Vector3D computeNormalD() const;
+#endif
 		virtual int detachPoints( GA_PointGroup &grp );
 #if UT_MAJOR_VERSION_INT >= 16
 
@@ -148,6 +155,9 @@ class IECOREHOUDINI_API GEO_CortexPrimitive : public GEO_Primitive
 		virtual GEO_Primitive *convert( ConvertParms &parms, GA_PointGroup *usedpts = 0 );
 		virtual GEO_Primitive *convertNew( ConvertParms &parms );
 		virtual void normal( NormalComp &output ) const;
+#if MIN_HOU_VERSION( 19, 5, 0 )
+		virtual void normal( NormalCompD &output ) const;
+#endif
 		virtual int intersectRay( const UT_Vector3 &o, const UT_Vector3 &d, float tmax=1E17F, float tol=1E-12F, float *distance=0, UT_Vector3 *pos=0, UT_Vector3 *nml=0, int accurate=0, float *u=0, float *v=0, int ignoretrim=1 ) const;
 
 		/// Set the IECore::Object contained by this GEO_Primitive. Note that in most situations
diff --git a/include/IECoreHoudini/GR_CortexPrimitive.h b/include/IECoreHoudini/GR_CortexPrimitive.h
index 6c258c4005..ab681ef043 100644
--- a/include/IECoreHoudini/GR_CortexPrimitive.h
+++ b/include/IECoreHoudini/GR_CortexPrimitive.h
@@ -35,6 +35,7 @@
 #ifndef IECOREHOUDINI_GRCORTEXPRIMITIVE_H
 #define IECOREHOUDINI_GRCORTEXPRIMITIVE_H
 
+#include "IECoreHoudini/CoreHoudiniVersion.h"
 #include "IECoreHoudini/Export.h"
 
 #include "IECoreScene/Renderable.h"
@@ -73,20 +74,27 @@ class IECOREHOUDINI_API GR_CortexPrimitive : public GR_Primitive
 
 	protected :
 
+#if MIN_HOU_VERSION( 20, 0, 0 )
+		virtual void update( RE_RenderContext r, const GT_PrimitiveHandle &primh, const GR_UpdateParms &p );
+#else
 		virtual void update( RE_Render *r, const GT_PrimitiveHandle &primh, const GR_UpdateParms &p );
+#endif
 
-#if UT_MAJOR_VERSION_INT >= 16
 
+#if MIN_HOU_VERSION( 20, 0, 0 )
+		virtual void render( RE_RenderContext r, GR_RenderMode render_mode, GR_RenderFlags flags, GR_DrawParms parms);
+#elif UT_MAJOR_VERSION_INT >= 16
 		virtual void render( RE_Render *r, GR_RenderMode render_mode, GR_RenderFlags flags, GR_DrawParms parms);
-
 #else
-
 		virtual void render( RE_Render *r, GR_RenderMode render_mode, GR_RenderFlags flags, const GR_DisplayOption *opt, const UT_Array<RE_MaterialPtr> *materials );
-
 #endif
 
+#if MIN_HOU_VERSION( 20, 0, 0 )
+		virtual int renderPick( RE_RenderContext r, const GR_DisplayOption *opt, unsigned int pick_type, GR_PickStyle pick_style, bool has_pick_map );
+#else
 		virtual void renderInstances( RE_Render *r, GR_RenderMode render_mode, GR_RenderFlags flags, const GR_DisplayOption *opt, const UT_Array<RE_MaterialPtr> *materials, int render_instance );
 		virtual int renderPick( RE_Render *r, const GR_DisplayOption *opt, unsigned int pick_type, GR_PickStyle pick_style, bool has_pick_map );
+#endif
 
 	private :
 
diff --git a/include/IECoreImage/DisplayDriverServer.h b/include/IECoreImage/DisplayDriverServer.h
index d66503f8bf..7a14ba830a 100644
--- a/include/IECoreImage/DisplayDriverServer.h
+++ b/include/IECoreImage/DisplayDriverServer.h
@@ -92,7 +92,6 @@ class IECOREIMAGE_API DisplayDriverServer : public IECore::RunTimeTyped
 		static const PortRange &registeredPortRange( const std::string &name );
 
 	private:
-
 		// Session class
 		// Takes care of one client connection.
 		class Session;
diff --git a/include/IECorePython/VectorTypedDataBinding.inl b/include/IECorePython/VectorTypedDataBinding.inl
index b26410c744..6fc7a74e8e 100644
--- a/include/IECorePython/VectorTypedDataBinding.inl
+++ b/include/IECorePython/VectorTypedDataBinding.inl
@@ -36,6 +36,7 @@
 #define IECOREPYTHON_VECTORTYPEDDATABINDING_INL
 
 #include "boost/python.hpp"
+#include "boost/numeric/conversion/cast.hpp"
 
 #include "IECorePython/IECoreBinding.h"
 #include "IECorePython/RunTimeTypedBinding.h"
diff --git a/include/IECoreScene/ShaderNetworkAlgo.h b/include/IECoreScene/ShaderNetworkAlgo.h
index 9f39280cd3..e7e7ef445c 100644
--- a/include/IECoreScene/ShaderNetworkAlgo.h
+++ b/include/IECoreScene/ShaderNetworkAlgo.h
@@ -63,17 +63,39 @@ IECORESCENE_API void removeUnusedShaders( ShaderNetwork *network );
 template<typename Visitor>
 void depthFirstTraverse( const ShaderNetwork *network, Visitor &&visitor, IECore::InternedString shader = "" );
 
-/// Replace connections between sub components of colors or vectors with connections to whole parameters
-/// on adapter shaders.  Currently uses the OSL shaders mx_pack_color and mx_swizzle_color_float as adapters.
-/// The newly created shaders will be labelled with a blind data so they can be identified.
-/// If `targetPrefix` is given, only translates connections to shaders with a type starting with this string
+/// Replaces connections between sub components of colors or vectors with
+/// connections to whole parameters on adapter shaders. If `targetPrefix` is
+/// given, only translates connections to shaders with a type starting with this
+/// string.
 IECORESCENE_API void addComponentConnectionAdapters( ShaderNetwork *network, std::string targetPrefix = "" );
 
-/// Find adapters that were created by addComponentConnectionAdapters ( based on the blind data label ),
-/// and remove them, replacing them with the original component connections
+/// Finds adapters that were created by addComponentConnectionAdapters, and
+/// removes them, replacing them with the original component connections.
 IECORESCENE_API void removeComponentConnectionAdapters( ShaderNetwork *network );
 
-/// The name of the boolean blindData label used by add/removeComponentConnectionAdapters
+/// Registers an adapter to split a component from a color or vector output, ready for connection into
+/// a scalar input. Used by `addComponentConnectionAdapters()`.
+///
+/// - `destinationShaderType` : The type prefix for the shader receiving the connection - e.g. "ai", "osl".
+/// - `component` : "r", "g", "b", "a", "x", "y", or "z".
+/// - `adapter` : The shader to be used as the adapter.
+/// - `inParameter` : The parameter that receives the color or vector input.
+/// - `outParameter` : The parameter that outputs the component.
+IECORESCENE_API void registerSplitAdapter( const std::string &destinationShaderType, IECore::InternedString component, const IECoreScene::Shader *adapter, IECore::InternedString inParameter, IECore::InternedString outParameter );
+/// Removes an adapter registration.
+IECORESCENE_API void deregisterSplitAdapter( const std::string &destinationShaderType, IECore::InternedString component );
+
+/// Registers an adapter to join multiple scalar components into a color or vector output. Used by `addComponentConnectionAdapters()`.
+///
+/// - `destinationShaderType` : The type prefix for the shader receiving the connection - e.g. "ai", "osl".
+/// - `destinationParameterType` : `(V2i|V3i|V2f|V3f|Color3f|Color4f)DataTypeId`.
+/// - `inParameters` : The parameters that receives the individual components of the vector or color.
+/// - `outParameter` : The parameter that outputs the vector or color.
+IECORESCENE_API void registerJoinAdapter( const std::string &destinationShaderType, IECore::TypeId destinationParameterType, const IECoreScene::Shader *adapter, const std::array<IECore::InternedString, 4> &inParameters, IECore::InternedString outParameter );
+/// Removes an adapter registration.
+IECORESCENE_API void deregisterJoinAdapter( const std::string &destinationShaderType, IECore::TypeId destinationParameterType );
+
+/// \deprecated
 IECORESCENE_API const IECore::InternedString &componentConnectionAdapterLabel();
 
 /// Converts various aspects of how shaders are stored to be ready to pass directly to OSL.
diff --git a/include/IECoreScene/private/PrimitiveAlgoUtils.h b/include/IECoreScene/private/PrimitiveAlgoUtils.h
index 15c107d5c8..3a2ee888f1 100644
--- a/include/IECoreScene/private/PrimitiveAlgoUtils.h
+++ b/include/IECoreScene/private/PrimitiveAlgoUtils.h
@@ -48,8 +48,15 @@
 
 #include "boost/format.hpp"
 
+#include <tbb/version.h>
+#if TBB_INTERFACE_VERSION >= 12040
+#include "oneapi/tbb/blocked_range.h"
+#include "oneapi/tbb/parallel_for.h"
+#include <oneapi/tbb/task_group.h>
+#else	
 #include "tbb/blocked_range.h"
 #include "tbb/parallel_for.h"
+#endif
 
 #include <unordered_set>
 #include <type_traits>
@@ -174,7 +181,120 @@ struct FillVectorFromValue
 /// Numeric & string like arrays, which contain elements which can be added to a std::set
 template<typename T> struct IsDeletablePrimVar : boost::mpl::or_< IECore::TypeTraits::IsStringVectorTypedData<T>, IECore::TypeTraits::IsNumericVectorTypedData<T> > {};
 
+#if TBB_INTERFACE_VERSION >= 12040
+template<typename T, typename S, typename P>
+class SplitTask
+{
+	private:
+		typedef typename P::Ptr Ptr;
+	public:
+		SplitTask(const std::vector<T> &segments, typename P::Ptr primitive, const S& splitter, const std::string &primvarName, std::vector<Ptr> &outputPrimitives, size_t offset, size_t depth, const IECore::Canceller *canceller )
+			: m_segments(segments), m_primitive(primitive), m_splitter(splitter), m_primvarName(primvarName), m_outputPrimitives( outputPrimitives ), m_offset(offset), m_depth(depth), m_canceller( canceller )
+		{
+		}
+
+		void execute()
+		{
+
+			if ( numPrimitives ( m_primitive.get() ) == 0 && !m_segments.empty() )
+			{
+				m_outputPrimitives[m_offset] = m_primitive;
+			}
+
+			if ( m_segments.size () == 0 )
+			{
+				return;
+			}
+
+			size_t offset = m_segments.size() / 2;
+			typename std::vector<T>::iterator mid = m_segments.begin() + offset;
+
+			IECoreScene::PrimitiveVariable segmentPrimVar = m_primitive->variables.find( m_primvarName )->second;
+
+			std::vector<T> lowerSegments (m_segments.begin(), mid);
+			std::vector<T> upperSegments (mid, m_segments.end());
+
+			std::set<T> lowerSegmentsSet ( m_segments.begin(), mid );
+			std::set<T> upperSegmentsSet (mid, m_segments.end());
+
+			const auto &readable = IECore::runTimeCast<IECore::TypedData<std::vector<T> > >( segmentPrimVar.data )->readable();
+
+			IECore::BoolVectorDataPtr deletionArrayLower = new IECore::BoolVectorData();
+			auto &writableLower = deletionArrayLower->writable();
+
+			IECore::BoolVectorDataPtr deletionArrayUpper = new IECore::BoolVectorData();
+			auto &writableUpper = deletionArrayUpper->writable();
+
+			size_t deleteCount = 0;
+			if( segmentPrimVar.indices )
+			{
+				auto &readableIndices = segmentPrimVar.indices->readable();
+				writableLower.resize( readableIndices.size() );
+				writableUpper.resize( readableIndices.size() );
+
+				for( size_t i = 0; i < readableIndices.size(); ++i )
+				{
+					size_t index = readableIndices[i];
+					writableLower[i] = lowerSegmentsSet.find( readable[index] ) == lowerSegmentsSet.end();
+					writableUpper[i] = upperSegmentsSet.find( readable[index] ) == upperSegmentsSet.end();
+
+					deleteCount += ( writableLower[i] && !lowerSegments.empty() ) || ( writableUpper[i] && !upperSegments.empty() ) ? 1 : 0;
+				}
+			}
+			else
+			{
+				writableLower.resize( readable.size() );
+				writableUpper.resize( readable.size() );
+
+				for( size_t i = 0; i < readable.size(); ++i )
+				{
+					writableLower[i] = lowerSegmentsSet.find( readable[i] ) == lowerSegmentsSet.end();
+					writableUpper[i] = upperSegmentsSet.find( readable[i] ) == upperSegmentsSet.end();
+					deleteCount += ( writableLower[i] && !lowerSegments.empty() ) || ( writableUpper[i] && !upperSegments.empty() ) ? 1 : 0;
+				}
+			}
+
+			if ( m_segments.size() == 1 && deleteCount == 0)
+			{
+				m_outputPrimitives[m_offset] = m_primitive;
+			}
+
+			IECoreScene::PrimitiveVariable::Interpolation i = splitPrimvarInterpolation( m_primitive.get() );
+
+			IECoreScene::PrimitiveVariable delPrimVarLower( i, deletionArrayLower );
+			Ptr a = m_splitter( m_primitive.get(), delPrimVarLower, false, m_canceller ) ;
+
+			IECoreScene::PrimitiveVariable delPrimVarUpper( i, deletionArrayUpper);
+			Ptr b = m_splitter( m_primitive.get(), delPrimVarUpper, false, m_canceller ) ;
+
+			size_t numSplits = 2;
+			
+ 			oneapi::tbb::task_group tg;
+	  		tg.run([=] {
+	  			SplitTask lowerTask(lowerSegments, a, m_splitter, m_primvarName, m_outputPrimitives, m_offset, m_depth + 1, m_canceller);
+            			lowerTask.execute();
+       			});
+
+        		tg.run([=] {
+            			SplitTask upperTask(upperSegments, b, m_splitter, m_primvarName, m_outputPrimitives, m_offset + offset, m_depth + 1, m_canceller);
+            			upperTask.execute();
+        		});
+
+        		tg.wait();
+		}
 
+	private:
+
+		std::vector<T> m_segments;
+		typename P::Ptr m_primitive;
+		const S &m_splitter;
+		std::string m_primvarName;
+		std::vector<Ptr> &m_outputPrimitives;
+		size_t m_offset;
+		size_t m_depth;
+		const IECore::Canceller *m_canceller;
+};
+#else
 template<typename T, typename S, typename P>
 class SplitTask : public tbb::task
 {
@@ -288,6 +408,7 @@ class SplitTask : public tbb::task
 		size_t m_depth;
 		const IECore::Canceller *m_canceller;
 };
+#endif
 
 template<typename P, typename S>
 class TaskSegmenter
@@ -322,6 +443,27 @@ class TaskSegmenter
 
 			ReturnType results( segmentsReadable.size() );
 
+#if TBB_INTERFACE_VERSION >= 12040
+			oneapi::tbb::task_group_context taskGroupContext(oneapi::tbb::task_group_context::isolated);
+            oneapi::tbb::task_group tg(taskGroupContext);
+
+            tg.run([&] {
+                        SplitTask<T, S, P> task(
+                        segmentsReadable,
+                        const_cast<P*>(m_primitive),
+                        m_splitter,
+                        m_primVarName,
+                        results,
+                        0,
+                        0,
+                        m_canceller
+                        );
+                        task.execute();
+                });
+
+            tg.wait();
+
+#else
 			tbb::task_group_context taskGroupContext( tbb::task_group_context::isolated );
 			SplitTask<T, S, P> *task = new( tbb::task::allocate_root( taskGroupContext ) ) SplitTask<T, S, P>(
 				segmentsReadable,
@@ -334,7 +476,7 @@ class TaskSegmenter
 				m_canceller
 			);
 			tbb::task::spawn_root_and_wait( *task );
-
+#endif
 			return results;
 
 		}
diff --git a/include/IECoreVDB/VDBObject.h b/include/IECoreVDB/VDBObject.h
index ea2f7eb650..972a1e015d 100644
--- a/include/IECoreVDB/VDBObject.h
+++ b/include/IECoreVDB/VDBObject.h
@@ -58,7 +58,7 @@ IECORE_PUSH_DEFAULT_VISIBILITY
 #endif
 IECORE_POP_DEFAULT_VISIBILITY
 
-#include "tbb/recursive_mutex.h"
+#include <mutex>
 
 #include <unordered_map>
 
diff --git a/src/IECore/DirNameParameter.cpp b/src/IECore/DirNameParameter.cpp
index c1e1119c33..2d057a5b7a 100644
--- a/src/IECore/DirNameParameter.cpp
+++ b/src/IECore/DirNameParameter.cpp
@@ -38,7 +38,11 @@
 
 #include "boost/algorithm/string/classification.hpp"
 #include "boost/algorithm/string/split.hpp"
-#include "boost/filesystem/convenience.hpp"
+#if BOOST_VERSION >= 108500
+#include <boost/filesystem/path.hpp>
+#else
+#include <boost/filesystem/convenience.hpp>
+#endif
 #include "boost/filesystem/operations.hpp"
 
 #include <algorithm>
diff --git a/src/IECore/FileNameParameter.cpp b/src/IECore/FileNameParameter.cpp
index 6dac471876..364ce33fc0 100644
--- a/src/IECore/FileNameParameter.cpp
+++ b/src/IECore/FileNameParameter.cpp
@@ -39,7 +39,11 @@
 
 #include "boost/algorithm/string/classification.hpp"
 #include "boost/algorithm/string/split.hpp"
-#include "boost/filesystem/convenience.hpp"
+#if BOOST_VERSION >= 108500
+#include <boost/filesystem/path.hpp>
+#else
+#include <boost/filesystem/convenience.hpp>
+#endif
 #include "boost/filesystem/operations.hpp"
 #include "boost/format.hpp"
 
@@ -87,8 +91,11 @@ bool FileNameParameter::valueValid( const Object *value, std::string *reason ) c
 	// extensions check
 	if( extensions().size() )
 	{
+#if BOOST_VERSION >= 108500
+		string ext = boost::filesystem::path(boost::filesystem::path( s->readable())).extension().string();
+#else
 		string ext = boost::filesystem::extension(boost::filesystem::path( s->readable()));
-
+#endif
 		const vector<string> &exts = extensions();
 		bool found = false;
 		for( vector<string>::const_iterator it=exts.begin(); it!=exts.end(); it++ )
diff --git a/src/IECore/FileSequenceFunctions.cpp b/src/IECore/FileSequenceFunctions.cpp
index 51f1d228e7..18d190208e 100644
--- a/src/IECore/FileSequenceFunctions.cpp
+++ b/src/IECore/FileSequenceFunctions.cpp
@@ -42,13 +42,17 @@
 #include "IECore/ReversedFrameList.h"
 
 #include "boost/algorithm/string.hpp"
-#include "boost/filesystem/convenience.hpp"
 #include "boost/filesystem/operations.hpp"
 #include "boost/filesystem/path.hpp"
 #include "boost/format.hpp"
 #include "boost/lexical_cast.hpp"
 #include "boost/regex.hpp"
-#include "boost/version.hpp"
+#include <boost/version.hpp>
+#if BOOST_VERSION >= 108500
+#include <boost/filesystem/directory.hpp>
+#else
+#include <boost/filesystem/convenience.hpp>
+#endif
 
 #include <algorithm>
 #include <cassert>
diff --git a/src/IECore/FileSequenceParameter.cpp b/src/IECore/FileSequenceParameter.cpp
index 399a49cebf..b85d53f1ae 100644
--- a/src/IECore/FileSequenceParameter.cpp
+++ b/src/IECore/FileSequenceParameter.cpp
@@ -41,7 +41,12 @@
 
 #include "boost/algorithm/string/classification.hpp"
 #include "boost/algorithm/string/split.hpp"
-#include "boost/filesystem/convenience.hpp"
+#if BOOST_VERSION >= 108500
+#include <boost/filesystem/operations.hpp>
+#include <boost/filesystem/path.hpp>
+#else
+#include <boost/filesystem/convenience.hpp>
+#endif
 
 #include <algorithm>
 #include <cassert>
@@ -130,7 +135,11 @@ bool FileSequenceParameter::valueValid( const Object *value, std::string *reason
 
 	if ( m_extensions.size() )
 	{
+#if BOOST_VERSION >= 108500
+		std::string ext = boost::filesystem::path( boost::filesystem::path( fileSequence->getFileName() ) ).extension().string();
+#else
 		std::string ext = boost::filesystem::extension( boost::filesystem::path( fileSequence->getFileName() ) );
+#endif
 		if ( ext.size() && ext[0] == '.' )
 		{
 			ext = ext.substr( 1, ext.size() - 1 );
diff --git a/src/IECore/FileSequenceVectorParameter.cpp b/src/IECore/FileSequenceVectorParameter.cpp
index 3939b09df7..061b616b19 100644
--- a/src/IECore/FileSequenceVectorParameter.cpp
+++ b/src/IECore/FileSequenceVectorParameter.cpp
@@ -41,7 +41,13 @@
 
 #include "boost/algorithm/string/classification.hpp"
 #include "boost/algorithm/string/split.hpp"
-#include "boost/filesystem/convenience.hpp"
+#include <boost/version.hpp>
+#if BOOST_VERSION >= 108500
+#include <boost/filesystem/operations.hpp>
+#include <boost/filesystem/path.hpp>
+#else
+#include <boost/filesystem/convenience.hpp>
+#endif
 
 #include <algorithm>
 #include <cassert>
@@ -131,7 +137,11 @@ bool FileSequenceVectorParameter::valueValid( const Object *value, std::string *
 
 		if ( m_extensions.size() )
 		{
+#if BOOST_VERSION >= 108500
+			std::string ext = boost::filesystem::path( boost::filesystem::path( fileSequence->getFileName())).extension().string();
+#else
 			std::string ext = boost::filesystem::extension( boost::filesystem::path( fileSequence->getFileName() ) );
+#endif
 			if ( ext.size() && ext[0] == '.' )
 			{
 				ext = ext.substr( 1, ext.size() - 1 );
diff --git a/src/IECore/IndexedIO.cpp b/src/IECore/IndexedIO.cpp
index b6af9d3bb8..7cde691e9a 100644
--- a/src/IECore/IndexedIO.cpp
+++ b/src/IECore/IndexedIO.cpp
@@ -33,10 +33,14 @@
 //////////////////////////////////////////////////////////////////////////
 
 #include "IECore/IndexedIO.h"
-
 #include "IECore/Exception.h"
 
+#include <boost/version.hpp>
+#if BOOST_VERSION >= 108500
+#include "boost/filesystem/path.hpp"
+#else
 #include "boost/filesystem/convenience.hpp"
+#endif
 #include "boost/algorithm/string.hpp"
 
 #include <iostream>
@@ -76,7 +80,11 @@ IndexedIOPtr IndexedIO::create( const std::string &path, const IndexedIO::EntryI
 {
 	IndexedIOPtr result = nullptr;
 
+#if BOOST_VERSION >= 108500
+	std::string extension = fs::path(path).extension().string();
+#else
 	std::string extension = fs::extension(path);
+#endif
 	boost::to_lower( extension );
 
 	const CreatorMap &createFns = creators();
diff --git a/src/IECore/IndexedIOAlgo.cpp b/src/IECore/IndexedIOAlgo.cpp
index 50f809d95e..418a15ed83 100644
--- a/src/IECore/IndexedIOAlgo.cpp
+++ b/src/IECore/IndexedIOAlgo.cpp
@@ -33,8 +33,12 @@
 //////////////////////////////////////////////////////////////////////////
 
 #include "IECore/IndexedIOAlgo.h"
-
+#include <tbb/version.h>
+#if TBB_INTERFACE_VERSION >= 12140
+#include <oneapi/tbb/task_group.h>
+#else
 #include "tbb/task.h"
+#endif
 
 #include <atomic>
 
@@ -227,6 +231,49 @@ void recursiveCopy( const IndexedIO *src, IndexedIO *dst )
 	}
 }
 
+#if TBB_INTERFACE_VERSION >= 12040
+template<template<typename, typename> class FileHandler, typename FileCallback>
+class FileTask
+{
+public:
+    FileTask(const IndexedIO* src, FileCallback& fileCallback)
+        : m_src(src), m_fileCallback(fileCallback)
+    {
+    }
+
+    void operator()() const
+    {
+        IndexedIO::EntryIDList fileNames;
+        m_src->entryIds(fileNames, IndexedIO::EntryType::File);
+
+        for (const auto& fileName : fileNames)
+        {
+            handleFile<FileHandler, FileCallback>(m_src, nullptr, fileName, m_fileCallback);
+        }
+
+        IndexedIO::EntryIDList directoryNames;
+        m_src->entryIds(directoryNames, IndexedIO::EntryType::Directory);
+
+        std::vector<ConstIndexedIOPtr> childDirectories;
+        childDirectories.reserve(directoryNames.size());
+        for (const auto& directoryName : directoryNames)
+        {
+            childDirectories.push_back(m_src->subdirectory(directoryName, IndexedIO::ThrowIfMissing));
+        }
+
+        oneapi::tbb::task_group tg;
+        for (const auto& childDirectory : childDirectories)
+        {
+            tg.run(FileTask(childDirectory.get(), m_fileCallback));
+        }
+        tg.wait();
+    }
+
+private:
+    const IndexedIO* m_src;
+    FileCallback& m_fileCallback;
+};
+#else
 //! Task for traversing all files in parallel. New tasks are spawned for each directory
 template<template<typename, typename> class FileHandler, typename FileCallback>
 class FileTask : public tbb::task
@@ -280,7 +327,7 @@ class FileTask : public tbb::task
 		const IndexedIO *m_src;
 		FileCallback &m_fileCallback;
 };
-
+#endif
 } // namespace
 
 namespace IECore
@@ -293,6 +340,26 @@ void copy( const IndexedIO *src, IndexedIO *dst )
 	::recursiveCopy( src, dst );
 }
 
+#if TBB_INTERFACE_VERSION >= 12140
+FileStats<size_t> parallelReadAll(const IndexedIO* src)
+{
+    FileStats<std::atomic<size_t>> fileStats;
+
+    auto fileCallback = [&fileStats](size_t numBytes)
+    {
+        fileStats.addBlock(numBytes);
+    };
+
+    oneapi::tbb::task_group tg;
+    tg.run([src, &fileCallback, &tg]()
+    {
+    	FileTask<Reader, decltype(fileCallback)>(src, fileCallback);
+    });
+    tg.wait();
+
+    return fileStats;
+}
+#else
 FileStats<size_t> parallelReadAll( const IndexedIO *src )
 {
 	FileStats<std::atomic<size_t> > fileStats;
@@ -307,6 +374,6 @@ FileStats<size_t> parallelReadAll( const IndexedIO *src )
 	tbb::task::spawn_root_and_wait( *task );
 	return fileStats;
 }
-
+#endif
 } // IndexedIOAlgo
 } // IECore
diff --git a/src/IECore/PathParameter.cpp b/src/IECore/PathParameter.cpp
index 3743376e09..c05c6cdab9 100644
--- a/src/IECore/PathParameter.cpp
+++ b/src/IECore/PathParameter.cpp
@@ -38,7 +38,12 @@
 
 #include "boost/algorithm/string/classification.hpp"
 #include "boost/algorithm/string/split.hpp"
-#include "boost/filesystem/convenience.hpp"
+#include <boost/version.hpp>
+#if BOOST_VERSION >= 108500
+#include <boost/filesystem/path.hpp>
+#else
+#include <boost/filesystem/convenience.hpp>
+#endif
 #include "boost/filesystem/operations.hpp"
 
 #include <algorithm>
diff --git a/src/IECore/PathVectorParameter.cpp b/src/IECore/PathVectorParameter.cpp
index 1a76aea167..9e5cc16186 100644
--- a/src/IECore/PathVectorParameter.cpp
+++ b/src/IECore/PathVectorParameter.cpp
@@ -38,7 +38,11 @@
 
 #include "boost/algorithm/string/classification.hpp"
 #include "boost/algorithm/string/split.hpp"
-#include "boost/filesystem/convenience.hpp"
+#if BOOST_VERSION >= 108500
+#include <boost/filesystem/path.hpp>
+#else
+#include <boost/filesystem/convenience.hpp>
+#endif
 #include "boost/filesystem/operations.hpp"
 
 #include <algorithm>
diff --git a/src/IECore/Reader.cpp b/src/IECore/Reader.cpp
index 40e7d06d3c..7ddf1735d3 100644
--- a/src/IECore/Reader.cpp
+++ b/src/IECore/Reader.cpp
@@ -40,7 +40,11 @@
 
 #include "boost/algorithm/string/classification.hpp"
 #include "boost/algorithm/string/split.hpp"
-#include "boost/filesystem/convenience.hpp"
+#if BOOST_VERSION >= 108500
+#include <boost/filesystem/path.hpp>
+#else
+#include <boost/filesystem/convenience.hpp>
+#endif
 
 using namespace std;
 using namespace IECore;
@@ -84,7 +88,11 @@ ReaderPtr Reader::create( const std::string &fileName )
 	bool knownExtension = false;
 	ExtensionsToFnsMap *m = extensionsToFns();
 	assert( m );
+#if BOOST_VERSION >= 108500
+	string ext = path(boost::filesystem::path(fileName)).extension().string();
+#else
 	string ext = extension(boost::filesystem::path(fileName));
+#endif
 	if( ext!="" )
 	{
 		ExtensionsToFnsMap::const_iterator it = m->find( ext );
diff --git a/src/IECore/SearchPath.cpp b/src/IECore/SearchPath.cpp
index 372e393565..e7f6e3cdf8 100644
--- a/src/IECore/SearchPath.cpp
+++ b/src/IECore/SearchPath.cpp
@@ -34,6 +34,7 @@
 
 #include "IECore/SearchPath.h"
 
+#include "boost/version.hpp"
 #include "boost/filesystem/operations.hpp"
 #include "boost/tokenizer.hpp"
 
@@ -106,6 +107,18 @@ std::string SearchPath::getPaths( const std::string &separator ) const
 boost::filesystem::path SearchPath::find( const boost::filesystem::path &file ) const
 {
 	// if it's a full path then there's no need to do any searching
+#if BOOST_VERSION >= 108500
+    if (file.is_absolute()) {
+		if( exists( file ) )
+		{
+			return file;
+		}
+		else
+		{
+			return "";
+		}
+	}
+#else
 	if( file.is_complete() )
 	{
 		if( exists( file ) )
@@ -117,7 +130,7 @@ boost::filesystem::path SearchPath::find( const boost::filesystem::path &file )
 			return "";
 		}
 	}
-
+#endif
 	// do some searching
 	for( list<path>::const_iterator it = paths.begin(); it!=paths.end(); it++ )
 	{
diff --git a/src/IECore/Version.cpp b/src/IECore/Version.cpp
new file mode 100644
index 0000000000..5e6e0aa6a9
--- /dev/null
+++ b/src/IECore/Version.cpp
@@ -0,0 +1,74 @@
+//////////////////////////////////////////////////////////////////////////
+//
+//  Copyright (c) 2024, Cinesite VFX Ltd. All rights reserved.
+//
+//  Redistribution and use in source and binary forms, with or without
+//  modification, are permitted provided that the following conditions are
+//  met:
+//
+//     * Redistributions of source code must retain the above copyright
+//       notice, this list of conditions and the following disclaimer.
+//
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+//     * Neither the name of Image Engine Design nor the names of any
+//       other contributors to this software may be used to endorse or
+//       promote products derived from this software without specific prior
+//       written permission.
+//
+//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+//  IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+//  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+//  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+//  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+//  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+//  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+//  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+//  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+//  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+//  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+//////////////////////////////////////////////////////////////////////////
+
+#include "IECore/Version.h"
+
+#include "boost/format.hpp"
+
+int IECore::milestoneVersion()
+{
+	return CORTEX_MILESTONE_VERSION;
+}
+
+int IECore::majorVersion()
+{
+	return CORTEX_MAJOR_VERSION;
+}
+
+int IECore::minorVersion()
+{
+	return CORTEX_MINOR_VERSION;
+}
+
+int IECore::patchVersion()
+{
+	return CORTEX_PATCH_VERSION;
+}
+
+int IECore::compatibilityVersion()
+{
+	return CORTEX_COMPATIBILITY_VERSION;
+}
+
+const std::string &IECore::compatibilityVersionString()
+{
+	static std::string v = boost::str( boost::format( "%d.%d" ) % milestoneVersion() % majorVersion() );
+	return v;
+}
+
+const std::string &IECore::versionString()
+{
+	static std::string v = boost::str( boost::format( "%d.%d.%d.%d" ) % milestoneVersion() % majorVersion() % minorVersion() % patchVersion() );
+	return v;
+}
diff --git a/src/IECore/Writer.cpp b/src/IECore/Writer.cpp
index 151b6a7607..1ce200c25a 100644
--- a/src/IECore/Writer.cpp
+++ b/src/IECore/Writer.cpp
@@ -41,7 +41,13 @@
 
 #include "boost/algorithm/string/classification.hpp"
 #include "boost/algorithm/string/split.hpp"
-#include "boost/filesystem/convenience.hpp"
+#include <boost/version.hpp>
+#if BOOST_VERSION >= 108500
+#include <boost/filesystem/path.hpp>
+#else
+#include <boost/filesystem/convenience.hpp>
+#endif
+
 
 #include <cassert>
 
@@ -116,8 +122,11 @@ void Writer::registerWriter( const std::string &extensions, CanWriteFn canWrite,
 
 WriterPtr Writer::create( ObjectPtr object, const std::string &fileName )
 {
+#if BOOST_VERSION >= 108500
+	string ext = path(boost::filesystem::path(fileName)).extension().string();
+#else
 	string ext = extension(boost::filesystem::path(fileName));
-
+#endif
 	ExtensionsToFnsMap *m = extensionsToFns();
 	assert( m );
 	ExtensionsToFnsMap::const_iterator it = m->find( ext );
@@ -146,8 +155,11 @@ WriterPtr Writer::create( ObjectPtr object, const std::string &fileName )
 
 WriterPtr Writer::create( const std::string &fileName )
 {
+#if BOOST_VERSION >= 108500
+	string ext = path(boost::filesystem::path(fileName)).extension().string();
+#else
 	string ext = extension(boost::filesystem::path(fileName));
-
+#endif
 	ExtensionsToFnsMap *m = extensionsToFns();
 	ExtensionsToFnsMap::const_iterator it = m->find( ext );
 
diff --git a/src/IECoreGL/DeferredRendererImplementation.cpp b/src/IECoreGL/DeferredRendererImplementation.cpp
index 13bc67b499..3c257ea1d7 100644
--- a/src/IECoreGL/DeferredRendererImplementation.cpp
+++ b/src/IECoreGL/DeferredRendererImplementation.cpp
@@ -47,8 +47,14 @@
 
 #include "boost/noncopyable.hpp"
 
+#include <tbb/version.h>
+#if TBB_INTERFACE_VERSION >= 12040
+#include <oneapi/tbb/task_arena.h>
+#include <oneapi/tbb/task_group.h>
+#else
 #include "tbb/task.h"
 #include "tbb/task_scheduler_init.h"
+#endif
 
 #include <cassert>
 
@@ -354,6 +360,59 @@ struct DeferredRendererImplementation::ScopedRenderContext : private boost::nonc
 		const char *m_msgContext;
 };
 
+#if TBB_INTERFACE_VERSION >= 12040
+class DeferredRendererImplementation::ProceduralTask : private boost::noncopyable
+{
+	public:
+
+		ProceduralTask( DeferredRendererImplementation &renderer, IECoreScene::Renderer::ProceduralPtr proc, IECoreScene::RendererPtr param ) :
+			m_renderer(renderer), m_procedural(proc), m_param(param)
+		{
+			m_numSubtasks = 0;
+			RenderContext *curContext = m_renderer.currentContext();
+
+			// create a RenderContext for a new Procedural based on the current context
+			StatePtr completeState = new State( false );
+			for ( StateStack::iterator it = curContext->stateStack.begin(); it != curContext->stateStack.end(); it++ )
+			{
+				completeState->add( *it );
+			}
+			m_proceduralContext = new RenderContext();
+			m_proceduralContext->localTransform = curContext->localTransform;
+			m_proceduralContext->transformStack.push( curContext->transformStack.top() );
+			m_proceduralContext->stateStack.push_back( completeState );
+			m_proceduralContext->groupStack.push( curContext->groupStack.top() );
+		}
+
+		~ProceduralTask()
+		{
+		}
+
+    	void execute() {
+        	oneapi::tbb::task_group taskGroup;
+
+        	// Activate the render context on the task's thread.
+        	ScopedRenderContext scopedProceduralContext(m_proceduralContext, m_renderer, "DeferredRendererImplementation::ProceduralTask::execute");
+        	m_procedural->render(m_param.get());
+
+       	 	taskGroup.wait();
+    	}
+
+    	void addSubtask(std::function<void()> subtask) {
+        	m_taskGroup.run(subtask);
+        	m_numSubtasks++;
+    	}
+
+private:
+    	std::atomic<unsigned int> m_numSubtasks;
+    	RenderContextPtr m_proceduralContext;
+    	DeferredRendererImplementation& m_renderer;
+    	IECoreScene::Renderer::ProceduralPtr m_procedural;
+    	IECoreScene::RendererPtr m_param;
+   	 	oneapi::tbb::task_group m_taskGroup;
+
+};
+#else
 class DeferredRendererImplementation::ProceduralTask : public tbb::task, private boost::noncopyable
 {
 	public:
@@ -417,9 +476,63 @@ class DeferredRendererImplementation::ProceduralTask : public tbb::task, private
 		IECoreScene::RendererPtr m_param;
 		tbb::task_list *m_taskList;
 };
+#endif
 
-
-
+#if TBB_INTERFACE_VERSION >= 12040
+void DeferredRendererImplementation::addProcedural( IECoreScene::Renderer::ProceduralPtr proc, IECoreScene::RendererPtr renderer )
+{
+    bool visible = static_cast<CameraVisibilityStateComponent *>( getState( CameraVisibilityStateComponent::staticTypeId() ) )->value();
+    if( !visible )
+    {
+        return;
+    }
+
+    bool withThreads = static_cast<ProceduralThreadingStateComponent *>( getState( ProceduralThreadingStateComponent::staticTypeId() ) )->value();
+    if( withThreads )
+    {
+        bool mainProcedural = ( m_threadContextPool.size() == 0 );
+
+        if ( mainProcedural )
+        {
+            // Vytvoření task_arena pro řízení prostředí úloh
+            oneapi::tbb::task_arena arena;
+            arena.execute([&] {
+                // Vytvoření task_group pro správu skupiny úloh
+                oneapi::tbb::task_group tg;
+                tg.run([&] {
+                    // Spuštění procedurální úlohy
+                    proc->render( renderer.get() );
+                });
+                tg.wait();
+            });
+
+            // Kontrola, zda byly všechny kontexty vyčištěny
+            for ( const auto& context : m_threadContextPool )
+            {
+                if ( !context.empty() )
+                {
+                    IECore::msg( IECore::Msg::Error, "DeferredRendererImplementation::procedural", "Non empty thread render context detected!" );
+                }
+            }
+            m_threadContextPool.clear();
+        }
+        else
+        {
+            // Spuštění procedurální úlohy v rámci existující task_arena
+            oneapi::tbb::task_group tg;
+            tg.run([&] {
+                proc->render( renderer.get() );
+            });
+            tg.wait();
+        }
+    }
+    else
+    {
+        // Pokud není požadováno vlákno, provede se okamžitě
+        proc->render( renderer.get() );
+    }
+}
+#else
 void DeferredRendererImplementation::addProcedural( IECoreScene::Renderer::ProceduralPtr proc, IECoreScene::RendererPtr renderer )
 {
 	bool visible = static_cast<CameraVisibilityStateComponent *>( getState( CameraVisibilityStateComponent::staticTypeId() ) )->value();
@@ -478,6 +591,8 @@ void DeferredRendererImplementation::addProcedural( IECoreScene::Renderer::Proce
 	}
 }
 
+#endif
+
 ScenePtr DeferredRendererImplementation::scene()
 {
 	return m_scene;
diff --git a/src/IECoreGL/Selector.cpp b/src/IECoreGL/Selector.cpp
index 53dd5d063a..79dba969c8 100644
--- a/src/IECoreGL/Selector.cpp
+++ b/src/IECoreGL/Selector.cpp
@@ -53,8 +53,12 @@
 #include "IECore/MessageHandler.h"
 
 #include "boost/format.hpp"
-#include "boost/timer.hpp"
-
+#include <boost/version.hpp>
+#if BOOST_VERSION >= 107000
+#include <boost/timer/timer.hpp>
+#else
+#include <boost/timer.hpp>
+#endif
 using namespace IECoreGL;
 
 //////////////////////////////////////////////////////////////////////////
diff --git a/src/IECoreHoudini/DetailSplitter.cpp b/src/IECoreHoudini/DetailSplitter.cpp
index e6d3b92d90..24a594276f 100644
--- a/src/IECoreHoudini/DetailSplitter.cpp
+++ b/src/IECoreHoudini/DetailSplitter.cpp
@@ -58,6 +58,7 @@
 #include "boost/algorithm/string/join.hpp"
 #include "boost/algorithm/string/replace.hpp"
 #include "boost/regex.hpp"
+#include "boost/container_hash/hash.hpp"
 
 #include "tbb/tbb.h"
 
diff --git a/src/IECoreHoudini/GEO_CobIOTranslator.cpp b/src/IECoreHoudini/GEO_CobIOTranslator.cpp
index 7e436f250c..0aa1639285 100644
--- a/src/IECoreHoudini/GEO_CobIOTranslator.cpp
+++ b/src/IECoreHoudini/GEO_CobIOTranslator.cpp
@@ -46,6 +46,8 @@
 
 #include "UT/UT_Version.h"
 
+#include <fstream>
+
 using namespace IECore;
 using namespace IECoreHoudini;
 
diff --git a/src/IECoreHoudini/GEO_CortexPrimitive.cpp b/src/IECoreHoudini/GEO_CortexPrimitive.cpp
index 76c16bab28..47149602c0 100644
--- a/src/IECoreHoudini/GEO_CortexPrimitive.cpp
+++ b/src/IECoreHoudini/GEO_CortexPrimitive.cpp
@@ -288,7 +288,11 @@ void GEO_CortexPrimitive::reverse()
 {
 }
 
+#if MIN_HOU_VERSION( 19, 5, 0 )
+bool GEO_CortexPrimitive::getBBox( UT_BoundingBox *bbox ) const
+#else
 int GEO_CortexPrimitive::getBBox( UT_BoundingBox *bbox ) const
+#endif
 {
 	if ( !m_object )
 	{
@@ -323,6 +327,13 @@ UT_Vector3 GEO_CortexPrimitive::computeNormal() const
 	return UT_Vector3( 0, 0, 0 );
 }
 
+#if MIN_HOU_VERSION( 19, 5, 0 )
+UT_Vector3D GEO_CortexPrimitive::computeNormalD() const
+{
+	return UT_Vector3D( 0, 0, 0 );
+}
+#endif
+
 int GEO_CortexPrimitive::detachPoints( GA_PointGroup &grp )
 {
 #if UT_MAJOR_VERSION_INT >= 16
@@ -669,6 +680,12 @@ void GEO_CortexPrimitive::normal( NormalComp &output ) const
 {
 }
 
+#if MIN_HOU_VERSION( 19, 5, 0 )
+void GEO_CortexPrimitive::normal( NormalCompD &output ) const
+{
+}
+#endif
+
 #ifdef GA_PRIMITIVE_VERTEXLIST
 
 bool GEO_CortexPrimitive::saveVertexArray( UT_JSONWriter &w, const GA_SaveMap &map ) const
diff --git a/src/IECoreHoudini/GR_CortexPrimitive.cpp b/src/IECoreHoudini/GR_CortexPrimitive.cpp
index 7e471e62fd..c756fdb36c 100644
--- a/src/IECoreHoudini/GR_CortexPrimitive.cpp
+++ b/src/IECoreHoudini/GR_CortexPrimitive.cpp
@@ -55,6 +55,10 @@
 #include "IECore/SimpleTypedData.h"
 
 #include "RE/RE_Render.h"
+#if MIN_HOU_VERSION( 20, 0, 0 )
+#include "RE/RE_RenderContext.h"
+#endif
+
 #include "UT/UT_Version.h"
 
 #if UT_MAJOR_VERSION_INT >= 14
@@ -124,7 +128,11 @@ void GR_CortexPrimitive::resetPrimitives()
 	m_renderable = 0;
 }
 
+#if MIN_HOU_VERSION( 20, 0, 0 )
+void GR_CortexPrimitive::update( RE_RenderContext r, const GT_PrimitiveHandle &primh, const GR_UpdateParms &p )
+#else
 void GR_CortexPrimitive::update( RE_Render *r, const GT_PrimitiveHandle &primh, const GR_UpdateParms &p )
+#endif
 {
 
 #if UT_MAJOR_VERSION_INT >= 15
@@ -185,14 +193,12 @@ void GR_CortexPrimitive::update( RE_Render *r, const GT_PrimitiveHandle &primh,
 	m_scene->setCamera( 0 ); // houdini will be providing the camera
 }
 
-#if UT_MAJOR_VERSION_INT >= 16
-
-void GR_CortexPrimitive::render( RE_Render* r, GR_RenderMode render_mode, GR_RenderFlags flags, GR_DrawParms dp )
-
+#if MIN_HOU_VERSION( 20, 0, 0 )
+void GR_CortexPrimitive::render( RE_RenderContext r, GR_RenderMode render_mode, GR_RenderFlags flags, GR_DrawParms dp )
+#elif UT_MAJOR_VERSION_INT >= 16
+void GR_CortexPrimitive::render( RE_Render *r, GR_RenderMode render_mode, GR_RenderFlags flags, GR_DrawParms dp )
 #else
-
 void GR_CortexPrimitive::render( RE_Render *r, GR_RenderMode render_mode, GR_RenderFlags flags, const GR_DisplayOption *opt, const UT_Array<RE_MaterialPtr> *materials )
-
 #endif
 {
 	if ( !m_scene )
@@ -276,6 +282,14 @@ void GR_CortexPrimitive::render( RE_Render *r, GR_RenderMode render_mode, GR_Ren
 	}
 }
 
+#if MIN_HOU_VERSION( 20, 0, 0 )
+int GR_CortexPrimitive::renderPick( RE_RenderContext r, const GR_DisplayOption *opt, unsigned int pick_type, GR_PickStyle pick_style, bool has_pick_map )
+{
+	// return 0 to indicate we don't support component picking
+	return 0;
+}
+
+#else
 void GR_CortexPrimitive::renderInstances( RE_Render *r, GR_RenderMode render_mode, GR_RenderFlags flags, const GR_DisplayOption *opt, const  UT_Array<RE_MaterialPtr> *materials, int render_instance )
 {
 	/// \todo: implement this to support instanced rendering.
@@ -294,6 +308,7 @@ int GR_CortexPrimitive::renderPick( RE_Render *r, const GR_DisplayOption *opt, u
 	// return 0 to indicate we don't support component picking
 	return 0;
 }
+#endif
 
 IECoreGL::StatePtr GR_CortexPrimitive::g_lit = 0;
 IECoreGL::StatePtr GR_CortexPrimitive::g_shaded = 0;
diff --git a/src/IECoreImage/ClientDisplayDriver.cpp b/src/IECoreImage/ClientDisplayDriver.cpp
index 6358c0ff3f..e14c465301 100644
--- a/src/IECoreImage/ClientDisplayDriver.cpp
+++ b/src/IECoreImage/ClientDisplayDriver.cpp
@@ -42,7 +42,13 @@
 // This header needs to be here so that on Windows it doesn't fail with
 // winsock2.h included more than once, and under the above include so that it
 // doesn't fail on macOS as intrusive_ptr needs to be defined via RefCounted.h
-#include "boost/asio.hpp"
+#include <boost/version.hpp>
+#if BOOST_VERSION >= 106600
+#include <boost/asio.hpp>
+#include <boost/asio/io_context.hpp>
+#else
+#include <boost/asio/io_service.hpp>
+#endif
 
 #include "IECoreImage/Private/DisplayDriverServerHeader.h"
 
@@ -73,7 +79,11 @@ class ClientDisplayDriver::PrivateData : public RefCounted
 			m_socket.close();
 		}
 
+#if BOOST_VERSION >= 106600
+		boost::asio::io_context m_service;
+#else
 		boost::asio::io_service m_service;
+#endif
 		std::string m_host;
 		std::string m_port;
 		bool m_scanLineOrderOnly;
@@ -96,11 +106,33 @@ ClientDisplayDriver::ClientDisplayDriver( const Imath::Box2i &displayWindow, con
 	m_data->m_host = displayHostData->readable();
 	m_data->m_port = displayPortData->readable();
 
+#if BOOST_VERSION >= 106600
+	boost::asio::io_context io_context;
+	tcp::resolver resolver(io_context);
+	boost::system::error_code error;
+	auto endpoints = resolver.resolve(m_data->m_host, m_data->m_port, error);
+	
+	if (!error)
+	{
+	    error = boost::asio::error::host_not_found;
+   	 	for (auto it = endpoints.begin(); it != endpoints.end() && error; ++it)
+    	{
+        	m_data->m_socket.close();
+        	m_data->m_socket.connect(*it, error);
+    	}
+	}
+
+	if (error)
+	{
+    	throw Exception("Could not connect to remote display driver server: " + error.message());
+	}
+#else
 	tcp::resolver resolver(m_data->m_service);
 	tcp::resolver::query query(m_data->m_host, m_data->m_port);
 
 	boost::system::error_code error;
 	tcp::resolver::iterator iterator = resolver.resolve( query, error );
+
 	if( !error )
 	{
 		error = boost::asio::error::host_not_found;
@@ -114,6 +146,7 @@ ClientDisplayDriver::ClientDisplayDriver( const Imath::Box2i &displayWindow, con
 	{
 		throw Exception( std::string( "Could not connect to remote display driver server : " ) + error.message() );
 	}
+#endif
 
 	MemoryIndexedIOPtr io;
 	ConstCharVectorDataPtr buf;
diff --git a/src/IECoreImage/DisplayDriverServer.cpp b/src/IECoreImage/DisplayDriverServer.cpp
index c337225b76..dd3d023251 100644
--- a/src/IECoreImage/DisplayDriverServer.cpp
+++ b/src/IECoreImage/DisplayDriverServer.cpp
@@ -41,7 +41,14 @@
 // This header needs to be here so that on Windows it doesn't fail with
 // winsock2.h included more than once, and under the above include so that it
 // doesn't fail on macOS as intrusive_ptr needs to be defined via RefCounted.h
-#include "boost/asio.hpp"
+#include <boost/version.hpp>
+#if BOOST_VERSION >= 106600
+#include <boost/asio.hpp>
+#include <boost/asio/io_context.hpp>
+#else
+#include <boost/asio.hpp>
+#include <boost/asio/io_service.hpp>
+#endif
 
 #include "IECoreImage/Private/DisplayDriverServerHeader.h"
 
@@ -53,6 +60,9 @@
 #include "boost/bind/bind.hpp"
 
 #include <thread>
+#include <map>
+#include <utility>
+#include <optional>
 
 #include <fcntl.h>
 #ifndef _MSC_VER
@@ -70,6 +80,14 @@ IE_CORE_DEFINERUNTIMETYPED( DisplayDriverServer );
 namespace
 {
 
+struct MergeDriverInfo
+{
+	DisplayDriverPtr mergeDriver = nullptr;
+	int mergeCount = 0;
+};
+
+using MergeMap = std::map<int, MergeDriverInfo>;
+
 /* Set the FD_CLOEXEC flag for the given socket descriptor, so that it will not exist on child processes.*/
 static void fixSocketFlags( int socketDesc )
 {
@@ -91,11 +109,39 @@ static std::map<std::string, const DisplayDriverServer::PortRange> g_portRegistr
 
 } // namespace
 
+#if BOOST_VERSION >= 106600
 class DisplayDriverServer::Session : public RefCounted
 {
 	public:
 
-		Session( boost::asio::io_service& io_service );
+		Session( boost::asio::io_context& io_service, MergeMap& mergeMap );
+        ~Session() override;
+
+        boost::asio::ip::tcp::socket& socket();
+        void start();
+
+    private:
+
+  		void handleReadHeader( const boost::system::error_code& error );
+      	void handleReadOpenParameters( const boost::system::error_code& error );
+        void handleReadDataParameters( const boost::system::error_code& error );
+        void sendResult( DisplayDriverServerHeader::MessageType msg, size_t dataSize );
+        void sendException( const char *message );
+
+    private:
+		boost::asio::ip::tcp::socket m_socket;
+		DisplayDriverPtr m_displayDriver;
+		DisplayDriverServerHeader m_header;
+		CharVectorDataPtr m_buffer;
+  		MergeMap& m_mergeMap;
+		std::optional<int> m_mergeId;
+};
+#else
+class DisplayDriverServer::Session : public RefCounted
+{
+	public:
+
+		Session( boost::asio::io_service& io_service, MergeMap& mergeMap );
 		~Session() override;
 
 		boost::asio::ip::tcp::socket& socket();
@@ -114,7 +160,10 @@ class DisplayDriverServer::Session : public RefCounted
 		DisplayDriverPtr m_displayDriver;
 		DisplayDriverServerHeader m_header;
 		CharVectorDataPtr m_buffer;
+		MergeMap& m_mergeMap;
+		std::optional<int> m_mergeId;
 };
+#endif
 
 class DisplayDriverServer::PrivateData : public RefCounted
 {
@@ -122,9 +171,14 @@ class DisplayDriverServer::PrivateData : public RefCounted
 	public :
 
 		boost::asio::ip::tcp::endpoint m_endpoint;
+#if BOOST_VERSION >= 106600
+		boost::asio::io_context m_service;
+#else
 		boost::asio::io_service m_service;
+#endif
 		boost::asio::ip::tcp::acceptor m_acceptor;
 		std::thread m_thread;
+		MergeMap m_mergeMap;
 
 		PrivateData( DisplayDriverServer::Port portNumber ) :
 			m_service(),
@@ -196,7 +250,7 @@ DisplayDriverServer::DisplayDriverServer( DisplayDriverServer::Port portNumber )
 {
 	m_data = new DisplayDriverServer::PrivateData( portNumber );
 
-	DisplayDriverServer::SessionPtr newSession( new DisplayDriverServer::Session( m_data->m_service ) );
+	DisplayDriverServer::SessionPtr newSession( new DisplayDriverServer::Session( m_data->m_service, m_data->m_mergeMap ) );
 	m_data->m_acceptor.async_accept( newSession->socket(),
 			boost::bind( &DisplayDriverServer::handleAccept, this, newSession,
 			boost::asio::placeholders::error));
@@ -280,7 +334,7 @@ void DisplayDriverServer::handleAccept( DisplayDriverServer::SessionPtr session,
 {
 	if (!error)
 	{
-		DisplayDriverServer::SessionPtr newSession( new DisplayDriverServer::Session( m_data->m_service ) );
+		DisplayDriverServer::SessionPtr newSession( new DisplayDriverServer::Session( m_data->m_service, m_data->m_mergeMap ) );
 		m_data->m_acceptor.async_accept( newSession->socket(),
 				boost::bind( &DisplayDriverServer::handleAccept,  this, newSession,
 				boost::asio::placeholders::error));
@@ -291,11 +345,17 @@ void DisplayDriverServer::handleAccept( DisplayDriverServer::SessionPtr session,
 /*
  * DisplayDriverServer::Session functions
  */
-
-DisplayDriverServer::Session::Session( boost::asio::io_service& io_service ) :
-	m_socket( io_service ), m_displayDriver(nullptr), m_buffer( new CharVectorData( ) )
+#if BOOST_VERSION >= 106600
+DisplayDriverServer::Session::Session( boost::asio::io_context& io_service, MergeMap& mergeMap ) :
+ 	m_socket( io_service ), m_displayDriver(nullptr), m_buffer( new CharVectorData( ) ), m_mergeMap( mergeMap )
+{
+}
+#else
+DisplayDriverServer::Session::Session( boost::asio::io_service& io_service, MergeMap& mergeMap ) :
+	m_socket( io_service ), m_displayDriver(nullptr), m_buffer( new CharVectorData( ) ), m_mergeMap( mergeMap )
 {
 }
+#endif
 
 DisplayDriverServer::Session::~Session()
 {
@@ -363,7 +423,19 @@ void DisplayDriverServer::Session::handleReadHeader( const boost::system::error_
 		{
 			try
 			{
-				m_displayDriver->imageClose();
+				if ( !m_mergeId.has_value() )
+				{
+					m_displayDriver->imageClose();
+				}
+				else
+				{
+					auto &m = m_mergeMap.at(m_mergeId.value()); // Error out if not found
+					if ( --m.mergeCount <= 0 )
+					{
+						m_mergeMap.erase(m_mergeId.value());
+						m_displayDriver->imageClose();
+					}
+				}
 			}
 			catch ( std::exception &e )
 			{
@@ -424,8 +496,31 @@ void DisplayDriverServer::Session::handleReadOpenParameters( const boost::system
 		const StringData *displayType = parameters->member<StringData>( "remoteDisplayType", true /* throw if missing */ );
 
 		// create a displayDriver using the factory function.
-		m_displayDriver = DisplayDriver::create( displayType->readable(), displayWindow->readable(), dataWindow->readable(), channelNames->readable(), parameters );
+		if ( !parameters->member<IntData>( "displayDriverServer:mergeId", false ) )
+		{
+			m_displayDriver = DisplayDriver::create( displayType->readable(), displayWindow->readable(), dataWindow->readable(), channelNames->readable(), parameters );
+		}
+		else
+		{
+			m_mergeId = parameters->member<IntData>( "displayDriverServer:mergeId", false /* throw if missing */ )->readable();
 
+			// Check if merge ID in map, if not then create display driver and session count pair with merge ID.
+			auto &m = m_mergeMap[m_mergeId.value()];
+			if ( !m.mergeDriver )
+			{
+				const IntData *sessionClientsData = parameters->member<IntData>( "displayDriverServer:mergeClients", true /* throw if missing */ );
+				m.mergeDriver= DisplayDriver::create(
+						displayType->readable(),
+						displayWindow->readable(),
+						displayWindow->readable(), // For merge we want dataWindow = displayWindow
+						channelNames->readable(),
+						parameters
+						);
+				m.mergeCount = sessionClientsData->readable();
+			}
+			// Merge ID is now in map, so load the display driver.
+			m_displayDriver = m.mergeDriver;
+		}
 		scanLineOrder = m_displayDriver->scanLineOrderOnly();
 		acceptsRepeatedData = m_displayDriver->acceptsRepeatedData();
 	}
diff --git a/src/IECoreImage/OpenImageIOAlgo.cpp b/src/IECoreImage/OpenImageIOAlgo.cpp
index db023f8f3d..0d51eb338d 100644
--- a/src/IECoreImage/OpenImageIOAlgo.cpp
+++ b/src/IECoreImage/OpenImageIOAlgo.cpp
@@ -347,6 +347,14 @@ DataView::DataView( const IECore::Data *d, bool createUStrings )
 			type = TypeDesc::TypeInt;
 			data = static_cast<const IntData *>( d )->baseReadable();
 			break;
+		case UInt64DataTypeId :
+			type = TypeDesc::UINT64;
+			data = static_cast<const UInt64Data *>( d )->baseReadable();
+			break;
+		case Int64DataTypeId :
+			type = TypeDesc::INT64;
+			data = static_cast<const Int64Data *>( d )->baseReadable();
+			break;
 		case FloatDataTypeId :
 			type = TypeDesc::TypeFloat;
 			data = static_cast<const FloatData *>( d )->baseReadable();
@@ -471,6 +479,24 @@ DataView::DataView( const IECore::Data *d, bool createUStrings )
 			);
 			data = static_cast<const UIntVectorData *>( d )->baseReadable();
 			break;
+		case UInt64VectorDataTypeId :
+			type = TypeDesc(
+				TypeDesc::UINT64,
+				TypeDesc::SCALAR,
+				TypeDesc::NOSEMANTICS,
+				static_cast<const UInt64VectorData *>( d )->readable().size()
+			);
+			data = static_cast<const UInt64VectorData *>( d )->baseReadable();
+			break;
+		case Int64VectorDataTypeId :
+			type = TypeDesc(
+				TypeDesc::INT64,
+				TypeDesc::SCALAR,
+				TypeDesc::NOSEMANTICS,
+				static_cast<const Int64VectorData *>( d )->readable().size()
+			);
+			data = static_cast<const Int64VectorData *>( d )->baseReadable();
+			break;
 		case CharVectorDataTypeId :
 			type = TypeDesc(
 				TypeDesc::CHAR,
diff --git a/src/IECoreMaya/FromMayaInstancerConverter.cpp b/src/IECoreMaya/FromMayaInstancerConverter.cpp
index 7186132582..3b10778102 100644
--- a/src/IECoreMaya/FromMayaInstancerConverter.cpp
+++ b/src/IECoreMaya/FromMayaInstancerConverter.cpp
@@ -141,7 +141,7 @@ IECore::QuatfVectorDataPtr eulerToQuat( IECore::V3fVectorData *eulerData, Imath:
 
 	writableQuatData.reserve( readableEulerData.size() );
 
-	for( const auto rotation : readableEulerData )
+	for( const auto& rotation : readableEulerData )
 	{
 		float x = isDegrees ? IECore::degreesToRadians( rotation.x ) : rotation.x;
 		float y = isDegrees ? IECore::degreesToRadians( rotation.y ) : rotation.y;
diff --git a/src/IECoreMaya/FromMayaParticleConverter.cpp b/src/IECoreMaya/FromMayaParticleConverter.cpp
index 8b5adf6327..4545d26da2 100644
--- a/src/IECoreMaya/FromMayaParticleConverter.cpp
+++ b/src/IECoreMaya/FromMayaParticleConverter.cpp
@@ -305,7 +305,7 @@ IECoreScene::PrimitivePtr FromMayaParticleConverter::doPrimitiveConversion( MFnP
 
 				writableQuatData.reserve( readableEulerData.size() );
 
-				for( const auto rotation : readableEulerData )
+				for( const auto& rotation : readableEulerData )
 				{
 					float x = IECore::degreesToRadians( rotation.x );
 					float y = IECore::degreesToRadians( rotation.y );
diff --git a/src/IECoreMaya/SceneShapeUI.cpp b/src/IECoreMaya/SceneShapeUI.cpp
index 25fa55810c..35b840a9ad 100644
--- a/src/IECoreMaya/SceneShapeUI.cpp
+++ b/src/IECoreMaya/SceneShapeUI.cpp
@@ -521,7 +521,7 @@ bool SceneShapeUI::snap( MSelectInfo &snapInfo ) const
 	const std::vector<Imath::V3f> &vertices( pointData->readable() );
 
 	// Find the vertex that is closest to the snap point.
-	Imath::V3d closestVertex;
+	Imath::V3d closestVertex(0, 0, 0);
 	float closestDistance = std::numeric_limits<float>::max();
 
 	for( std::vector<Imath::V3f>::const_iterator it( vertices.begin() ); it != vertices.end(); ++it )
diff --git a/src/IECorePython/IECoreBinding.cpp b/src/IECorePython/IECoreBinding.cpp
index d0ee134754..ce5a4f9ed1 100644
--- a/src/IECorePython/IECoreBinding.cpp
+++ b/src/IECorePython/IECoreBinding.cpp
@@ -58,6 +58,14 @@ IECORE_POP_DEFAULT_VISIBILITY
 using namespace std;
 using namespace Imath;
 
+namespace
+{
+
+static std::string g_positiveInfString = "float( 'inf' )";
+static std::string g_negativeInfString = "-float( 'inf' )";
+
+}
+
 namespace IECorePython
 {
 
@@ -70,7 +78,18 @@ std::string repr<VEC>( VEC &x )\
 	s << "imath." << #VEC << "( ";\
 	for( unsigned i=0; i<VEC::dimensions(); i++ )\
 	{\
-		s << boost::lexical_cast<string>( x[i] );\
+		if constexpr( std::numeric_limits<VEC::BaseType>::has_infinity )\
+		{\
+			s << (\
+				x[i] == std::numeric_limits<VEC::BaseType>::infinity() ? g_positiveInfString :\
+				( x[i] == -std::numeric_limits<VEC::BaseType>::infinity() ? g_negativeInfString :\
+				boost::lexical_cast<std::string>( x[i] ) ) \
+			);\
+		}\
+		else\
+		{\
+			s << boost::lexical_cast<string>( x[i] );\
+		}\
 		if( i!=VEC::dimensions()-1 )\
 		{\
 			s << ", ";\
@@ -142,7 +161,18 @@ std::string repr<COL>( COL &x )\
 	s << "imath." << #COL << "( ";\
 	for( unsigned i=0; i<COL::dimensions(); i++ )\
 	{\
-		s << boost::lexical_cast<std::string>( x[i] );\
+		if constexpr( std::numeric_limits<COL::BaseType>::has_infinity )\
+		{\
+			s << (\
+				x[i] == std::numeric_limits<COL::BaseType>::infinity() ? g_positiveInfString :\
+				( x[i] == -std::numeric_limits<COL::BaseType>::infinity() ? g_negativeInfString :\
+				boost::lexical_cast<std::string>( x[i] ) ) \
+			);\
+		}\
+		else\
+		{\
+			s << boost::lexical_cast<string>( x[i] );\
+		}\
 		if( i!=COL::dimensions()-1 )\
 		{\
 			s << ", ";\
diff --git a/src/IECorePythonModule/TBBBinding.cpp b/src/IECorePythonModule/TBBBinding.cpp
index 970cca5b9e..bc3f8a7187 100644
--- a/src/IECorePythonModule/TBBBinding.cpp
+++ b/src/IECorePythonModule/TBBBinding.cpp
@@ -37,11 +37,14 @@
 #include "boost/python.hpp"
 
 #include "TBBBinding.h"
-
+#include "tbb/version.h"
+#if TBB_INTERFACE_VERSION >= 12040
+#include "oneapi/tbb/global_control.h"
+#else
 #include "tbb/task_scheduler_init.h"
-
 #define TBB_PREVIEW_GLOBAL_CONTROL 1
 #include "tbb/global_control.h"
+#endif
 
 #include <thread>
 
@@ -50,6 +53,9 @@ using namespace boost::python;
 namespace
 {
 
+#if TBB_INTERFACE_VERSION >= 12040
+// from onetbb was remove task_scheduler_init
+#else
 // Wraps task_scheduler_init so it can be used as a python
 // context manager.
 class TaskSchedulerInitWrapper : public tbb::task_scheduler_init
@@ -83,6 +89,7 @@ class TaskSchedulerInitWrapper : public tbb::task_scheduler_init
 		int m_maxThreads;
 
 };
+#endif
 
 class GlobalControlWrapper : public boost::noncopyable
 {
@@ -117,13 +124,16 @@ class GlobalControlWrapper : public boost::noncopyable
 
 void IECorePythonModule::bindTBB()
 {
+#if TBB_INTERFACE_VERSION >= 12040
+// from onetbb was remove task_scheduler_init
+#else
 	object tsi = class_<TaskSchedulerInitWrapper, boost::noncopyable>( "tbb_task_scheduler_init", no_init )
 		.def( init<int>( arg( "max_threads" ) = int( tbb::task_scheduler_init::automatic ) ) )
 		.def( "__enter__", &TaskSchedulerInitWrapper::enter, return_self<>() )
 		.def( "__exit__", &TaskSchedulerInitWrapper::exit )
 	;
 	tsi.attr( "automatic" ) = int( tbb::task_scheduler_init::automatic );
-
+#endif
 	class_<GlobalControlWrapper, boost::noncopyable> globalControl( "tbb_global_control", no_init );
 	{
 		scope globalControlScope = globalControl;
diff --git a/src/IECoreScene/MeshAlgoSplit.cpp b/src/IECoreScene/MeshAlgoSplit.cpp
index cab5e58412..587e3e3304 100644
--- a/src/IECoreScene/MeshAlgoSplit.cpp
+++ b/src/IECoreScene/MeshAlgoSplit.cpp
@@ -490,7 +490,8 @@ class Reindexer
 		m_newIndices( m_newIndicesData->writable() ),
 		m_blockSize( blockSize ),
 		m_fromOldIds( ( numOriginalIds - 1 ) / blockSize + 1 ),
-		m_numIdsUsed( 0 )
+		m_numIdsUsed( 0 ),
+		m_indicesComputed( false )
 	{
 		m_newIndices.reserve( numIndices );
 	}
@@ -510,21 +511,21 @@ class Reindexer
 			block = std::make_unique< std::vector<int> >( m_blockSize, -1 );
 		}
 
-		int &r = (*block)[ subIndex ];
-		if( r == -1 )
-		{
-			// Id isn't used yet, we need to set this location in the block, and use it
-			r = m_numIdsUsed;
-			m_numIdsUsed++;
-		}
+		// We initially record that this index is used just by marking it with a 0, against the background of -1.
+		// Once computeIndices is called, the 0 will be replaced with a new index, only counting indices that are
+		// used.
+		(*block)[ subIndex ] = 0;
 
-		m_newIndices.push_back( r );
+		m_newIndices.push_back( id );
+
+		m_indicesComputed = false;
 	}
 
 	// Don't add the index, but just test if it is a part of the reindex. If it is an
 	// id which has already been added, return the new id, otherwise return -1
 	inline int testIndex( int id )
 	{
+		computeIndices();
 		int blockId = id / m_blockSize;
 		int subIndex = id % m_blockSize;
 		auto &block = m_fromOldIds[ blockId ];
@@ -541,6 +542,7 @@ class Reindexer
 	// Get the new indices. Call after calling addIndex for every original index
 	IntVectorDataPtr getNewIndices()
 	{
+		computeIndices();
 		return m_newIndicesData;
 	}
 
@@ -550,6 +552,7 @@ class Reindexer
 	template <typename T >
 	void remapData( const std::vector<T> &in, std::vector<T> &out )
 	{
+		computeIndices();
 		out.resize( m_numIdsUsed );
 		for( unsigned int i = 0; i < m_fromOldIds.size(); i++ )
 		{
@@ -574,6 +577,7 @@ class Reindexer
 	// original id corresponding to each id of the output
 	void getDataRemapping( std::vector<int> &dataRemap )
 	{
+		computeIndices();
 		dataRemap.resize( m_numIdsUsed );
 		for( unsigned int i = 0; i < m_fromOldIds.size(); i++ )
 		{
@@ -595,6 +599,45 @@ class Reindexer
 	}
 
 private:
+
+	void computeIndices()
+	{
+		// Once indices have been added, and before using them, this function is called to
+		// compute the new indices.
+		if( m_indicesComputed )
+		{
+			return;
+		}
+
+		m_indicesComputed = true;
+
+		for( unsigned int blockId = 0; blockId < m_fromOldIds.size(); blockId++ )
+		{
+			auto &block = m_fromOldIds[ blockId ];
+			if( !block )
+			{
+				continue;
+			}
+
+			for( int i = 0; i < m_blockSize; i++ )
+			{
+				if( (*block)[i] != -1 )
+				{
+					(*block)[i] = m_numIdsUsed;
+					m_numIdsUsed++;
+				}
+			}
+		}
+
+		for( int &id : m_newIndices )
+		{
+			int blockId = id / m_blockSize;
+			int subIndex = id % m_blockSize;
+
+			id = (*m_fromOldIds[ blockId ])[subIndex];
+		}
+	}
+
 	// IntVectorData to hold the new indices
 	IntVectorDataPtr m_newIndicesData;
 	std::vector< int > &m_newIndices;
@@ -605,13 +648,16 @@ class Reindexer
 	// Store the mapping from old ids to new ids. The outer vector holds a unique_ptr for each
 	// block of m_blockSize ids in the original id range. These pointers are null if no ids from
 	// that block have been used. Once a block is used, it is allocated with a vector that is set
-	// to -1 for ids which have not been used, and the new corresponding id for ids which have been
-	// used
+	// to -1 for ids which have not been used, and zeros for ids which have been used.  When computeIndices()
+	// is called, all used elements get a new id assigned, relative to just the used ids.
 	std::vector< std::unique_ptr< std::vector< int > > > m_fromOldIds;
 
 	// How many unique ids have appeared in the indices added so far
 	int m_numIdsUsed;
 
+	// Whether we have yet computed the new indices for each used index
+	bool m_indicesComputed;
+
 };
 
 struct ResamplePrimitiveVariableFunctor
diff --git a/src/IECoreScene/SceneAlgo.cpp b/src/IECoreScene/SceneAlgo.cpp
index f71577fbd4..a13358f30a 100644
--- a/src/IECoreScene/SceneAlgo.cpp
+++ b/src/IECoreScene/SceneAlgo.cpp
@@ -39,7 +39,12 @@
 #include "IECoreScene/PointsPrimitive.h"
 #include "IECoreScene/SceneInterface.h"
 
+#include <tbb/version.h>
+#if TBB_INTERFACE_VERSION >= 12040
+#include <oneapi/tbb/task_group.h>
+#else
 #include "tbb/task.h"
+#endif
 
 #include <atomic>
 
@@ -49,6 +54,64 @@ using namespace IECoreScene;
 namespace
 {
 
+#if TBB_INTERFACE_VERSION >= 12040
+template<typename LocationFn>
+class Task
+{
+
+	public :
+
+		Task(
+			const SceneInterface *src, SceneInterface *dst, LocationFn &locationFn, double time, unsigned int flags
+		) : m_src( src ), m_dst( dst ), m_locationFn( locationFn ), m_time( time ), m_flags( flags )
+		{
+		}
+
+		~Task()
+		{
+		}
+
+        	void operator()() const
+        	{
+            		m_locationFn(m_src, m_dst, m_time, m_flags);
+
+            		SceneInterface::NameList childNames;
+            		m_src->childNames(childNames);
+
+            		std::vector<SceneInterfacePtr> childSceneInterfaces;
+            		childSceneInterfaces.reserve(childNames.size());
+
+            		std::vector<ConstSceneInterfacePtr> srcChildSceneInterfaces;
+            		srcChildSceneInterfaces.reserve(childNames.size());
+
+            		oneapi::tbb::task_group tg;
+            		for (const auto& childName : childNames)
+            		{
+                		SceneInterfacePtr dstChild = m_dst ? m_dst->child(childName, SceneInterface::CreateIfMissing) : nullptr;
+                		if (dstChild)
+                		{
+                    			childSceneInterfaces.push_back(dstChild);
+                	}
+
+                	ConstSceneInterfacePtr srcChild = m_src->child(childName);
+                	srcChildSceneInterfaces.push_back(srcChild);
+
+                	tg.run(Task(srcChild.get(), dstChild.get(), m_locationFn, m_time, m_flags));
+            	}
+
+            	tg.wait();
+        }
+
+	private :
+
+		const SceneInterface *m_src;
+		SceneInterface *m_dst;
+		LocationFn &m_locationFn;
+		double m_time;
+		unsigned int m_flags;
+
+};
+#else
 template<typename LocationFn>
 class Task : public tbb::task
 {
@@ -108,7 +171,7 @@ class Task : public tbb::task
 		unsigned int m_flags;
 
 };
-
+#endif
 template<typename T>
 struct CopyInfo
 {
@@ -257,6 +320,21 @@ SceneStats parallelReadAll( const SceneInterface *src, int startFrame, int endFr
 		copyInfos.pointCount += copyInfo.pointCount;
 	};
 
+#if TBB_INTERFACE_VERSION >= 12040
+	for (int f = startFrame; f <= endFrame; ++f)
+	{
+    	double time = f / frameRate;
+  	  	oneapi::tbb::task_group_context taskGroupContext(oneapi::tbb::task_group_context::isolated);
+    	oneapi::tbb::task_group tg(taskGroupContext);
+
+    	tg.run([=, &locationFn]() {
+        	Task<decltype(locationFn)> task(src, nullptr, locationFn, time, flags);
+       	 	task();
+    	});
+
+    	tg.wait();
+	}
+#else
 	for( int f = startFrame; f <= endFrame; ++f )
 	{
 		double time = f / frameRate;
@@ -264,7 +342,7 @@ SceneStats parallelReadAll( const SceneInterface *src, int startFrame, int endFr
 		Task<decltype( locationFn )> *task = new( tbb::task::allocate_root( taskGroupContext ) ) Task<decltype( locationFn )>( src, nullptr, locationFn, time, flags );
 		tbb::task::spawn_root_and_wait( *task );
 	}
-
+#endif
 	SceneStats stats;
 	stats["locations"] = locationCount;
 	stats["polygons"] = copyInfos.polygonCount;
@@ -293,4 +371,4 @@ void copy( const SceneInterface *src, SceneInterface *dst, int startFrame, int e
 
 } // SceneAlgo
 
-} // IECoreScene
\ No newline at end of file
+} // IECoreScene
diff --git a/src/IECoreScene/SceneInterface.cpp b/src/IECoreScene/SceneInterface.cpp
index 661d7ef276..6b9c710b67 100644
--- a/src/IECoreScene/SceneInterface.cpp
+++ b/src/IECoreScene/SceneInterface.cpp
@@ -34,7 +34,14 @@
 
 #include "IECoreScene/SceneInterface.h"
 
-#include "boost/filesystem/convenience.hpp"
+#include <boost/version.hpp>
+
+#if BOOST_VERSION >= 108500
+#include <boost/filesystem/path.hpp>
+#else
+#include <boost/filesystem/convenience.hpp>
+#endif
+
 #include "boost/tokenizer.hpp"
 #include "boost/algorithm/string.hpp"
 
@@ -104,7 +111,11 @@ SceneInterfacePtr SceneInterface::create( const std::string &path, IndexedIO::Op
 {
 	SceneInterfacePtr result = nullptr;
 
+#if BOOST_VERSION >= 108500
+	std::string extension = boost::filesystem::path(path).extension().string();
+#else
 	std::string extension = boost::filesystem::extension(path);
+#endif
 	boost::algorithm::to_lower( extension );
 	IndexedIO::OpenModeFlags openMode = IndexedIO::OpenModeFlags( mode & (IndexedIO::Read|IndexedIO::Write|IndexedIO::Append) );
 	std::pair< std::string, IndexedIO::OpenModeFlags > key( extension, openMode );
diff --git a/src/IECoreScene/ShaderNetworkAlgo.cpp b/src/IECoreScene/ShaderNetworkAlgo.cpp
index 578da65f2c..51230dc994 100644
--- a/src/IECoreScene/ShaderNetworkAlgo.cpp
+++ b/src/IECoreScene/ShaderNetworkAlgo.cpp
@@ -36,14 +36,17 @@
 
 #include "IECoreScene/ShaderNetworkAlgo.h"
 
+#include "IECore/DataAlgo.h"
 #include "IECore/SimpleTypedData.h"
 #include "IECore/StringAlgo.h"
 #include "IECore/SplineData.h"
+#include "IECore/TypeTraits.h"
 #include "IECore/VectorTypedData.h"
 #include "IECore/MessageHandler.h"
 
 #include "boost/algorithm/string/predicate.hpp"
 #include "boost/algorithm/string/replace.hpp"
+#include "boost/container/flat_map.hpp"
 #include "boost/regex.hpp"
 
 #include <unordered_map>
@@ -54,11 +57,9 @@ using namespace Imath;
 using namespace IECore;
 using namespace IECoreScene;
 
-namespace {
-
-BoolDataPtr g_trueData( new BoolData( true ) );
-
-}
+//////////////////////////////////////////////////////////////////////////
+// `addShaders()`
+//////////////////////////////////////////////////////////////////////////
 
 ShaderNetwork::Parameter ShaderNetworkAlgo::addShaders( ShaderNetwork *network, const ShaderNetwork *sourceNetwork, bool connections )
 {
@@ -91,6 +92,10 @@ ShaderNetwork::Parameter ShaderNetworkAlgo::addShaders( ShaderNetwork *network,
 	);
 }
 
+//////////////////////////////////////////////////////////////////////////
+// `removeUnusedShaders()`
+//////////////////////////////////////////////////////////////////////////
+
 namespace
 {
 
@@ -126,55 +131,199 @@ void ShaderNetworkAlgo::removeUnusedShaders( ShaderNetwork *network )
 	}
 }
 
+//////////////////////////////////////////////////////////////////////////
+// Component connection adapters
+//////////////////////////////////////////////////////////////////////////
+
 namespace
 {
 
-const InternedString g_swizzleHandle( "swizzle" );
-const InternedString g_packHandle( "pack" );
+const InternedString g_splitAdapterHandle( "splitAdapter" );
+const InternedString g_splitAdapterComponent( "splitAdapter:component" );
+const InternedString g_splitAdapterInParameter( "splitAdapter:inParameter" );
+const InternedString g_splitAdapterOutParameter( "splitAdapter:outParameter" );
+
+const InternedString g_joinAdapterHandle( "joinAdapter" );
+const InternedString g_joinAdapterInParameters( "joinAdapter:inParameters" );
+const InternedString g_joinAdapterOutParameter( "joinAdapter:outParameter" );
+
+const boost::regex g_componentRegex( "^(.*)\\.([rgbaxyz])$" );
+array<string, 3> g_vectorComponents = { "x", "y", "z" };
+array<string, 4> g_colorComponents = { "r", "g", "b", "a" };
+BoolDataPtr g_trueData( new BoolData( true ) );
+
 const InternedString g_inParameterName( "in" );
 const InternedString g_outParameterName( "out" );
-const InternedString g_packInParameterNames[4] = { "in1", "in2", "in3", "in4" };
-const boost::regex g_componentRegex( "^(.*)\\.([rgbaxyz])$" );
-const boost::regex g_splineElementRegex( "^(.*)\\[(.*)\\]\\.y(.*)$" );
-const boost::regex g_splineAdapterInRegex( "^in([0-9]+)(\\..*)?$" );
-const char *g_vectorComponents[3] = { "x", "y", "z" };
-const char *g_colorComponents[4] = { "r", "g", "b", "a" };
+array<InternedString, 4> g_packInParameterNames = { "in1", "in2", "in3", "in4" };
 
-ShaderNetwork::Parameter convertComponentSuffix( const ShaderNetwork::Parameter &parameter, const std::string &suffix )
+struct SplitAdapter
 {
-	int index;
-	auto it = find( begin( g_vectorComponents ), end( g_vectorComponents ), suffix );
-	if( it != end( g_vectorComponents ) )
+	InternedString component;
+	ConstShaderPtr shader;
+	InternedString inParameter;
+	InternedString outParameter;
+};
+
+// One adapter for each output component.
+using ComponentsToSplitAdapters = boost::container::flat_map<InternedString, SplitAdapter>;
+using SplitAdapterMap = std::unordered_map<string, ComponentsToSplitAdapters>;
+
+SplitAdapterMap &splitAdapters()
+{
+	static SplitAdapterMap g_map;
+	return g_map;
+}
+
+const SplitAdapter &findSplitAdapter( const std::string &destinationShaderType, InternedString component )
+{
+	const auto &map = splitAdapters();
+	const string typePrefix = destinationShaderType.substr( 0, destinationShaderType.find_first_of( ':' ) );
+
+	for( const auto &key : { typePrefix, string( "*" ) } )
 	{
-		index = it - begin( g_vectorComponents );
+		auto it = map.find( key );
+		if( it != map.end() )
+		{
+			auto cIt = it->second.find( component );
+			if( cIt != it->second.end() )
+			{
+				return cIt->second;
+			}
+		}
 	}
-	else
+
+	throw IECore::Exception(
+		"No component split adapter registered"
+	);
+}
+
+struct JoinAdapter
+{
+	ConstShaderPtr shader;
+	std::array<InternedString, 4> inParameters;
+	InternedString outParameter;
+};
+
+using TypesToJoinAdapters = boost::container::flat_map<IECore::TypeId, JoinAdapter>;
+using JoinAdapterMap = std::unordered_map<string, TypesToJoinAdapters>;
+
+JoinAdapterMap &joinAdapters()
+{
+	static JoinAdapterMap g_map;
+	return g_map;
+}
+
+const JoinAdapter &findJoinAdapter( const std::string &destinationShaderType, IECore::TypeId destinationParameterType )
+{
+	const auto &map = joinAdapters();
+	const string typePrefix = destinationShaderType.substr( 0, destinationShaderType.find_first_of( ':' ) );
+
+	for( const auto &key : { typePrefix, string( "*" ) } )
 	{
-		it = find( begin( g_colorComponents ), end( g_colorComponents ), suffix );
-		assert( it != end( g_colorComponents ) );
-		index = it - begin( g_colorComponents );
+		auto it = map.find( key );
+		if( it != map.end() )
+		{
+			auto tIt = it->second.find( destinationParameterType );
+			if( tIt != it->second.end() )
+			{
+				return tIt->second;
+			}
+		}
 	}
 
-	return ShaderNetwork::Parameter(
-		parameter.shader,
-		boost::replace_last_copy( parameter.name.string(), "." + suffix, "[" + to_string( index ) + "]" )
+	throw IECore::Exception(
+		"No component join adapter registered"
 	);
 }
 
+const bool g_defaultAdapterRegistrations = [] () {
 
-const int maxArrayInputAdapterSize = 32;
-const InternedString g_arrayInputNames[maxArrayInputAdapterSize] = {
-	"in0", "in1", "in2", "in3", "in4", "in5", "in6", "in7", "in8", "in9",
-	"in10", "in11", "in12", "in13", "in14", "in15", "in16", "in17", "in18", "in19",
-	"in20", "in21", "in22", "in23", "in24", "in25", "in26", "in27", "in28", "in29",
-	"in30", "in31"
-};
-const InternedString g_arrayOutputNames[maxArrayInputAdapterSize + 1] = {
-	"unused", "out1", "out2", "out3", "out4", "out5", "out6", "out7", "out8", "out9",
-	"out10", "out11", "out12", "out13", "out14", "out15", "out16", "out17", "out18", "out19",
-	"out20", "out21", "out22", "out23", "out24", "out25", "out26", "out27", "out28", "out29",
-	"out30", "out31", "out32"
-};
+	ShaderPtr splitter = new Shader(
+		"MaterialX/mx_swizzle_color_float", "osl:shader"
+	);
+
+	for( auto c : string( "rgbaxyz" ) )
+	{
+		splitter->parameters()["channels"] = new StringData( { c } );
+		ShaderNetworkAlgo::registerSplitAdapter(
+			"*", string( { c } ),
+			splitter.get(),
+			g_inParameterName,
+			g_outParameterName
+		);
+	}
+
+	ShaderPtr joiner = new Shader( "MaterialX/mx_pack_color", "osl:shader" );
+	for( auto t : { V2iDataTypeId, V3iDataTypeId, V2fDataTypeId, V3fDataTypeId, Color3fDataTypeId, Color4fDataTypeId } )
+	{
+		ShaderNetworkAlgo::registerJoinAdapter(
+			"*", t,
+			joiner.get(),
+			g_packInParameterNames,
+			g_outParameterName
+		);
+	}
+
+	return true;
+} ();
+
+bool isSplitAdapter( const Shader *shader, InternedString &component, InternedString &inParameter, InternedString &outParameter )
+{
+	if( auto *d = shader->blindData()->member<InternedStringData>( g_splitAdapterComponent ) )
+	{
+		auto inP = shader->blindData()->member<InternedStringData>( g_splitAdapterInParameter );
+		auto outP = shader->blindData()->member<InternedStringData>( g_splitAdapterOutParameter );
+		if( inP && outP )
+		{
+			component = d->readable();
+			inParameter = inP->readable();
+			outParameter = outP->readable();
+		}
+		return true;
+	}
+	else if( auto *b = shader->blindData()->member<BoolData>( ShaderNetworkAlgo::componentConnectionAdapterLabel() ) )
+	{
+		// Legacy format.
+		if( b->readable() && shader->getName() == "MaterialX/mx_swizzle_color_float" )
+		{
+			component = shader->parametersData()->member<StringData>( "channels" )->readable();
+			inParameter = g_inParameterName;
+			outParameter = g_outParameterName;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+bool isJoinAdapter( const Shader *shader, std::array<InternedString, 4> &inParameters, InternedString &outParameter )
+{
+	if( auto d = shader->blindData()->member<InternedStringVectorData>( g_joinAdapterInParameters ) )
+	{
+		auto o = shader->blindData()->member<InternedStringData>( g_joinAdapterOutParameter );
+		if( o )
+		{
+			for( size_t i = 0; i < inParameters.size(); ++i )
+			{
+				inParameters[i] = i < d->readable().size() ? d->readable()[i] : InternedString();
+			}
+			outParameter = o->readable();
+			return true;
+		}
+	}
+	else if( auto *b = shader->blindData()->member<BoolData>( ShaderNetworkAlgo::componentConnectionAdapterLabel() ) )
+	{
+		// Legacy format.
+		if( b->readable() && shader->getName() == "MaterialX/mx_pack_color" )
+		{
+			inParameters = g_packInParameterNames;
+			outParameter = g_outParameterName;
+			return true;
+		}
+	}
+
+	return false;
+}
 
 } // namespace
 
@@ -202,21 +351,24 @@ void ShaderNetworkAlgo::addComponentConnectionAdapters( ShaderNetwork *network,
 			boost::cmatch match;
 			if( boost::regex_match( connection.source.name.c_str(), match, g_componentRegex ) )
 			{
-				// Insert a conversion shader to handle connection to component
+				// Insert a conversion shader to handle connection from component.
 				auto inserted = outputConversions.insert( { connection.source, ShaderNetwork::Parameter() } );
 				if( inserted.second )
 				{
-					ShaderPtr swizzle = new Shader( "MaterialX/mx_swizzle_color_float", "osl:shader" );
+					InternedString component = match[2].str();
+					const SplitAdapter &adapter = findSplitAdapter( sourceShader->getType(), component );
 
-					swizzle->blindData()->writable()[ componentConnectionAdapterLabel() ] = g_trueData;
+					ShaderPtr adapterShader = adapter.shader->copy();
+					adapterShader->blindData()->writable()[g_splitAdapterComponent] = new InternedStringData( component );
+					adapterShader->blindData()->writable()[g_splitAdapterInParameter] = new InternedStringData( adapter.inParameter );
+					adapterShader->blindData()->writable()[g_splitAdapterOutParameter] = new InternedStringData( adapter.outParameter );
 
-					swizzle->parameters()["channels"] = new StringData( match[2] );
-					const InternedString swizzleHandle = network->addShader( g_swizzleHandle, std::move( swizzle ) );
+					const InternedString adapterHandle = network->addShader( g_splitAdapterHandle, std::move( adapterShader ) );
 					network->addConnection( ShaderNetwork::Connection(
 						ShaderNetwork::Parameter{ connection.source.shader, InternedString( match[1] ) },
-						ShaderNetwork::Parameter{ swizzleHandle, g_inParameterName }
+						ShaderNetwork::Parameter{ adapterHandle, adapter.inParameter }
 					) );
-					inserted.first->second = { swizzleHandle, g_outParameterName };
+					inserted.first->second = { adapterHandle, adapter.outParameter };
 				}
 				network->removeConnection( connection );
 				network->addConnection( { inserted.first->second, connection.destination } );
@@ -246,10 +398,8 @@ void ShaderNetworkAlgo::addComponentConnectionAdapters( ShaderNetwork *network,
 			if( boost::regex_match( connection.destination.name.c_str(), match, g_componentRegex ) )
 			{
 				// Connection into a color/vector component
-
-				// Insert a conversion shader to handle connection from component
-
 				const InternedString parameterName = match[1].str();
+
 				auto inserted = convertedParameters.insert( parameterName );
 				if( !inserted.second )
 				{
@@ -259,40 +409,66 @@ void ShaderNetworkAlgo::addComponentConnectionAdapters( ShaderNetwork *network,
 					continue;
 				}
 
-				// All components won't necessarily have connections, so get
-				// the values to fall back on for those that don't.
-				Color4f value( 0 );
-				const Data *d = shader.second->parametersData()->member<Data>( parameterName );
-				if( const V3fData *vd = runTimeCast<const V3fData>( d ) )
-				{
-					value = Color4f( vd->readable()[0], vd->readable()[1], vd->readable()[2], 0.0f );
-				}
-				else if( const Color3fData *cd = runTimeCast<const Color3fData>( d ) )
-				{
-					value = Color4f( cd->readable()[0], cd->readable()[1], cd->readable()[2], 0.0f );
-				}
-				else if( auto c4d = runTimeCast<const Color4fData>( d ) )
-				{
-					value = c4d->readable();
-				}
-
-				// Make shader and set fallback values
+				// Insert a conversion shader to handle connection from component
 
-				ShaderPtr packShader = new Shader( "MaterialX/mx_pack_color", "osl:shader" );
-				packShader->blindData()->writable()[ componentConnectionAdapterLabel() ] = g_trueData;
-				for( int i = 0; i < 4; ++i )
+				const Data *parameterValue = shader.second->parametersData()->member<Data>( parameterName );
+				if( !parameterValue )
 				{
-					packShader->parameters()[g_packInParameterNames[i]] = new FloatData( value[i] );
+					throw IECore::Exception(
+						boost::str( boost::format(
+							"No value found for parameter `%1%.%2%`"
+						) % shader.first % parameterName )
+					);
 				}
 
-				const InternedString packHandle = network->addShader( g_packHandle, std::move( packShader ) );
+				// Make adapter shader.
 
-				// Make connections
+				const JoinAdapter &adapter = findJoinAdapter( shader.second->getType(), parameterValue->typeId() );
+				ShaderPtr adapterShader = adapter.shader->copy();
+				adapterShader->blindData()->writable()[g_joinAdapterInParameters] = new InternedStringVectorData(
+					vector<InternedString>( adapter.inParameters.begin(), adapter.inParameters.end() )
+				);
+				adapterShader->blindData()->writable()[g_joinAdapterOutParameter] = new InternedStringData( adapter.outParameter );
+
+				// Set fallback values for adapter input parameters (since all may not receive connections).
+
+				dispatch(
+					parameterValue,
+					[&] ( auto *d ) {
+						using DataType = typename std::remove_const_t<std::remove_pointer_t<decltype( d )>>;
+						if constexpr(
+							TypeTraits::IsVecTypedData<DataType>::value ||
+							std::is_same_v<DataType, Color3fData> ||
+							std::is_same_v<DataType, Color4fData>
+						)
+						{
+							using ValueType = typename DataType::ValueType;
+							using BaseType = typename ValueType::BaseType;
+							for( size_t i = 0; i < ValueType::dimensions(); ++i )
+							{
+								if( !adapter.inParameters[i].string().empty() )
+								{
+									adapterShader->parameters()[adapter.inParameters[i]] = new TypedData<BaseType>(
+										d->readable()[i]
+									);
+								}
+							}
+						}
+					}
+				);
+
+				// Add shader to network and make connections.
 
-				network->addConnection( { { packHandle, g_outParameterName }, { shader.first, parameterName } } );
+				const InternedString adapterHandle = network->addShader( g_joinAdapterHandle, std::move( adapterShader ) );
+				network->addConnection( { { adapterHandle, adapter.outParameter }, { shader.first, parameterName } } );
 
 				for( int i = 0; i < 4; ++i )
 				{
+					if( adapter.inParameters[i].string().empty() )
+					{
+						continue;
+					}
+
 					ShaderNetwork::Parameter source = network->input( { shader.first, parameterName.string() + "." + g_colorComponents[i] } );
 					if( !source && i < 3 )
 					{
@@ -300,7 +476,7 @@ void ShaderNetworkAlgo::addComponentConnectionAdapters( ShaderNetwork *network,
 					}
 					if( source )
 					{
-						network->addConnection( { source, { packHandle, g_packInParameterNames[i] } } );
+						network->addConnection( { source, { adapterHandle, adapter.inParameters[i] } } );
 					}
 				}
 
@@ -314,111 +490,75 @@ void ShaderNetworkAlgo::removeComponentConnectionAdapters( ShaderNetwork *networ
 {
 	std::vector< IECore::InternedString > toRemove;
 
+	InternedString component;
+	InternedString inParameter;
+	std::array<InternedString, 4> inParameters;
+	InternedString outParameter;
+
 	for( const auto &s : network->shaders() )
 	{
-		ConstBoolDataPtr labelValue = s.second->blindData()->member<BoolData>( componentConnectionAdapterLabel() );
-		if( !labelValue || !labelValue->readable() )
+		if( isSplitAdapter( s.second.get(), component, inParameter, outParameter ) )
 		{
-			continue;
-		}
+			ShaderNetwork::Parameter source = network->input( ShaderNetwork::Parameter( s.first, inParameter ) );
+			if( !source )
+			{
+				throw IECore::Exception( boost::str(
+					boost::format(
+						"removeComponentConnectionAdapters : \"%1%.%2%\" has no input"
+					) % s.first.string() % inParameter.string()
+				) );
+			}
+			source.name = source.name.string() + "." + component.string();
 
-		bool isPack = s.second->getName() == "MaterialX/mx_pack_color";
-		bool isSwizzle = s.second->getName() == "MaterialX/mx_swizzle_color_float";
+			const ShaderNetwork::ConnectionRange outputConnections = network->outputConnections( s.first );
+			for( auto connectionIt = outputConnections.begin(); connectionIt != outputConnections.end(); )
+			{
+				// Copy and increment now so we still have a valid iterator when we
+				// remove the connection.
+				const ShaderNetwork::Connection connection = *connectionIt++;
+				network->removeConnection( connection );
+				network->addConnection( { source, connection.destination } );
+			}
 
-		if( !( s.second->getType() == "osl:shader" && ( isSwizzle || isPack ) ) )
-		{
-			throw IECore::Exception( boost::str(
-				boost::format( "removeComponentConnectionAdapters : adapter is not of supported type and name: '%s' %s : %s" ) %
-				s.first % s.second->getType() % s.second->getName()
-			) );
+			toRemove.push_back( s.first );
 		}
-
-		toRemove.push_back( s.first );
-
-		ShaderNetwork::ConnectionRange outputConnections = network->outputConnections( s.first );
-
-		for( ShaderNetwork::ConnectionIterator it = outputConnections.begin(); it != outputConnections.end(); )
+		else if( isJoinAdapter( s.second.get(), inParameters, outParameter ) )
 		{
-			// Copy and increment now so we still have a valid iterator
-			// if we remove the connection.
-			const ShaderNetwork::Connection connection = *it++;
-			network->removeConnection( connection );
-
-			if( isPack )
+			std::array<ShaderNetwork::Parameter, 4> componentInputs;
+			for( size_t i = 0; i < inParameters.size(); ++i )
 			{
-				const Shader *targetShader = network->getShader( connection.destination.shader );
-
-				ShaderNetwork::ConnectionRange inputConnections = network->inputConnections( s.first );
-				for( ShaderNetwork::ConnectionIterator inputIt = inputConnections.begin(); inputIt != inputConnections.end(); inputIt++ )
+				if( !inParameters[i].string().empty() )
 				{
-					const IECore::InternedString &inputName = inputIt->destination.name;
-					int inputIndex = -1;
-					for( int i = 0; i < 4; i++ )
-					{
-						if( inputName == g_packInParameterNames[i] )
-						{
-							inputIndex = i;
-						}
-					}
+					componentInputs[i] = network->input( { s.first, inParameters[i] } );
+				}
+			}
 
-					if( inputIndex == -1 )
-					{
-						throw IECore::Exception( boost::str(
-							boost::format(
-								"removeComponentConnectionAdapters : Unrecognized input for mx_pack_color \"%1%\""
-							) % inputName
-						) );
-					}
+			const ShaderNetwork::ConnectionRange outputConnections = network->outputConnections( s.first );
+			for( auto connectionIt = outputConnections.begin(); connectionIt != outputConnections.end(); )
+			{
+				// Copy and increment now so we still have a valid iterator when we
+				// remove the connection.
+				const ShaderNetwork::Connection connection = *connectionIt++;
+				network->removeConnection( connection );
 
-					ShaderNetwork::Parameter componentDest;
-					if(
-						targetShader->parametersData()->member<Color4fData>( connection.destination.name ) ||
-						targetShader->parametersData()->member<Color3fData>( connection.destination.name )
-					)
-					{
-						componentDest = { connection.destination.shader, IECore::InternedString( connection.destination.name.string() + "." + g_colorComponents[inputIndex] ) };
-					}
-					else if( targetShader->parametersData()->member<V3fData>( connection.destination.name ) )
-					{
-						componentDest = { connection.destination.shader, IECore::InternedString( connection.destination.name.string() + "." + g_vectorComponents[inputIndex] ) };
-					}
-					else
+				const Data *destinationValue = network->getShader( connection.destination.shader )->parametersData()->member<Data>( connection.destination.name );
+				const bool isColor = runTimeCast<const Color3fData>( destinationValue ) || runTimeCast<const Color4fData>( destinationValue );
+
+				for( size_t i = 0; i < componentInputs.size(); ++i )
+				{
+					if( !componentInputs[i] )
 					{
-						throw IECore::Exception( boost::str(
-							boost::format(
-								"removeComponentConnectionAdapters : Unrecognized type for target parameter \"%1%.%2%\""
-							) % connection.destination.shader.string() % connection.destination.name.string()
-						) );
+						continue;
 					}
 
-					network->addConnection( { inputIt->source, componentDest } );
+					InternedString component = isColor ? g_colorComponents.at( i ) : g_vectorComponents.at( i );
+					network->addConnection(
+						{ componentInputs[i], { connection.destination.shader, connection.destination.name.string() + "." + component.string() } }
+					);
 				}
 			}
-			else
-			{
-				const StringData *channelsData = s.second->parametersData()->member<StringData>( "channels" );
-				if( !channelsData )
-				{
-					throw IECore::Exception( boost::str(
-						boost::format(
-							"removeComponentConnectionAdapters : mx_swizzle_color_float \"%1%\"should have \"channels\" parameter"
-						) % s.first.string()
-					) );
-				}
-
-				ShaderNetwork::Parameter componentSource = network->input( ShaderNetwork::Parameter( s.first, "in" ) );
-				if( !componentSource )
-				{
-					throw IECore::Exception( boost::str(
-						boost::format(
-							"removeComponentConnectionAdapters : mx_swizzle_color_float \"%1%\" must have an input"
-						) % s.first.string()
-					) );
-				}
-				componentSource.name = componentSource.name.string() + "." + channelsData->readable();
 
-				network->addConnection( { componentSource, connection.destination } );
-			}
+			toRemove.push_back( s.first );
 		}
 	}
 
@@ -428,12 +568,61 @@ void ShaderNetworkAlgo::removeComponentConnectionAdapters( ShaderNetwork *networ
 	}
 }
 
+void ShaderNetworkAlgo::registerSplitAdapter( const std::string &destinationShaderType, IECore::InternedString component, const IECoreScene::Shader *adapter, IECore::InternedString inParameter, IECore::InternedString outParameter )
+{
+	splitAdapters()[destinationShaderType][component] = { component, adapter->copy(), inParameter, outParameter };
+}
+
+void ShaderNetworkAlgo::deregisterSplitAdapter( const std::string &destinationShaderType, IECore::InternedString component )
+{
+	splitAdapters()[destinationShaderType].erase( component );
+}
+
+void ShaderNetworkAlgo::registerJoinAdapter( const std::string &destinationShaderType, IECore::TypeId destinationParameterType, const IECoreScene::Shader *adapter, const std::array<IECore::InternedString, 4> &inParameters, IECore::InternedString outParameter )
+{
+	joinAdapters()[destinationShaderType][destinationParameterType] = { adapter->copy(), inParameters, outParameter };
+}
+
+void ShaderNetworkAlgo::deregisterJoinAdapter( const std::string &destinationShaderType, IECore::TypeId destinationParameterType )
+{
+	joinAdapters()[destinationShaderType].erase( destinationParameterType );
+}
+
 const InternedString &ShaderNetworkAlgo::componentConnectionAdapterLabel()
 {
 	static InternedString ret( "cortex_autoAdapter" );
 	return ret;
 }
 
+//////////////////////////////////////////////////////////////////////////
+// OSL Utilities
+//////////////////////////////////////////////////////////////////////////
+
+namespace
+{
+
+ShaderNetwork::Parameter convertComponentSuffix( const ShaderNetwork::Parameter &parameter, const std::string &suffix )
+{
+	int index;
+	auto it = find( begin( g_vectorComponents ), end( g_vectorComponents ), suffix );
+	if( it != end( g_vectorComponents ) )
+	{
+		index = it - begin( g_vectorComponents );
+	}
+	else
+	{
+		auto cIt = find( begin( g_colorComponents ), end( g_colorComponents ), suffix );
+		assert( cIt != end( g_colorComponents ) );
+		index = cIt - begin( g_colorComponents );
+	}
+
+	return ShaderNetwork::Parameter(
+		parameter.shader,
+		boost::replace_last_copy( parameter.name.string(), "." + suffix, "[" + to_string( index ) + "]" )
+	);
+}
+
+} // namespace
 
 void ShaderNetworkAlgo::convertOSLComponentConnections( ShaderNetwork *network )
 {
@@ -490,6 +679,21 @@ void ShaderNetworkAlgo::convertOSLComponentConnections( ShaderNetwork *network,
 	}
 }
 
+void ShaderNetworkAlgo::convertToOSLConventions( ShaderNetwork *network, int oslVersion )
+{
+	expandSplines( network, "osl:" );
+
+	// \todo - it would be a bit more efficient to integrate this, and only traverse the network once,
+	// but I don't think it's worth duplicated the code - fix this up once this call is standard and we
+	// deprecate and remove convertOSLComponentConnections
+	convertOSLComponentConnections( network, oslVersion);
+
+}
+
+//////////////////////////////////////////////////////////////////////////
+// `convertObjectVector()`
+//////////////////////////////////////////////////////////////////////////
+
 namespace
 {
 
@@ -574,6 +778,10 @@ ShaderNetworkPtr ShaderNetworkAlgo::convertObjectVector( const ObjectVector *net
 	return result;
 }
 
+//////////////////////////////////////////////////////////////////////////
+// Spline handling
+//////////////////////////////////////////////////////////////////////////
+
 namespace
 {
 
@@ -725,6 +933,23 @@ const std::string g_oslShader( "osl:shader" );
 const std::string g_colorToArrayAdapter( "Utility/__ColorToArray" );
 const std::string g_floatToArrayAdapter( "Utility/__FloatToArray" );
 
+const int maxArrayInputAdapterSize = 32;
+const InternedString g_arrayInputNames[maxArrayInputAdapterSize] = {
+	"in0", "in1", "in2", "in3", "in4", "in5", "in6", "in7", "in8", "in9",
+	"in10", "in11", "in12", "in13", "in14", "in15", "in16", "in17", "in18", "in19",
+	"in20", "in21", "in22", "in23", "in24", "in25", "in26", "in27", "in28", "in29",
+	"in30", "in31"
+};
+const InternedString g_arrayOutputNames[maxArrayInputAdapterSize + 1] = {
+	"unused", "out1", "out2", "out3", "out4", "out5", "out6", "out7", "out8", "out9",
+	"out10", "out11", "out12", "out13", "out14", "out15", "out16", "out17", "out18", "out19",
+	"out20", "out21", "out22", "out23", "out24", "out25", "out26", "out27", "out28", "out29",
+	"out30", "out31", "out32"
+};
+
+const boost::regex g_splineElementRegex( "^(.*)\\[(.*)\\]\\.y(.*)$" );
+const boost::regex g_splineAdapterInRegex( "^in([0-9]+)(\\..*)?$" );
+
 template< typename TypedSpline >
 std::pair< InternedString, int > createSplineInputAdapter(
 	ShaderNetwork *network, const TypedData<TypedSpline> *splineData,
@@ -792,17 +1017,6 @@ void ensureParametersCopy(
 
 } // namespace
 
-void ShaderNetworkAlgo::convertToOSLConventions( ShaderNetwork *network, int oslVersion )
-{
-	expandSplines( network, "osl:" );
-
-	// \todo - it would be a bit more efficient to integrate this, and only traverse the network once,
-	// but I don't think it's worth duplicated the code - fix this up once this call is standard and we
-	// deprecate and remove convertOSLComponentConnections
-	convertOSLComponentConnections( network, oslVersion);
-
-}
-
 void ShaderNetworkAlgo::collapseSplines( ShaderNetwork *network, std::string targetPrefix )
 {
 	std::vector< IECore::InternedString > adapters;
@@ -968,7 +1182,7 @@ void ShaderNetworkAlgo::expandSplines( ShaderNetwork *network, std::string targe
 			continue;
 		}
 
-		// currentSplineArrayAdapters holds array adaptors that we need to use to hook up inputs to
+		// currentSplineArrayAdapters holds array adapters that we need to use to hook up inputs to
 		// spline plugs. It is indexed by the name of a spline parameter for the shader, and holds
 		// the name of the adapter shader, and the offset we need to use when accessing the knot
 		// vector.
diff --git a/src/IECoreScene/bindings/ShaderNetworkAlgoBinding.cpp b/src/IECoreScene/bindings/ShaderNetworkAlgoBinding.cpp
index af9c2097e0..c886c98a7d 100644
--- a/src/IECoreScene/bindings/ShaderNetworkAlgoBinding.cpp
+++ b/src/IECoreScene/bindings/ShaderNetworkAlgoBinding.cpp
@@ -33,12 +33,14 @@
 //////////////////////////////////////////////////////////////////////////
 
 #include "boost/python.hpp"
-#include "boost/pointer_cast.hpp"
 
 #include "ShaderNetworkAlgoBinding.h"
 
 #include "IECoreScene/ShaderNetworkAlgo.h"
 
+#include "boost/pointer_cast.hpp"
+#include "boost/python/stl_iterator.hpp"
+
 using namespace boost::python;
 using namespace IECore;
 using namespace IECoreScene;
@@ -46,6 +48,23 @@ using namespace IECoreScene;
 namespace
 {
 
+void registerJoinAdapterWrapper( const std::string &destinationShaderType, IECore::TypeId destinationParameterType, const Shader *adapter, object pythonInParameters, InternedString outParameter )
+{
+	std::array<InternedString, 4> inParameters;
+	size_t i = 0;
+	for( auto it = stl_input_iterator<object>( pythonInParameters ), eIt = stl_input_iterator<object>(); it != eIt; ++it, ++i )
+	{
+		if( i >= inParameters.size() )
+		{
+			PyErr_SetString( PyExc_IndexError, "Too many input parameters" );
+			throw_error_already_set();
+		}
+		inParameters[i] = extract<InternedString>( *it );
+	}
+
+	ShaderNetworkAlgo::registerJoinAdapter( destinationShaderType, destinationParameterType, adapter, inParameters, outParameter );
+}
+
 void convertOSLComponentConnectionsWrapper( ShaderNetwork *network, int oslVersion )
 {
 	ShaderNetworkAlgo::convertOSLComponentConnections( network, oslVersion );
@@ -78,6 +97,10 @@ void IECoreSceneModule::bindShaderNetworkAlgo()
 	def( "removeUnusedShaders", &ShaderNetworkAlgo::removeUnusedShaders );
 	def( "addComponentConnectionAdapters", &ShaderNetworkAlgo::addComponentConnectionAdapters, ( arg( "network" ), arg( "targetPrefix" ) = "" ) );
 	def( "removeComponentConnectionAdapters", &ShaderNetworkAlgo::removeComponentConnectionAdapters, ( arg( "network" ) ) );
+	def( "registerSplitAdapter", &ShaderNetworkAlgo::registerSplitAdapter, ( arg( "destinationShaderType" ), arg( "component" ), arg( "adapter" ), arg( "inParameter" ), arg( "outParameter" ) ) );
+	def( "deregisterSplitAdapter", &ShaderNetworkAlgo::deregisterSplitAdapter, ( arg( "destinationShaderType" ), arg( "component" ) ) );
+	def( "registerJoinAdapter", &registerJoinAdapterWrapper, ( arg( "destinationShaderType" ), arg( "destinationParameterType" ), arg( "adapter" ), arg( "inParameters" ), arg( "outParameter" ) ) );
+	def( "deregisterJoinAdapter", &ShaderNetworkAlgo::deregisterJoinAdapter, ( arg( "destinationShaderType" ), arg( "destinationParameterType" ) ) );
 	def( "componentConnectionAdapterLabel", &componentConnectionAdapterLabelWrapper );
 	def( "convertToOSLConventions", &ShaderNetworkAlgo::convertToOSLConventions );
 	def( "convertOSLComponentConnections", &convertOSLComponentConnectionsWrapper, ( arg( "network" ), arg( "oslVersion" ) = 10900 ) );
diff --git a/test/IECore/InterpolatorTest.inl b/test/IECore/InterpolatorTest.inl
index 3c12edbe9b..1aeee14cd1 100644
--- a/test/IECore/InterpolatorTest.inl
+++ b/test/IECore/InterpolatorTest.inl
@@ -429,7 +429,7 @@ void MatrixCubicInterpolatorTest<T>::testSimple()
 	CubicInterpolator< Matrix > interp;
 	CubicInterpolator< Vector > vectorInterp;
 
-	Vector s0( 1, 1, 1 ), h0( 0, 0, 0 ), r0( 0, 0, 0 ), t0( 5, 0, 0 );
+	Vector s0( 1, 1, 1 ), h0( 0, 0, 0 ), t0( 5, 0, 0 );
 	Vector s1( 1, 2, 3 ), h1( 1, 2, 3 ), r1( 0, 1, 0 ), t1( 10, 0, 0 );
 	Vector s2( 0.5, 1.4, 5 ), h2( 2, 3, 4 ), r2( 0, 0.5, 0 ), t2( 20, 0, 0 );
 	Vector s3( 1, 2, 3 ), h3( 1, 2, 3 ), r3( 0, 1, 0 ), t3( 0, 0, 0 );
@@ -492,7 +492,7 @@ void MatrixCubicInterpolatorTest<T>::testTyped()
 	CubicInterpolator< MatrixData > interp;
 	CubicInterpolator< Vector > vectorInterp;
 
-	Vector s0( 1, 1, 1 ), h0( 0, 0, 0 ), r0( 0, 0, 0 ), t0( 5, 0, 0 );
+	Vector s0( 1, 1, 1 ), h0( 0, 0, 0 ), t0( 5, 0, 0 );
 	Vector s1( 1, 2, 3 ), h1( 1, 2, 3 ), r1( 0, 1, 0 ), t1( 10, 0, 0 );
 	Vector s2( 0.5, 1.4, 5 ), h2( 2, 3, 4 ), r2( 0, 0.5, 0 ), t2( 20, 0, 0 );
 	Vector s3( 1, 2, 3 ), h3( 1, 2, 3 ), r3( 0, 1, 0 ), t3( 0, 0, 0 );
diff --git a/test/IECore/ReprTest.py b/test/IECore/ReprTest.py
index 6423a93ac4..65b7ed19d3 100644
--- a/test/IECore/ReprTest.py
+++ b/test/IECore/ReprTest.py
@@ -60,6 +60,28 @@ def test( self ) :
 		] :
 			self.assertTrue( type( v ) is type( eval( IECore.repr( v ) ) ) )
 			self.assertEqual( v, eval( IECore.repr( v ) ) )
+	
+	def testInfinity( self ) :
+
+		for v in [
+			# Python raises "OverflowError : bad numeric conversion : positive overflow"
+			# when passing `float( "inf" )` to `V2f``
+			imath.V2d( float( "inf" ), float( "inf" ) ),
+			imath.V3f( float( "inf" ), float( "inf" ), float( "inf" ) ),
+			imath.V3d( float( "inf" ), float( "inf" ), float( "inf" ) ),
+			imath.Color3f( float( "inf" ), float( "inf" ), float( "inf" ) ),
+			imath.Color4f( float( "inf" ), float( "inf" ), float( "inf" ), float( "inf" ) ),
+		] :
+			with self.subTest( v = v ) :
+				self.assertTrue( type( v ) is type( eval( IECore.repr( v ) ) ) )
+				self.assertEqual( v, eval( IECore.repr( v ) ) )
+
+				self.assertTrue( type( -v ) is type( eval( IECore.repr( -v ) ) ) )
+				self.assertEqual( -v, eval( IECore.repr( -v ) ) )
+
+				self.assertEqual( str( v ), "{}({})".format( type( v ).__name__, ", ".join( ["inf"] * v.dimensions() ) ) )
+				self.assertEqual( str( -v ), "{}({})".format( type( v ).__name__, ", ".join( ["-inf"] * v.dimensions() ) ) )
+
 
 if __name__ == "__main__":
 	unittest.main()
diff --git a/test/IECoreGL/Camera.py b/test/IECoreGL/Camera.py
index 9840160f62..8476a25b25 100644
--- a/test/IECoreGL/Camera.py
+++ b/test/IECoreGL/Camera.py
@@ -64,7 +64,7 @@ def testPositioning( self ) :
 		i = IECore.Reader.create( os.path.join( os.path.dirname( __file__ ), "output", "testCamera.tif" ) ).read()
 		dimensions = i.dataWindow.size() + imath.V2i( 1 )
 		midpoint = dimensions.x * dimensions.y//2 + dimensions.x//2
-		self.assertEqual( i["G"][midpoint], 0 )
+		self.assertAlmostEqual( i["G"][midpoint], 0, 6 )
 
 		# render a plane at z = 0 with the camera moved back a touch to see it
 		r = IECoreGL.Renderer()
@@ -87,7 +87,7 @@ def testPositioning( self ) :
 		i = IECore.Reader.create( os.path.join( os.path.dirname( __file__ ), "output", "testCamera.tif" ) ).read()
 		dimensions = i.dataWindow.size() + imath.V2i( 1 )
 		midpoint = dimensions.x * dimensions.y//2 + dimensions.x//2
-		self.assertEqual( i["A"][midpoint], 1 )
+		self.assertAlmostEqual( i["A"][midpoint], 1, 6 )
 
 	def testXYOrientation( self ) :
 
@@ -113,15 +113,15 @@ def testXYOrientation( self ) :
 		i = IECore.Reader.create( os.path.join( os.path.dirname( __file__ ), "output", "testCamera.tif" ) ).read()
 		dimensions = i.dataWindow.size() + imath.V2i( 1 )
 		index = dimensions.x * dimensions.y//2 + dimensions.x - 1
-		self.assertEqual( i["A"][index], 1 )
+		self.assertAlmostEqual( i["A"][index], 1, 6 )
 		self.assertAlmostEqual( i["R"][index], 1, 6 )
-		self.assertEqual( i["G"][index], 0 )
-		self.assertEqual( i["B"][index], 0 )
+		self.assertAlmostEqual( i["G"][index], 0, 6 )
+		self.assertAlmostEqual( i["B"][index], 0, 6 )
 		index = dimensions.x//2
-		self.assertEqual( i["A"][index], 1 )
-		self.assertEqual( i["R"][index], 0 )
+		self.assertAlmostEqual( i["A"][index], 1, 6 )
+		self.assertAlmostEqual( i["R"][index], 0, 6 )
 		self.assertAlmostEqual( i["G"][index], 1, 6 )
-		self.assertEqual( i["B"][index], 0 )
+		self.assertAlmostEqual( i["B"][index], 0, 6 )
 
 	def setUp( self ) :
 
diff --git a/test/IECoreGL/ImmediateRenderer.py b/test/IECoreGL/ImmediateRenderer.py
index d381690d28..d97acac24b 100644
--- a/test/IECoreGL/ImmediateRenderer.py
+++ b/test/IECoreGL/ImmediateRenderer.py
@@ -76,20 +76,20 @@ def test( self ) :
 		i = IECore.Reader.create( outputFileName ).read()
 		dimensions = i.dataWindow.size() + imath.V2i( 1 )
 		index = int(dimensions.x * 0.5)
-		self.assertEqual( i["A"][index], 1 )
+		self.assertAlmostEqual( i["A"][index], 1, 6 )
 		self.assertAlmostEqual( i["R"][index], 1, 6 )
 		self.assertAlmostEqual( i["G"][index], 1, 6 )
-		self.assertEqual( i["B"][index], 0 )
+		self.assertAlmostEqual( i["B"][index], 0, 6 )
 		index = dimensions.x * int(dimensions.y * 0.5) + int(dimensions.x * 0.5)
-		self.assertEqual( i["A"][index], 1 )
-		self.assertEqual( i["R"][index], 0 )
-		self.assertEqual( i["G"][index], 0 )
+		self.assertAlmostEqual( i["A"][index], 1, 6 )
+		self.assertAlmostEqual( i["R"][index], 0, 6 )
+		self.assertAlmostEqual( i["G"][index], 0, 6 )
 		self.assertAlmostEqual( i["B"][index], 1, 6 )
 		index = 0
-		self.assertEqual( i["A"][index], 0 )
-		self.assertEqual( i["R"][index], 0 )
-		self.assertEqual( i["G"][index], 0 )
-		self.assertEqual( i["B"][index], 0 )
+		self.assertAlmostEqual( i["A"][index], 0, 6 )
+		self.assertAlmostEqual( i["R"][index], 0, 6 )
+		self.assertAlmostEqual( i["G"][index], 0, 6 )
+		self.assertAlmostEqual( i["B"][index], 0, 6 )
 
 	def setUp( self ) :
 
diff --git a/test/IECoreGL/MeshPrimitiveTest.py b/test/IECoreGL/MeshPrimitiveTest.py
index 7fdd3f6eea..4e4aad7546 100644
--- a/test/IECoreGL/MeshPrimitiveTest.py
+++ b/test/IECoreGL/MeshPrimitiveTest.py
@@ -158,13 +158,13 @@ def testUniformCs( self ) :
 		dimensions = image.dataWindow.size() + imath.V2i( 1 )
 		index = dimensions.x * int(dimensions.y * 0.75) + int(dimensions.x * 0.25)
 		self.assertAlmostEqual( image["R"][index], 1, 6 )
-		self.assertEqual( image["G"][index], 0 )
-		self.assertEqual( image["B"][index], 0 )
+		self.assertAlmostEqual( image["G"][index], 0, 6 )
+		self.assertAlmostEqual( image["B"][index], 0, 6 )
 
 		index = dimensions.x * int(dimensions.y * 0.75) + int(dimensions.x * 0.75)
-		self.assertEqual( image["R"][index], 0 )
+		self.assertAlmostEqual( image["R"][index], 0, 6 )
 		self.assertAlmostEqual( image["G"][index], 1, 6 )
-		self.assertEqual( image["B"][index], 0 )
+		self.assertAlmostEqual( image["B"][index], 0, 6 )
 
 		index = dimensions.x * int(dimensions.y * 0.25) + int(dimensions.x * 0.75)
 		self.assertAlmostEqual( image["R"][index], 1, 6 )
@@ -172,8 +172,8 @@ def testUniformCs( self ) :
 		self.assertAlmostEqual( image["B"][index], 1, 6 )
 
 		index = dimensions.x * int(dimensions.y * 0.25) + int(dimensions.x * 0.25)
-		self.assertEqual( image["R"][index], 0 )
-		self.assertEqual( image["G"][index], 0 )
+		self.assertAlmostEqual( image["R"][index], 0, 6 )
+		self.assertAlmostEqual( image["G"][index], 0, 6 )
 		self.assertAlmostEqual( image["B"][index], 1, 6 )
 
 	def testBound( self ) :
@@ -192,10 +192,10 @@ def testFaceNormals( self ) :
 		#include "IECoreGL/FragmentShader.h"
 		IECOREGL_FRAGMENTSHADER_IN vec3 fragmentN;
 
- 		void main()
- 		{
- 			gl_FragColor = vec4( fragmentN, 1.0 );
- 		}
+		void main()
+		{
+			gl_FragColor = vec4( fragmentN, 1.0 );
+		}
 		"""
 
 		r = IECoreGL.Renderer()
@@ -224,8 +224,8 @@ def testFaceNormals( self ) :
 		image = IECore.Reader.create( self.outputFileName ).read()
 		dimensions = image.dataWindow.size() + imath.V2i( 1 )
 		index = dimensions.x * dimensions.y//2 + dimensions.x//2
-		self.assertEqual( image["R"][index], 0 )
-		self.assertEqual( image["G"][index], 0 )
+		self.assertAlmostEqual( image["R"][index], 0, 6 )
+		self.assertAlmostEqual( image["G"][index], 0, 6 )
 		self.assertAlmostEqual( image["B"][index], 1, 6 )
 
 	def testIndexedUV( self ) :
diff --git a/test/IECoreGL/Renderer.py b/test/IECoreGL/Renderer.py
index e977c2f879..effdd751ca 100644
--- a/test/IECoreGL/Renderer.py
+++ b/test/IECoreGL/Renderer.py
@@ -282,17 +282,17 @@ def testStackBug( self ) :
 		index = dimensions.x * int(dimensions.y * 0.5) + int(dimensions.x * 0.5)
 		self.assertAlmostEqual( i["R"][index], 1, 6 )
 		self.assertAlmostEqual( i["G"][index], 1, 6 )
-		self.assertEqual( i["B"][index], 0 )
+		self.assertAlmostEqual( i["B"][index], 0, 6 )
 
 		index = dimensions.x * int(dimensions.y * 0.5)
 		self.assertAlmostEqual( i["R"][index], 1, 6 )
-		self.assertEqual( i["G"][index], 0 )
-		self.assertEqual( i["B"][index], 0 )
+		self.assertAlmostEqual( i["G"][index], 0, 6 )
+		self.assertAlmostEqual( i["B"][index], 0, 6 )
 
 		index = dimensions.x * int(dimensions.y * 0.5) + int(dimensions.x * 1) - 1
 		self.assertAlmostEqual( i["R"][index], 1, 6 )
-		self.assertEqual( i["G"][index], 0 )
-		self.assertEqual( i["B"][index], 0 )
+		self.assertAlmostEqual( i["G"][index], 0, 6 )
+		self.assertAlmostEqual( i["B"][index], 0, 6 )
 
 	def testPrimVars( self ) :
 
@@ -345,18 +345,18 @@ def testPrimVars( self ) :
 		i = IECore.Reader.create( os.path.join( os.path.dirname( __file__ ), "output", "testPrimVars.tif" ) ).read()
 		dimensions = i.dataWindow.size() + imath.V2i( 1 )
 		index = dimensions.x * int(dimensions.y * 0.5)
-		self.assertEqual( i["R"][index], 0 )
+		self.assertAlmostEqual( i["R"][index], 0, 6 )
 		self.assertAlmostEqual( i["G"][index], 1, 6 )
-		self.assertEqual( i["B"][index], 0 )
+		self.assertAlmostEqual( i["B"][index], 0, 6 )
 
 		index = dimensions.x * int(dimensions.y * 0.5) + int(dimensions.x * 0.5)
 		self.assertAlmostEqual( i["R"][index], 1, 6 )
-		self.assertEqual( i["G"][index], 0 )
-		self.assertEqual( i["B"][index], 0 )
+		self.assertAlmostEqual( i["G"][index], 0, 6 )
+		self.assertAlmostEqual( i["B"][index], 0, 6 )
 
 		index = dimensions.x * int(dimensions.y * 0.5) + int(dimensions.x * 1) - 1
-		self.assertEqual( i["R"][index], 0 )
-		self.assertEqual( i["G"][index], 0 )
+		self.assertAlmostEqual( i["R"][index], 0, 6 )
+		self.assertAlmostEqual( i["G"][index], 0, 6 )
 		self.assertAlmostEqual( i["B"][index], 1, 6 )
 
 	## \todo Make this assert something
diff --git a/test/IECoreGL/ShadingTest.py b/test/IECoreGL/ShadingTest.py
index 0720e2fb1a..9cffe9f290 100644
--- a/test/IECoreGL/ShadingTest.py
+++ b/test/IECoreGL/ShadingTest.py
@@ -825,10 +825,11 @@ def testVertexCsDoesntAffectWireframe( self ) :
 		# wireframe is green, and vertex Cs is black,
 		# so there should be no contribution from
 		# wireframe or solid shading in the red channel.
-		self.assertEqual( sum( image["R"] ), 0 )
+		self.assertAlmostEqual( sum( image["R"] ), 0, 4 )
 		# black vertex colour should have no effect on
 		# green wireframe, so we should have some wireframe
 		# contribution in the green channel.
+		self.assertNotAlmostEqual( sum( image["G"] ), 0, 4 )
 		self.assertTrue( sum( image["G"] ) > 0 )
 
 	def testUniformFloatArrayParameters( self ) :
diff --git a/test/IECoreHoudini/SceneCacheTest.py b/test/IECoreHoudini/SceneCacheTest.py
index b4c489f896..371f3bdac5 100644
--- a/test/IECoreHoudini/SceneCacheTest.py
+++ b/test/IECoreHoudini/SceneCacheTest.py
@@ -3647,7 +3647,10 @@ def testCurveAndPoint( self ):
 
 		curve = geo.createNode( "line" )
 		point = geo.createNode( "add" )
-		point.parm( "usept0" ).set( True )
+		if hou.applicationVersion()[0] < 19 :
+			point.parm( "usept0" ).set( True )
+		else :
+			point.parm( "points" ).set( 1 )
 		merge = geo.createNode( "merge" )
 
 		merge.setInput( 0, curve )
diff --git a/test/IECoreImage/DisplayDriverServerTest.py b/test/IECoreImage/DisplayDriverServerTest.py
index 9bb82a0e8d..ca1a00b4b7 100644
--- a/test/IECoreImage/DisplayDriverServerTest.py
+++ b/test/IECoreImage/DisplayDriverServerTest.py
@@ -34,12 +34,20 @@
 
 import unittest
 import sys
+import os
+import imath
 
 import IECore
 import IECoreImage
 
 class DisplayDriverServerTest( unittest.TestCase ) :
 
+	def __prepareBuf( self, buf, width, offset, red, green, blue ):
+		for i in range( 0, width ):
+			buf[3*i] = blue[i+offset]
+			buf[3*i+1] = green[i+offset]
+			buf[3*i+2] = red[i+offset]
+
 	def testPortNumber( self ) :
 
 		s1 = IECoreImage.DisplayDriverServer( 1559 )
@@ -118,6 +126,63 @@ def testPortRangeRegistry( self ) :
 		s2 = IECoreImage.DisplayDriverServer()
 		self.assertEqual( s2.portNumber(), 45021 )
 
+	def testMergeMap( self ) :
+		server = IECoreImage.DisplayDriverServer( 45001 )
+
+		img = IECore.Reader.create( os.path.join( "test", "IECoreImage", "data", "tiff", "bluegreen_noise.400x300.tif" ) )()
+		self.assertEqual( img.keys(), [ 'B', 'G', 'R' ] )
+		red = img['R']
+		green = img['G']
+		blue = img['B']
+		width = img.dataWindow.max().x - img.dataWindow.min().x + 1
+
+		params = IECore.CompoundData()
+		params['displayHost'] = IECore.StringData('localhost')
+		params['displayPort'] = IECore.StringData( '45001' )
+		params['displayDriverServer:mergeId'] = IECore.IntData( 42 )
+		params['displayDriverServer:mergeClients'] = IECore.IntData( 2 )
+		params["remoteDisplayType"] = IECore.StringData( "ImageDisplayDriver" )
+		params["handle"] = IECore.StringData( "myHandle1" )
+
+		idd1 = IECoreImage.ClientDisplayDriver( img.displayWindow, img.dataWindow, list( img.channelNames() ), params )
+
+		params["handle"] = IECore.StringData( "myHandle2" )
+		idd2 = IECoreImage.ClientDisplayDriver( img.displayWindow, img.dataWindow, list( img.channelNames() ), params )
+
+		params['displayDriverServer:mergeId'] = IECore.IntData( 666 )
+		params['displayDriverServer:mergeClients'] = IECore.IntData( 1 )
+		params["handle"] = IECore.StringData( "myHandle3" )
+		idd3 = IECoreImage.ClientDisplayDriver( img.displayWindow, img.dataWindow, list( img.channelNames() ), params )
+
+		buf = IECore.FloatVectorData( width * 3 )
+		for i in range( 0, img.dataWindow.max().y - img.dataWindow.min().y + 1 ):
+			self.__prepareBuf( buf, width, i*width, red, green, blue )
+			idd1.imageData( imath.Box2i( imath.V2i( img.dataWindow.min().x, i + img.dataWindow.min().y ), imath.V2i( img.dataWindow.max().x, i + img.dataWindow.min().y) ), buf )
+			idd2.imageData( imath.Box2i( imath.V2i( img.dataWindow.min().x, i + img.dataWindow.min().y ), imath.V2i( img.dataWindow.max().x, i + img.dataWindow.min().y) ), buf )
+			idd3.imageData( imath.Box2i( imath.V2i( img.dataWindow.min().x, i + img.dataWindow.min().y ), imath.V2i( img.dataWindow.max().x, i + img.dataWindow.min().y) ), buf )
+
+		idd1.imageClose()
+		idd2.imageClose()
+		idd3.imageClose()
+
+		newImg = IECoreImage.ImageDisplayDriver.removeStoredImage( "myHandle1" )
+		newImg2 = IECoreImage.ImageDisplayDriver.removeStoredImage( "myHandle2" )
+		newImg3 = IECoreImage.ImageDisplayDriver.removeStoredImage( "myHandle3" )
+
+		# merge drivers share the same display driver - so second image should be none,
+		# as there is no image drivere associated with it.
+		self.assertIsNone( newImg2 )
+
+		# remove blindData for comparison
+		newImg.blindData().clear()
+		img.blindData().clear()
+		self.assertEqual( newImg, img )
+
+		newImg3.blindData().clear()
+		self.assertEqual( newImg3, img )
+
+		server = None
+
 if __name__ == "__main__":
 	unittest.main()
 
diff --git a/test/IECoreScene/MeshAlgoDistributePointsTest.py b/test/IECoreScene/MeshAlgoDistributePointsTest.py
index 5ad165f943..fdcae2a944 100644
--- a/test/IECoreScene/MeshAlgoDistributePointsTest.py
+++ b/test/IECoreScene/MeshAlgoDistributePointsTest.py
@@ -544,14 +544,6 @@ def setUp( self ) :
 
 		os.environ["CORTEX_POINTDISTRIBUTION_TILESET"] = os.path.join( "test", "IECore", "data", "pointDistributions", "pointDistributionTileSet2048.dat" )
 
-if sys.platform == "darwin" :
-
-	# These fail because MacOS uses libc++, and libc++ has a
-	# different `std::random_shuffle()` than libstdc++.
-
-	MeshAlgoDistributePointsTest.testDensityMaskPrimVar = unittest.expectedFailure( MeshAlgoDistributePointsTest.testDensityMaskPrimVar )
-	MeshAlgoDistributePointsTest.testDistanceBetweenPoints = unittest.expectedFailure( MeshAlgoDistributePointsTest.testDistanceBetweenPoints )
-
 if __name__ == "__main__":
 	unittest.main()
 
diff --git a/test/IECoreScene/MeshAlgoSegmentTest.py b/test/IECoreScene/MeshAlgoSegmentTest.py
index fa5c52d6cb..a51e33f508 100644
--- a/test/IECoreScene/MeshAlgoSegmentTest.py
+++ b/test/IECoreScene/MeshAlgoSegmentTest.py
@@ -66,8 +66,8 @@ def testCanSegmentUsingIntegerPrimvar( self ) :
 		p12 = imath.V3f( 1, 2, 0 )
 		p22 = imath.V3f( 2, 2, 0 )
 
-		self.assertEqual( segments[0]["P"].data, IECore.V3fVectorData( [p00, p10, p11, p01, p20, p21], IECore.GeometricData.Interpretation.Point ) )
-		self.assertEqual( segments[1]["P"].data, IECore.V3fVectorData( [p01, p11, p12, p02, p21, p22], IECore.GeometricData.Interpretation.Point ) )
+		self.assertEqual( segments[0]["P"].data, IECore.V3fVectorData( [p00, p10, p20, p01, p11, p21], IECore.GeometricData.Interpretation.Point ) )
+		self.assertEqual( segments[1]["P"].data, IECore.V3fVectorData( [p01, p11, p21, p02, p12, p22], IECore.GeometricData.Interpretation.Point ) )
 
 	def testCanSegmentUsingStringPrimvar( self ) :
 		mesh = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( 0 ), imath.V2f( 2 ) ), imath.V2i( 2 ) )
@@ -95,8 +95,8 @@ def testCanSegmentUsingStringPrimvar( self ) :
 		p12 = imath.V3f( 1, 2, 0 )
 		p22 = imath.V3f( 2, 2, 0 )
 
-		self.assertEqual( segments[0]["P"].data, IECore.V3fVectorData( [p00, p10, p11, p01, p21, p22, p12], IECore.GeometricData.Interpretation.Point ) )
-		self.assertEqual( segments[1]["P"].data, IECore.V3fVectorData( [p10, p20, p21, p11, p01, p12, p02], IECore.GeometricData.Interpretation.Point ) )
+		self.assertEqual( segments[0]["P"].data, IECore.V3fVectorData( [p00, p10, p01, p11, p21, p12, p22], IECore.GeometricData.Interpretation.Point ) )
+		self.assertEqual( segments[1]["P"].data, IECore.V3fVectorData( [p10, p20, p01, p11, p21, p02, p12], IECore.GeometricData.Interpretation.Point ) )
 
 	def testSegmentsFullyIfNoSegmentValuesGiven( self ) :
 		mesh = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( 0 ), imath.V2f( 2 ) ), imath.V2i( 2 ) )
@@ -130,8 +130,8 @@ def testSegmentsFullyIfNoSegmentValuesGiven( self ) :
 			s0 = segments[1]
 			s1 = segments[0]
 
-		self.assertEqual( s0["P"].data, IECore.V3fVectorData( [p00, p10, p11, p01, p21, p22, p12], IECore.GeometricData.Interpretation.Point ) )
-		self.assertEqual( s1["P"].data, IECore.V3fVectorData( [p10, p20, p21, p11, p01, p12, p02], IECore.GeometricData.Interpretation.Point ) )
+		self.assertEqual( s0["P"].data, IECore.V3fVectorData( [p00, p10, p01, p11, p21, p12, p22], IECore.GeometricData.Interpretation.Point ) )
+		self.assertEqual( s1["P"].data, IECore.V3fVectorData( [p10, p20, p01, p11, p21, p02, p12], IECore.GeometricData.Interpretation.Point ) )
 
 
 	def testRaisesExceptionIfSegmentKeysNotSameTypeAsPrimvar( self ) :
@@ -184,7 +184,7 @@ def testSegmentSubset( self ) :
 		p22 = imath.V3f( 2, 2, 0 )
 
 		self.assertEqual( len( segments ), 1 )
-		self.assertEqual( segments[0]["P"].data, IECore.V3fVectorData( [p11, p21, p22, p12], IECore.GeometricData.Interpretation.Point ) )
+		self.assertEqual( segments[0]["P"].data, IECore.V3fVectorData( [p11, p21, p12, p22], IECore.GeometricData.Interpretation.Point ) )
 		self.assertEqual( segments[0]["s"].data, IECore.StringVectorData( ["b"] ) )
 
 
diff --git a/test/IECoreScene/MeshAlgoSplitTest.py b/test/IECoreScene/MeshAlgoSplitTest.py
index f6556c7dca..5e6b66c412 100644
--- a/test/IECoreScene/MeshAlgoSplitTest.py
+++ b/test/IECoreScene/MeshAlgoSplitTest.py
@@ -170,16 +170,29 @@ def accumulateInPython2( iterable ):
 			newVerticesPerFace = []
 			newVertIndices = []
 			mapFaceVert = []
-			reverseIndices = []
+
+			usedIndices = set()
 			for i in r:
 				vpf = mesh.verticesPerFace[i]
 				newVerticesPerFace.append( vpf )
 				for j in range( vpf ):
 					origFaceVert = faceIndices[i] + j
 					origVert = mesh.vertexIds[ origFaceVert ]
-					newIndex = reindex.setdefault( origVert, len( reindex ) )
-					if len( reindex ) > len( reverseIndices ):
-						reverseIndices.append( origVert )
+
+					usedIndices.add( origVert )
+
+			usedIndices = sorted( usedIndices )
+
+			for i in range( len( usedIndices ) ):
+				reindex[ usedIndices[i] ] = i
+
+			for i in r:
+				vpf = mesh.verticesPerFace[i]
+				for j in range( vpf ):
+					origFaceVert = faceIndices[i] + j
+					origVert = mesh.vertexIds[ origFaceVert ]
+
+					newIndex = usedIndices.index( origVert )
 					newVertIndices.append( newIndex )
 					mapFaceVert.append( origFaceVert )
 
@@ -226,7 +239,7 @@ def accumulateInPython2( iterable ):
 				if p.interpolation == interp.Constant:
 					d = p.data.copy()
 				elif p.interpolation in [ interp.Vertex, interp.Varying ]:
-					d = type( p.data )( [ pd[i] for i in reverseIndices] )
+					d = type( p.data )( [ pd[i] for i in usedIndices] )
 				elif p.interpolation == interp.FaceVarying:
 					d = type( p.data )( [ pd[i] for i in mapFaceVert ] )
 				elif p.interpolation == interp.Uniform:
@@ -269,8 +282,8 @@ def testCanSplitUsingIntegerPrimvar( self ) :
 		p12 = imath.V3f( 1, 2, 0 )
 		p22 = imath.V3f( 2, 2, 0 )
 
-		self.assertEqual( s0["P"].data, IECore.V3fVectorData( [p00, p10, p11, p01, p20, p21], IECore.GeometricData.Interpretation.Point ) )
-		self.assertEqual( s1["P"].data, IECore.V3fVectorData( [p01, p11, p12, p02, p21, p22], IECore.GeometricData.Interpretation.Point ) )
+		self.assertEqual( s0["P"].data, IECore.V3fVectorData( [p00, p10, p20, p01, p11, p21], IECore.GeometricData.Interpretation.Point ) )
+		self.assertEqual( s1["P"].data, IECore.V3fVectorData( [p01, p11, p21, p02, p12, p22], IECore.GeometricData.Interpretation.Point ) )
 
 	def testSplitsFully( self ) :
 		mesh = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( 0 ), imath.V2f( 2 ) ), imath.V2i( 2 ) )
@@ -303,8 +316,8 @@ def testSplitsFully( self ) :
 		if s0["s"].data[0] != 'a':
 			s0,s1 = s1,s0
 
-		self.assertEqual( s0["P"].data, IECore.V3fVectorData( [p00, p10, p11, p01, p21, p22, p12], IECore.GeometricData.Interpretation.Point ) )
-		self.assertEqual( s1["P"].data, IECore.V3fVectorData( [p10, p20, p21, p11, p01, p12, p02], IECore.GeometricData.Interpretation.Point ) )
+		self.assertEqual( s0["P"].data, IECore.V3fVectorData( [p00, p10, p01, p11, p21, p12, p22], IECore.GeometricData.Interpretation.Point ) )
+		self.assertEqual( s1["P"].data, IECore.V3fVectorData( [p10, p20, p01, p11, p21, p02, p12], IECore.GeometricData.Interpretation.Point ) )
 
 	def testSplitUsingIndexedPrimitiveVariable( self ) :
 
diff --git a/test/IECoreScene/MeshPrimitive.py b/test/IECoreScene/MeshPrimitive.py
index 7ffd78aca1..1250569862 100644
--- a/test/IECoreScene/MeshPrimitive.py
+++ b/test/IECoreScene/MeshPrimitive.py
@@ -266,7 +266,7 @@ def testBox( self ) :
 		self.assertEqual( len( m["N"].data ), 6 )
 		self.assertEqual( len( m["uv"].indices ), m.variableSize( IECoreScene.PrimitiveVariable.Interpolation.FaceVarying ) )
 
-
+	@unittest.skipIf( IECore.TestUtil.inMacCI(), "Incorrect pointAtUV results on virtualized macOS used in CI" )
 	def testPlane( self ) :
 
 		m = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( 0 ), imath.V2f( 1 ) ) )
diff --git a/test/IECoreScene/ShaderNetworkAlgoTest.py b/test/IECoreScene/ShaderNetworkAlgoTest.py
index 92ac2bfae7..0ae5268a0f 100644
--- a/test/IECoreScene/ShaderNetworkAlgoTest.py
+++ b/test/IECoreScene/ShaderNetworkAlgoTest.py
@@ -34,6 +34,7 @@
 #
 ##########################################################################
 
+import pathlib
 import unittest
 
 import imath
@@ -271,6 +272,7 @@ def testArnoldComponentConnectionsNotConverted( self ) :
 		self.assertEqual( n, n2 )
 
 	def testAddRemoveComponentConnectionAdapters( self ) :
+
 		source = IECoreScene.Shader( "source", "ai:shader" )
 
 		dest = IECoreScene.Shader( "dest", "ai:surface" )
@@ -350,9 +352,6 @@ def testAddRemoveComponentConnectionAdapters( self ) :
 		self.assertEqual( converted.getShader( connectionDict["b"].source.shader ).name, "MaterialX/mx_pack_color" )
 		self.assertEqual( converted.getShader( connectionDict["c"].source.shader ).name, "MaterialX/mx_swizzle_color_float" )
 
-		self.assertEqual( IECoreScene.ShaderNetworkAlgo.componentConnectionAdapterLabel(), IECore.InternedString( "cortex_autoAdapter" ) )
-		self.assertEqual( converted.getShader( connectionDict["c"].source.shader ).blindData(), IECore.CompoundData( { "cortex_autoAdapter" : True } ) )
-
 		# With a prefix that doesn't match, nothing happens
 		self.assertEqual( prefixMismatch, network )
 		# With a prefix that matches, everything gets adapted
@@ -365,12 +364,12 @@ def testAddRemoveComponentConnectionAdapters( self ) :
 		self.assertTrue( network.getShader( "source1", _copy = False ).isSame( converted.getShader( "source1", _copy = False ) ) )
 		self.assertTrue( network.getShader( "dest", _copy = False ).isSame( converted.getShader( "dest", _copy = False ) ) )
 
-		dest.blindData()["cortex_autoAdapter"] = IECore.BoolData( True )
-		badNetwork = IECoreScene.ShaderNetwork()
-		badNetwork.addShader( "badHandle", dest )
-		badNetwork.setOutput( IECoreScene.ShaderNetwork.Parameter( "badHandle", "" ) )
-		with self.assertRaisesRegex( RuntimeError, "removeComponentConnectionAdapters : adapter is not of supported type and name: 'badHandle' ai:surface : dest" ) :
-			IECoreScene.ShaderNetworkAlgo.removeComponentConnectionAdapters( badNetwork )
+		# Check that we can unconvert a network converted using legacy adaptor blind data.
+
+		legacyConverted = IECore.ObjectReader( str( pathlib.Path( __file__ ).parent / "data" / "legacyComponentConnectionAdaptors.cob" ) ).read()
+		legacyConvertedBack = legacyConverted.copy()
+		IECoreScene.ShaderNetworkAlgo.removeComponentConnectionAdapters( legacyConvertedBack )
+		self.assertEqual( legacyConvertedBack, network )
 
 	def testConvertObjectVector( self ) :
 
@@ -691,5 +690,83 @@ def testColor4ComponentConnections( self ) :
 		IECoreScene.ShaderNetworkAlgo.removeComponentConnectionAdapters( unconverted )
 		self.assertEqual( unconverted, original )
 
+	def testCustomComponentConnectionAdaptors( self ) :
+
+		original = IECoreScene.ShaderNetwork(
+			shaders = {
+				"noise1" : IECoreScene.Shader( "noise", "ai:shader" ),
+				"image" : IECoreScene.Shader( "image", "ai:shader", { "missing_texture_color" : imath.Color4f( 1, 2, 3, 4 ) } ),
+				"noise2" : IECoreScene.Shader( "noise", "ai:shader", { "color1" : imath.Color3f( 1, 2, 3 ) } ),
+			},
+			connections = [
+				( ( "noise1", "out.r" ), ( "image", "missing_texture_color.a" ) ),
+				( ( "image", "out.r" ), ( "noise2", "color1.b" ) ),
+				( ( "image", "out.g" ), ( "noise2", "color1.g" ) ),
+				( ( "image", "out.b" ), ( "noise2", "color1.r" ) ),
+			],
+			output = ( "noise2", "out" )
+		)
+
+		for c in "rgbaxyz" :
+			IECoreScene.ShaderNetworkAlgo.registerSplitAdapter(
+				"ai", c, IECoreScene.Shader( "rgba_to_float", "ai:shader", { "mode" : c } ), "input", "out"
+			)
+
+		IECoreScene.ShaderNetworkAlgo.registerJoinAdapter(
+			"ai", IECore.Color3fData.staticTypeId(), IECoreScene.Shader( "float_to_rgb", "ai:shader" ), ( "r", "g", "b" ), "out"
+		)
+
+		IECoreScene.ShaderNetworkAlgo.registerJoinAdapter(
+			"ai", IECore.Color4fData.staticTypeId(), IECoreScene.Shader( "float_to_rgba", "ai:shader" ), ( "r", "g", "b", "a" ), "out"
+		)
+
+		def deregisterAdaptors() :
+
+			for c in "rgbaxyz" :
+				IECoreScene.ShaderNetworkAlgo.deregisterSplitAdapter( "ai", c )
+
+			IECoreScene.ShaderNetworkAlgo.deregisterJoinAdapter( "ai", IECore.Color3fData.staticTypeId() )
+			IECoreScene.ShaderNetworkAlgo.deregisterJoinAdapter( "ai", IECore.Color4fData.staticTypeId() )
+
+		self.addCleanup( deregisterAdaptors )
+
+		converted = original.copy()
+		IECoreScene.ShaderNetworkAlgo.addComponentConnectionAdapters( converted )
+
+		noise2Input = converted.input( ( "noise2", "color1" ) )
+		noise2InputShader = converted.getShader( noise2Input.shader )
+		self.assertEqual( noise2InputShader.name, "float_to_rgb" )
+		self.assertEqual( noise2InputShader.type, "ai:shader" )
+		for c in "rgb" :
+			cInput = converted.input( ( noise2Input.shader, c ) )
+			cInputShader = converted.getShader( cInput.shader )
+			self.assertEqual( cInputShader.name, "rgba_to_float" )
+			self.assertEqual( cInputShader.type, "ai:shader" )
+			self.assertEqual( cInputShader.parameters["mode"], IECore.StringData( { "r" : "b", "g" : "g", "b" : "r" }[c] ) )
+			self.assertEqual( converted.input( ( cInput.shader, "input" ) ), ( "image", "out" ) )
+
+		imageInput = converted.input( ( "image", "missing_texture_color" ) )
+		self.assertEqual( imageInput.name, "out" )
+		imageInputShader = converted.getShader( imageInput.shader )
+		self.assertEqual( imageInputShader.name, "float_to_rgba" )
+		self.assertEqual( imageInputShader.type, "ai:shader" )
+
+		noise1Split = converted.input( ( imageInput.shader, "a" ) )
+		noise1SplitShader = converted.getShader( noise1Split.shader )
+		self.assertEqual( noise1SplitShader.name, "rgba_to_float" )
+		self.assertEqual( noise1SplitShader.type, "ai:shader" )
+		self.assertEqual( converted.input( ( noise1Split.shader, "input" ) ), ( "noise1", "out" ) )
+		self.assertEqual( noise1SplitShader.parameters["mode"], IECore.StringData( "r" ) )
+
+		# Check that removing the adaptors gets us back to the original network.
+		# We deregister the adaptors first because we want the removal process to
+		# be completely independent of the current registrations.
+
+		deregisterAdaptors()
+
+		unconverted = converted.copy()
+		IECoreScene.ShaderNetworkAlgo.removeComponentConnectionAdapters( unconverted )
+		self.assertEqual( unconverted, original )
+
 if __name__ == "__main__":
 	unittest.main()
diff --git a/test/IECoreScene/data/legacyComponentConnectionAdaptors.cob b/test/IECoreScene/data/legacyComponentConnectionAdaptors.cob
new file mode 100644
index 0000000000..62d2b5a212
Binary files /dev/null and b/test/IECoreScene/data/legacyComponentConnectionAdaptors.cob differ