Previous topic

SDK Getting Started

Next topic

SDK Samples

Upgrading from HairWorks 1.1.x

If you have been using HairWorks 1.1.1 upgrading to 1.2.1 will require some changes to your code. In most respects the API is basically the same, but it now uses namespaces, enum types are wrapped, and the method names went from upper camel to lower camel. Other changes include...

  • Error codes have changed to Nv::Result
  • There is the concept of ‘ApiHandle’ and ‘ApiPtr’ to pass rendering API specific data to and from the API
  • The directory structure has changed for includes, such they have to be specified with a path
  • Use of a simple ‘NvCommon’ library which provides memory allocator, logging etc
  • The dll name has changed – for Dx11 its NvHairWorksDx11.dll
  • The shader code has a different prefix NvHair_, and uses directory paths
  • Methods that report results from GPU use the ‘async idiom’

DirectX12

NVIDIA HairWorks 1.2 for DirectX12 is under active development, and is NOT AVAILABLE within this public release. The documentation on DirectX12 usage and the Sdk headers are for illustrative purposes only and may be subject to change. Early access to HairWorks 1.2 for DirectX12 is available to developers on request at visualfx-licensing@nvidia.com

Library Structure

HairWorks 1.2.1 works with two other public libraries NvCore and NvCommon

Library Location Namespace Short Alias/File prefix Macro prefix
NvCore /Src/Nv/Core/1.0 nvidia Nv NV_
NvCommon /Src/Nv/Common nvidia::Common NvCo NV_CO_
HairWorks /Src/Nv/HairWorks nvidia::HairWorks NvHair NV_HAIR_

The file prefix is placed in front of any file that is part of that library, and by convention is always the same as the short namespace, which is just an alias to the full namespace. The Macro prefix is similarly just an upper case version of the short namespace alias. It is recommended in client code that in general the short namespace should be used.

HairWorks public API is dependent on two other sub libraries - NvCore and NvCommon. NvCore is a simple header only based C/C++ library which provides compiler abstraction, and simple type support.

NvCommon is a C++ library which provides basic facilities such as

  • Memory management - MemoryAllocator interface, FreeList
  • Containers - currently Array, PodBuffer (simplified array), Queue
  • Stream/file abstraction
  • Logging
  • String processing and management (via SubString and String)
  • Api handle abstraction

In order to use HairWorks 1.2.1 the only aspects that are used are the interfaces for MemoryAllocator, Logger, and the Api handle abstraction. NvCommon uses NvCore.

In terms of the file structure there are some other conventions. Generally source files and headers are designed to be as platform independent. Files that are specific to a ‘platform’ can be found in directory/subdirectories of ‘Platform’. For example in Nv/Common/Platform there are the following directories

  • Dx - Code that works on either DirectX11 or DirectX12
  • Dx11 - Code for DirectX 11
  • Dx12 - Code for DirectX 12
  • StdC - Code that relies on standard C libraries
  • Win - Code that relies on ‘Windows’ APIs

In hairworks there is the folder Nv/HairWorks/Platform/Win. This contains ‘NvHairWinLoadSdk.h’ - which is windows specific code for loading the dll

#include <Nv/HairWorks/Platform/Win/NvHairWinLoadSdk.h>

// ...
void someFunction()
{
        // Note That you probably want to implement your own versions of MemoryAllocator and Logger interfaces.

        NvHair::Sdk* sdk = NvHair::loadSdk(path, NV_HAIR_VERSION, MemoryAllocator::getInstance(), Logger::getInstance());

        // ...

        sdk->release();
}
// ...

Discussion

Previously most methods in HairWorks would return GFSDK_HAIR_RETURN_CODES which would indicate the success or otherwise of a call. In the update this has been replaced by Nv::Result (or NvResult) which is basically a platform independent version of COM HRESULT. Also if a method couldn’t fail in ‘normal operation’ with ‘correct parameters’ the return code was removed entirely.

To check for failure with Nv::Result you can use NV_FAILED or for success NV_SUCCEEDED. If you have a function returning a Nv::Result you can use the macro NV_RETURN_ON_FAIL, which will test, and return the NvResult on failure. Another change from the 1.1.1 is that if invalid or inappropriate parameters are passed to a call this failure is reported via the NvCo::Logger interface, on a debug build. On a Release build the behavior is undefined if invalid parameters are passed to a method. The previous enumeration for error codes from 1.1.1 is effectively aliased over Result as the ‘SubResult’ enum. Use getSubResult to convert a result to a SubResult.

On the ‘directory structure has changed’ – instead of setting ‘include’ as the include path, and all the headers being in there, you set the include path to the ‘src’ directory. The headers (and source that can be used externally) are available there. To access the sdk use:

#include <Nv/HairWorks/NvHairSdk.h>
#include <Nv/Platform/Dx11/Foundation/NvDx11Handle.h>

The first include is for the platform agnostic API, the other include gives you the tools to wrap/unwrap Dx11 types (like ID3D11Device*) into an ApiHandle. You can do this with:

ID3D11Device* device = ..;
NvCo::ApiHandle handle = NvCo::Dx11Type::getHandle(device);

You can see this in practice in the samples, found in the ‘samples’ directory.

The other nuance is sometimes you need to pass into the API or get from the API an array of Dx11 handles. You now have to wrap these with ApiPtr

For example:

// get standard shader resources for attribute interpolation
ID3D11ShaderResourceView* srvs[NvHair::ShaderResourceType::COUNT_OF];
m_hairSdk->getShaderResources(m_instanceId, NV_NULL, NV_COUNT_OF(srvs), NvCo::Dx11Type::getPtr(srvs));

On the last point about the shader code – the first difference is that all hairworks specific shader types and functions are prefixed with NvHair_.

The shader include is found in NvHairWorksShader and is NvHairShaderCommon.h. In the samples the effort is made to set up a DxIncludeHandler sopaths to find shader includes - this is not necessary though because the public shader includes are all in the same directory, so you could just place the HairWorks shader headers in the same directory as your own shader code and include without a path.

Async Methods

HairWorks 1.2.1 introduces a style of method call which is ‘asynchronous’ or ‘async’. These methods generally return data from the GPU where it it may take a while for the result to be available, and where blocking the thread may not be a good idea or may not even be possible. The tracking of async data is wrapped up with the AsyncHandle type.

The basic idea behind the async handle is to provide a ‘cookie’ to track a result, such that if the method call cannot give you the result now, you can you the cookie in the method to ask for the result in the future.

To start with the simplest scenario - where the implementation can only block, or can return a result immediately. If you know the implementation implements blocking calls and thats suitable for your purposes then you can just pass NV_NULL to the async handle pointer. If it is possible to block or return the result immediately the call will do that, if not it will return NV_FAIL.

To do a call that may work asynchronously you must pass in a pointer to a AsyncHandle. This should be initialized to NV_NULL initially, to indicate this is the initial call to set up the request. The method call can return:

NV_OK                           // The results were returned and can be used
NV_E_MISC_PENDING       // The AsyncHandle has had a cookie written to it. Use in the future to get the result

If it failed for another reason, this will be returned as an error.

With the AsyncHandle ‘cookie’ set, you can request that specific result again in the future. NOTE! When in this state, all the other input parameters are effectively ignored - the AsyncHandle encapsulates the parameterization and the state when the method was called. That said it is a good convention to aim to pass in the same parameters to maintain consistency.

With the AsyncHandle not being NV_NULL a method can return the following results

NV_OK                                           // The results were available and have been set. NOTE! Will also set AsyncHandle to NV_NULL because the request has completed.
NV_E_MISC_PENDING                       // The results were not available, will need to be requested again at some future point
NV_E_MISC_INVALID_HANDLE        // AsyncHandles can be destroyed before a request completes (say the instance is destroyed), or the requestCancelled. NOTE! If this is returned the AsyncHandle will be set to NV_NULL.

NOTE!

  1. If NV_OK or NV_E_MISC_INVALID_HANDLE is returned the call WILL set the AsyncHandle to NV_NULL - because it has either expired returned the result, or it was invalid.
  2. AsyncHandles are ‘safe’ in that if a request is made invalid, the object the request is being made on destroyed, the request canceled the API will detect the situation, clear the AsyncHandle and return NV_E_MISC_INVALID_HANDLE.
  3. It is typically an incorrect usage to store an async handle in a local variable, unless it is later copied somewhere where it can be used later.
  4. Typically you can’t use a tight loop asking for a result to make an async method appear immediately. At a minimum calls to onGpuWorkSubmitted may be needed, or perhaps some other OS/API specific behavior.
  5. Writing code that supports async will typically work transparently on a blocking/immediate returning API - as all that will happen is AsyncHandle is never set, and the result is returned.

Finally generally methods that take a AsyncHandle typically have a Bool parameter asyncLatest. This is only relevant if the call is on an async implementation. If false then the behavior is as previously described. If true the call will either kick off a whole new async request, or return the latest result. Either way the method will return NV_OK as it will have returned a new result. Implementing without the asyncLatest is a little convoluted and would look something like the following:

AsyncHandle prevHandle = m_asyncHandle;
Result res = hairSdk->computeStats(&m_asyncHandle, false, m_instanceId, m_stats);
if (res == NV_E_MISC_INVALID_HANDLE)
{
        // If the handle is now invalid, initiate a new request, and res is the result
        res = computeStats(&m_asyncHandle, false, m_instanceId, m_stats);
}
else if (NV_SUCCEEDED(res) && prevHandle)
{
        // We've just received an async result, initiate a new request (but ignore the result, as NV_OK means we have a new result which we do)
        computeStats(&m_asyncHandle, false, m_instanceId, m_stats);
}

In essence if an async call completes, it initiates another call (most likely an async call). Thus when using an async implementation this makes it simpler to ‘poll’ for the latest results by just repeatedly calling the method with asyncRepeat set to true. On a blocking/non async implementation asyncRepeat has no effect. Thus if the usage of the API is to poll, and the most recent results desired use asyncRepeat set to true. If more controlled access async results are required - such as the values of each result individually

Thus a simple way of using an async handle might be as follows:

struct AnInstance
{
        AnInstance():m_statsAsyncHandle(NV_NULL), m_instanceId(NvHair::INSTANCE_ID_NULL) { }
        Void update();

        NvHair::AsyncHandle m_statsAsyncHandle;                         ///< Probably belongs to object
        NvHair::InstanceId m_instancId;
        NvHair::Stats m_stats;
};

Void AnInstance::update()
{
        // NOTE! Passing true to asyncRepeat to make sure there is always an async request in flight.
        hairSdk->computeStats(&m_statsAsyncHandle, true, m_instanceId, m_stats);
}