[NVRHI] HLSL TraceRay always return no intersection!

I am implementing GPU path tracing using RTXGI and NVRHI.
The problem:
Intersection queries always fails when there are obvious occluders.

Here my simple shader code adapted from the RTXGI sample shader.

#pragma pack_matrix(column_major)

#include "TinyUniformSampleGenerator.hlsli"

cbuffer PerFrameCameraCB : register(b0, space0)
{
    float4x4 ViewToWorld;
    float3 CameraWs;
    uint StartSampleIndex;
    uint NbSamplePerPixel;
    float AspectRatio;
    float FovyScale;
    float FocalDistance;
    float LensRadius;
};

RaytracingAccelerationStructure SceneBVH : register(t0, space0);
RWTexture2D<float4> Output : register(u0, space0);

struct [raypayload] PrimaryRayPayload
{
    float hitDistance : read(caller) : write(closesthit, miss);
};

struct [raypayload] ShadowRayPayload
{
    bool hit : read(caller, anyhit) : write(caller, closesthit, anyhit);
};

struct Attributes
{
    float2 uv;
};

[shader("closesthit")]
void ClosestHitPrimary(inout PrimaryRayPayload payload : SV_RayPayload, in Attributes attrib : SV_IntersectionAttributes)
{
    uint packedDistance = asuint(RayTCurrent()) & (~0x1u);
    packedDistance |= HitKind() == HIT_KIND_TRIANGLE_FRONT_FACE ? 0x1 : 0x0;
    payload.hitDistance = asfloat(packedDistance);
}

[shader("anyhit")]
void AnyHitPrimary(inout PrimaryRayPayload payload : SV_RayPayload, in Attributes attrib : SV_IntersectionAttributes)
{
    // AcceptHit but continue looking for the closest hit
}

[shader("miss")]
void MissPrimary(inout PrimaryRayPayload payload : SV_RayPayload)
{
    payload.hitDistance = -1.0f;
}

[shader("closesthit")]
void ClosestHitShadow(inout ShadowRayPayload payload : SV_RayPayload, in Attributes attrib : SV_IntersectionAttributes)
{
    payload.hit = true;
}

[shader("anyhit")]
void AnyHitShadow(inout ShadowRayPayload payload : SV_RayPayload, in Attributes attrib : SV_IntersectionAttributes)
{
    payload.hit = true;
}

[shader("miss")]
void MissShadow(inout ShadowRayPayload payload : SV_RayPayload)
{
}

float2 uniformSampleUnitCircle(const float r)
{
    float theta = 6.2831853f * r;
    return float2(cos(theta), sin(theta));
}

RayDesc spawnRay(inout TinyUniformSampleGenerator sg, uint2 pixel, uint2 viewportRes)
{
    RayDesc ray;
    ray.Origin = CameraWs;

    float2 randFloat2 = sampleNext2D(sg);

    const float xReal = pixel.x + randFloat2.x;
    const float yReal = pixel.y + randFloat2.y;

    const float xNorm = xReal / (float) viewportRes.x;
    const float yNorm = yReal / (float) viewportRes.y;

    const float x = (2.0f * xNorm - 1.0f) * AspectRatio * FovyScale;
    const float y = (1.0f - 2.0f * yNorm) * FovyScale;

    const float4 targetPointVec4 = mul(float4(x * FocalDistance, y * FocalDistance, -FocalDistance, 1.0f), ViewToWorld);
    const float3 targetPoint = targetPointVec4.xyz;

    if (LensRadius > 1e-6f)
    {
        randFloat2 = sampleNext2D(sg);

        const float2 lensSampleCameraSpace = uniformSampleUnitCircle(randFloat2.x)
            * LensRadius * randFloat2.y;

        const float4 lensSampleWorld = mul(float4(lensSampleCameraSpace.x, lensSampleCameraSpace.y, 0.0, 1.0f), ViewToWorld);
        ray.Origin = lensSampleWorld.xyz;
    }

    ray.Direction = normalize(targetPoint - ray.Origin);
    ray.TMin = 1e-8f;
    ray.TMax = 1e8f;
    return ray;
}

[shader("raygeneration")]
void RayGen()
{
    const uint2 pixel = DispatchRaysIndex().xy;
    const uint2 viewportRes = DispatchRaysDimensions().xy;

    if (pixel.x >= viewportRes.x || pixel.y >= viewportRes.y)
        return;
    
    for (uint i = 0u; i < NbSamplePerPixel; ++i)
    {
        TinyUniformSampleGenerator usg;
        usg.init(pixel, StartSampleIndex + i);

        RayDesc ray = spawnRay(usg, pixel, viewportRes);
        PrimaryRayPayload hitData;
        TraceRay(SceneBVH, 0, 0xFF, 0, 0, 0, ray, hitData);

        if (hitData.hitDistance > 0.f)
        {
            Output[pixel] = float4(1, 1, 1, 1);
        }
        else
        {
            Output[pixel] = float4(1, 0, 0, 1);
        }
    }
}


This shader will generate this image. I expect white when there is an intersection:

While the rasterization shaders will output this:

Sky in blue means there is no intersection there! And only there!

The spawnRay function comes from my CPU path tracer and it works well

Here my per frame constant buffer:

    __declspec(align(16)) struct PerFrameCameraCB
    {
        Math::Mat4 ViewToWorld;
        Math::Vec3 CameraWs;
        UINT StartSampleIndex;
        UINT NbSamplePerPixel;
        FLOAT AspectRatio;
        FLOAT FovyScale;
        FLOAT FocalDistance;
        FLOAT LensRadius;
    };


Checked in PIX and it looks fine i think.

CPP side it is updated like this:

    void PathTracer::updateCameraConstantBuffer()
    {
        PerFrameCameraCB constants = {};

        constants.ViewToWorld = m_renderStartCameraProperties.viewToWorld;
        constants.CameraWs = m_renderStartCameraProperties.position;
        constants.StartSampleIndex = m_currentSample;
        constants.NbSamplePerPixel = s_nbSamplePerFrame;
        constants.AspectRatio = m_settings.m_aspectRatio;
        constants.FovyScale = m_renderStartCameraProperties.fovyScale;
        constants.FocalDistance = m_renderStartCameraProperties.focalDistance;
        constants.LensRadius = m_renderStartCameraProperties.lensRadius;

        m_commandList->writeBuffer(m_cameraConstantuffer, &constants, sizeof(PerFrameCameraCB));
    }

FovyScale is tan(fovy * 0.5f);

Here how the raytracing shader is created according to the RTXGI reference:

     void PathTracer::initShader()
    {
        BindingLayouts bindingLayouts = BuildBindingLayouts();

        ID3DBlob* shaderBlob = Dx12::Effect::readShader(_STRING("Pathtracing"));
        _ASSERT(shaderBlob);

        nvrhi::ShaderLibraryHandle shaderLibrary = m_device->createShaderLibrary(shaderBlob->GetBufferPointer(), shaderBlob->GetBufferSize());
        _ASSERT(shaderLibrary);

        m_permutation.shaderLibrary = shaderLibrary;

        nvrhi::rt::PipelineDesc pipelineDesc;
        for (int i = 0; i < DescriptorSetIDs::COUNT; ++i)
            pipelineDesc.globalBindingLayouts.push_back(bindingLayouts.dummy[i]);

        pipelineDesc.globalBindingLayouts[DescriptorSetIDs::Globals] = bindingLayouts.global;
        //pipelineDesc.globalBindingLayouts[DescriptorSetIDs::Bindless] = layouts.bindless;
        //pipelineDesc.globalBindingLayouts[DescriptorSetIDs::Denoiser] = layouts.denoiser;

        pipelineDesc.shaders = {
            { "", shaderLibrary->getShader("RayGen", nvrhi::ShaderType::RayGeneration), nullptr },
            { "", shaderLibrary->getShader("MissPrimary", nvrhi::ShaderType::Miss), nullptr },
            { "", shaderLibrary->getShader("MissShadow", nvrhi::ShaderType::Miss), nullptr },
        };

        pipelineDesc.hitGroups = {
            {
                "HitGroup",
                shaderLibrary->getShader("ClosestHitPrimary", nvrhi::ShaderType::ClosestHit),
                shaderLibrary->getShader("AnyHitPrimary", nvrhi::ShaderType::AnyHit),
                nullptr, // intersectionShader
                nullptr, // bindingLayout
                false    // isProceduralPrimitive
            },
            {
                "HitGroupShadow",
                shaderLibrary->getShader("ClosestHitShadow", nvrhi::ShaderType::ClosestHit),
                shaderLibrary->getShader("AnyHitShadow", nvrhi::ShaderType::AnyHit),
                nullptr, // intersectionShader
                nullptr, // bindingLayout
                false    // isProceduralPrimitive
            },
        }

The ViewToWorld matrix has GLM layout so it is column major as expected by my path tracing shader!

Strange thing? PIX report that my acceleration structure are properly created. So the problem is not there.

Here how BLAS transforms are computed:

void ConvertMat4ToAffineTransform(const Math::Mat4& mat, nvrhi::rt::AffineTransform& dest) 
{
    // GLM to Affine transform
    const Math::Vec3 translation = Math::Vec3(mat[3].x, mat[3].y, mat[3].z);
    dest[0] = mat[0].x; dest[1] = mat[0].y; dest[2] = mat[0].z; dest[3] = translation.x;
    dest[4] = mat[1].x; dest[5] = mat[1].y; dest[6] = mat[1].z; dest[7] = translation.y;
    dest[8] = mat[2].x; dest[9] = mat[2].y; dest[10] = mat[2].z; dest[11] = translation.z;
}

Here how path tracing is launched:

void PathTracer::performPathTracing()
{
    ID3D12GraphicsCommandList* d3dCmdList =
        m_commandList->getNativeObject(nvrhi::ObjectTypes::D3D12_GraphicsCommandList);
    AutoRenderEventTracker autoTracker(d3dCmdList, _STRING("PathTracer::performPathTracing"));


    // Transition pathTracerOutput
    m_commandList->setTextureState(m_renderingTexture.Get(), nvrhi::TextureSubresourceSet(0, 1, 0, 1), nvrhi::ResourceStates::UnorderedAccess);
    m_commandList->commitBarriers();

    nvrhi::rt::State state;
    for (int i = 0; i < DescriptorSetIDs::COUNT; ++i)
        state.bindings.push_back(m_dummyBindingSets[i]); // Unified Binding

    state.bindings[DescriptorSetIDs::Globals] = m_globalBindingSet;
    state.shaderTable = m_permutation.shaderTable;

    m_commandList->setRayTracingState(state);

    nvrhi::rt::DispatchRaysArguments args;
    args.width = this->getFinalWidth();
    args.height = this->getFinalHeight();
    m_commandList->dispatchRays(args);
}

What Am I missing?
I tried to transpose ViewToWorld but it doesnt change anything

constants.ViewToWorld = glm::transpose(m_renderStartCameraProperties.viewToWorld);

HELP NEEDED!

Here the complete C++ implementation:

using namespace Graphics::Renderer::Realtime::Dx12;

PathTracer::PathTracer()
{
	nvrhi::d3d12::DeviceDesc deviceDesc;

	m_messageCB = std::make_unique<PathTracerMessageCallback>();
	deviceDesc.errorCB = m_messageCB.get();

	deviceDesc.pDevice = m_dx12_Native_CommandContext.getContext().device;
	deviceDesc.pGraphicsCommandQueue = m_dx12_Native_CommandContext.getContext().commandQueue;

	m_device = nvrhi::d3d12::createDevice(deviceDesc);
	m_commandList = m_device->createCommandList();

	initBindingLayouts();
	initShaders();

	createStaticMemoryConstantAndStructuredBuffers();
}

bool PathTracer::isHardwareRaytracingAvailable()
{
	CComPtr<ID3D12Device> testDevice;
	D3D12_FEATURE_DATA_D3D12_OPTIONS5 featureSupportData = {};

	return SUCCEEDED(D3D12CreateDevice(m_dx12_Native_CommandContext.getContext().adapter, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&testDevice)))
		&& SUCCEEDED(testDevice->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS5, &featureSupportData, sizeof(featureSupportData)))
		&& featureSupportData.RaytracingTier != D3D12_RAYTRACING_TIER_NOT_SUPPORTED;
}

void PathTracer::onScene(Scene::SerializableScene* scene)
{
	freeAccelerationStructureMemory();
	freeTextureMemory();
	freeDynamicMemoryConstantAndStructuredBuffers();
}

PathTracer::BindingLayouts PathTracer::BuildBindingLayouts()
{
    BindingLayouts layouts;
    layouts.global = m_globalBindingLayout;

    for (int i = 0; i < DescriptorSetIDs::COUNT; ++i)
        layouts.dummy[i] = m_dummyLayouts[i];
    return layouts;
}

void PathTracer::initBindingLayouts()
{
	nvrhi::BindingLayoutDesc bindingLayoutDesc;
	bindingLayoutDesc.visibility = nvrhi::ShaderType::All;
	bindingLayoutDesc.registerSpaceIsDescriptorSet = false;
	for (int i = 0; i < DescriptorSetIDs::COUNT; ++i)
	{
		bindingLayoutDesc.registerSpace = i;
		m_dummyLayouts[i] = m_device->createBindingLayout(bindingLayoutDesc);

		nvrhi::BindingSetDesc dummyBindingDesc;
		m_dummyBindingSets[i] = m_device->createBindingSet(dummyBindingDesc, m_dummyLayouts[i]);
	}

	bindingLayoutDesc.registerSpace = DescriptorSetIDs::Globals;
	bindingLayoutDesc.bindings = {
		nvrhi::BindingLayoutItem::VolatileConstantBuffer(0),
		nvrhi::BindingLayoutItem::RayTracingAccelStruct(0),
		nvrhi::BindingLayoutItem::Texture_UAV(0),
	};

	m_globalBindingLayout = m_device->createBindingLayout(bindingLayoutDesc);
}

void PathTracer::initShaders()
{
	BindingLayouts bindingLayouts = BuildBindingLayouts();

	ID3DBlob* shaderBlob = Dx12::Effect::readShader(_STRING("Pathtracing"));
	_ASSERT(shaderBlob);

	nvrhi::ShaderLibraryHandle shaderLibrary = m_device->createShaderLibrary(shaderBlob->GetBufferPointer(), shaderBlob->GetBufferSize());
	_ASSERT(shaderLibrary);

	m_permutation.shaderLibrary = shaderLibrary;

	nvrhi::rt::PipelineDesc pipelineDesc;
	for (int i = 0; i < DescriptorSetIDs::COUNT; ++i)
		pipelineDesc.globalBindingLayouts.push_back(bindingLayouts.dummy[i]);

	pipelineDesc.globalBindingLayouts[DescriptorSetIDs::Globals] = bindingLayouts.global;
	//pipelineDesc.globalBindingLayouts[DescriptorSetIDs::Bindless] = layouts.bindless;
	//pipelineDesc.globalBindingLayouts[DescriptorSetIDs::Denoiser] = layouts.denoiser;

	pipelineDesc.shaders = {
		{ "", shaderLibrary->getShader("RayGen", nvrhi::ShaderType::RayGeneration), nullptr },
		{ "", shaderLibrary->getShader("MissPrimary", nvrhi::ShaderType::Miss), nullptr },
		{ "", shaderLibrary->getShader("MissShadow", nvrhi::ShaderType::Miss), nullptr },
	};

	pipelineDesc.hitGroups = {
		{
			"HitGroup",
			shaderLibrary->getShader("ClosestHitPrimary", nvrhi::ShaderType::ClosestHit),
			shaderLibrary->getShader("AnyHitPrimary", nvrhi::ShaderType::AnyHit),
			nullptr, // intersectionShader
			nullptr, // bindingLayout
			false    // isProceduralPrimitive
		},
		{
			"HitGroupShadow",
			shaderLibrary->getShader("ClosestHitShadow", nvrhi::ShaderType::ClosestHit),
			shaderLibrary->getShader("AnyHitShadow", nvrhi::ShaderType::AnyHit),
			nullptr, // intersectionShader
			nullptr, // bindingLayout
			false    // isProceduralPrimitive
		},
	};

	pipelineDesc.maxPayloadSize = sizeof(float) * 6;

	m_permutation.pipeline = m_device->createRayTracingPipeline(pipelineDesc);
	m_permutation.shaderTable = m_permutation.pipeline->createShaderTable();

	m_permutation.shaderTable->setRayGenerationShader("RayGen");
	m_permutation.shaderTable->addHitGroup("HitGroup");
	m_permutation.shaderTable->addHitGroup("HitGroupShadow");
	m_permutation.shaderTable->addMissShader("MissPrimary");
	m_permutation.shaderTable->addMissShader("MissShadow");
}

void PathTracer::createStaticMemoryConstantAndStructuredBuffers()
{
	m_cameraConstantuffer = 
		m_device->createBuffer(nvrhi::utils::CreateVolatileConstantBufferDesc(sizeof(PerFrameCameraCB), "PerFrameCameraCB", MAX_RENDER_PASS_CONSTANT_BUFFER_VERSIONS));

}

void PathTracer::freeStaticMemoryConstantAndStructuredBuffers()
{
	m_cameraConstantuffer.Reset();
}

void PathTracer::createDynamicMemoryConstantAndStructuredBuffers()
{
	// Lights first.
	{
		uint32_t elementCount = Effect::LightStructuredBufferSingleton::instance()->getLightCount();
		uint32_t bufferByteSize = elementCount * sizeof(Effect::LightStructuredBuffer::DX12Light);

		nvrhi::BufferDesc bufferDesc = nvrhi::BufferDesc()
			.setDebugName("LightStructuredBuffer upload heap size = " + std::to_string(elementCount))//nvrhi overrided names need to set it.
			.setByteSize(bufferByteSize)
			.setStructStride(sizeof(Effect::LightStructuredBuffer::DX12Light))
			.setInitialState(nvrhi::ResourceStates::PixelShaderResource | nvrhi::ResourceStates::NonPixelShaderResource)
			.setKeepInitialState(true)
			.setCanHaveUAVs(false);
			
		m_lights = m_device->createHandleForNativeBuffer(
			nvrhi::ObjectTypes::D3D12_Resource,
			Effect::LightStructuredBufferSingleton::instance()->getLightStructuredBufferUploadHeap(),
			bufferDesc
		);
	}

	// Now materials.
	{
		const EntityIdentifierArray& materials = m_currentScene->getModel()->getMaterials();
		uint32_t elementCount = std::max((uint32_t)materials.size(), 1u);
		uint32_t bufferByteSize = elementCount * sizeof(Realtime::Dx12::Effect::MaterialStructuredBuffer::DX12Material);

		nvrhi::BufferDesc bufferDesc = nvrhi::BufferDesc()
			.setDebugName("Material Structured buffer")
			.setByteSize(bufferByteSize)
			.setStructStride(sizeof(Realtime::Dx12::Effect::MaterialStructuredBuffer::DX12Material))
			.setInitialState(nvrhi::ResourceStates::PixelShaderResource | nvrhi::ResourceStates::NonPixelShaderResource)
			.setKeepInitialState(true)
			.setCanHaveUAVs(false);

		m_materials = m_device->createBuffer(bufferDesc);
		// TODO fill array with material data.
	}
}

void PathTracer::freeDynamicMemoryConstantAndStructuredBuffers()
{
	m_lights.Reset();
	m_materials.Reset();
}

void PathTracer::onStartRender(Math::Uvec2 realtimeViewSize, bool forceCreateIntersector)
{
	onTryStopRender();

	m_settings = m_currentScene->getCurrentRenderSettings();
	if (m_settings.m_finalRender)
	{
		const Math::Uvec2 renderResolution = m_settings.computeRenderResolution(false);
		m_settings.m_aspectRatio = (float)renderResolution.x / (float)renderResolution.y;
	}
	else
	{
		m_settings.m_nbSamples = 20000;
		m_settings.m_aspectRatio = (float)realtimeViewSize.x / (float)realtimeViewSize.y;
	}

	tryCreateOuputBuffers(realtimeViewSize);

	m_stopRender = false;
	m_renderStartCameraProperties = m_currentScene->getCamera()->buildCameraRayProps();
	m_hasRenderedOnce = false;
	m_currentSample = 0u;

	m_renderingThread.reset(new std::thread(&PathTracer::renderingThreadProcedure, this, forceCreateIntersector));
}

void PathTracer::updateCameraConstantBuffer()
{
	PerFrameCameraCB constants = {};

	constants.ViewToWorld = m_renderStartCameraProperties.viewToWorld;
	constants.CameraWs = m_renderStartCameraProperties.position;
	constants.StartSampleIndex = m_currentSample;
	constants.NbSamplePerPixel = s_nbSamplePerFrame;
	constants.AspectRatio = m_settings.m_aspectRatio;
	constants.FovyScale = m_renderStartCameraProperties.fovyScale;
	constants.FocalDistance = m_renderStartCameraProperties.focalDistance;
	constants.LensRadius = m_renderStartCameraProperties.lensRadius;

	m_commandList->writeBuffer(m_cameraConstantuffer, &constants, sizeof(PerFrameCameraCB));
}

void PathTracer::createBindingSets()
{
	nvrhi::BindingSetDesc bindingSetDesc;
	bindingSetDesc.bindings = {
		nvrhi::BindingSetItem::ConstantBuffer(0, m_cameraConstantuffer),
		nvrhi::BindingSetItem::RayTracingAccelStruct(0, m_tlas),
		nvrhi::BindingSetItem::Texture_UAV(0, m_renderingTexture)
	};

	m_globalBindingSet = m_device->createBindingSet(bindingSetDesc, m_globalBindingLayout);
}

void PathTracer::renderingThreadProcedure(bool forceCreateIntersector)
{
	if (isHardwareRaytracingAvailable())
	{
		if (!m_tlas || forceCreateIntersector)
		{
			freeAccelerationStructureMemory();
			createAccelerationStructures();
		}

		freeDynamicMemoryConstantAndStructuredBuffers();
		createDynamicMemoryConstantAndStructuredBuffers();
		createBindingSets();

		bool isOver = false;
		while (!isOver && !m_stopRender)
		{
			m_commandList->open();

			updateCameraConstantBuffer();

			performPathTracing();

			updatePathTracingCPURenderBuffers_And_ExecuteCommandList();

			// We rendered once.
			m_hasRenderedOnce = true;

			m_currentSample += s_nbSamplePerFrame;
			isOver = (m_currentSample + 1 >= m_settings.m_nbSamples);
		}

		m_device->waitForIdle();

		stopWork();

	}
	else
	{
		const std::wstring message = _STRING("DirectX Raytracing is not supported by your OS,") 
			 + Hardware::HardwareInformationSingleton::instance()->getVendorString() + _STRING(" GPU and/or driver");

		m_onUnsupported(message);
	}
}

void PathTracer::performPathTracing()
{
	ID3D12GraphicsCommandList* d3dCmdList =
		m_commandList->getNativeObject(nvrhi::ObjectTypes::D3D12_GraphicsCommandList);
	AutoRenderEventTracker autoTracker(d3dCmdList, _STRING("HybridRenderer::performPathTracing"));


	// Transition pathTracerOutput
	m_commandList->setTextureState(m_renderingTexture.Get(), nvrhi::TextureSubresourceSet(0, 1, 0, 1), nvrhi::ResourceStates::UnorderedAccess);
	m_commandList->commitBarriers();

	nvrhi::rt::State state;
	for (int i = 0; i < DescriptorSetIDs::COUNT; ++i)
		state.bindings.push_back(m_dummyBindingSets[i]); // Unified Binding

	state.bindings[DescriptorSetIDs::Globals] = m_globalBindingSet;
	state.shaderTable = m_permutation.shaderTable;

	m_commandList->setRayTracingState(state);

	nvrhi::rt::DispatchRaysArguments args;
	args.width = this->getFinalWidth();
	args.height = this->getFinalHeight();
	m_commandList->dispatchRays(args);
}

void PathTracer::updatePathTracingCPURenderBuffers_And_ExecuteCommandList()
{
	nvrhi::TextureSlice slice;
	slice.arraySlice = 0;
	slice.depth = 1;
	slice.height = this->getFinalHeight();
	slice.mipLevel = 0;
	slice.width = this->getFinalWidth();

	m_commandList->copyTexture(m_readBackTexture.Get(), slice, m_renderingTexture, slice);
	m_commandList->close();

	m_device->executeCommandList(m_commandList);

	// Map the staging texture and get the CPU-side memory pointer
	size_t rowPitch = 0;
	void* data = m_device->mapStagingTexture(m_readBackTexture.Get(), slice, nvrhi::CpuAccessMode::Read, &rowPitch);
	_ASSERT(data);

	uint8_t* mappedBytes = static_cast<uint8_t*>(data);
	size_t destPixelSizeInBytes = 16; // float4 is 16 bytes
	_ASSERT(m_transientImage->getFormat() == Graphics::Texture::ImageFormat::RGBA32F);
	size_t destRowSizeInBytes = this->getFinalWidth() * destPixelSizeInBytes;

	tbb::parallel_for(size_t(0), (size_t)this->getFinalHeight(), [&](size_t y) {
		uint8_t* destRow = m_transientImage->getMutableRawData() + (y * destRowSizeInBytes);
		uint8_t* srcRow = static_cast<uint8_t*>(mappedBytes) + (y * rowPitch);
		std::memcpy(destRow, srcRow, destRowSizeInBytes);
		});

	m_device->unmapStagingTexture(m_readBackTexture.Get());

	std::lock_guard<std::recursive_mutex> lock(m_renderBuffersMutex);
	Graphics::Texture::Helper::RGBA32F_to_RGB32F(*m_transientImage, *m_outputImage);
}

void PathTracer::tryCreateOuputBuffers(const Math::Uvec2& realtimeViewSize)
{
	const Math::Uvec2 renderResolution = m_settings.computeRenderResolution(false);

	const uint32_t newWidth = m_settings.m_finalRender ? renderResolution.x : realtimeViewSize.x;
	const uint32_t newHeight = m_settings.m_finalRender ? renderResolution.y : realtimeViewSize.y;
	const bool newAllowEntityId = m_settings.m_finalRender && m_settings.m_aov;

	const bool allocate = !m_outputImage ||
		m_outputImage->getWidth() != newWidth ||
		m_outputImage->getHeight() != newHeight ||
		newAllowEntityId != (m_entityIdTexture != nullptr);

	if (!allocate)
	{
		return;
	}

	m_outputImage = std::make_unique<Texture::RGB32FImage>(newWidth, newHeight);
	m_transientImage = std::make_unique<Texture::RGBA32FImage>(newWidth, newHeight);

	{
		auto textureDesc = nvrhi::TextureDesc()
			.setDimension(nvrhi::TextureDimension::Texture2D)
			.setWidth(newWidth)
			.setHeight(newHeight)
			.setIsUAV(true)
			.setFormat(nvrhi::Format::RGBA32_FLOAT)
			.enableAutomaticStateTracking(nvrhi::ResourceStates::UnorderedAccess)
			.setInitialState(nvrhi::ResourceStates::UnorderedAccess)
			.setKeepInitialState(true)
			.setDebugName("Chronos rendering texture");

		m_renderingTexture = m_device->createTexture(textureDesc);
	}

	{
		auto textureDesc = nvrhi::TextureDesc()
			.setDimension(nvrhi::TextureDimension::Texture2D)
			.setWidth(newWidth)
			.setHeight(newHeight)
			.setFormat(nvrhi::Format::RGBA32_FLOAT)
			.enableAutomaticStateTracking(nvrhi::ResourceStates::ShaderResource)
			.setDebugName("Chronos staging texture");

		m_readBackTexture = m_device->createStagingTexture(textureDesc, nvrhi::CpuAccessMode::Read);
	}

	{
		auto textureDesc = nvrhi::TextureDesc()
			.setDimension(nvrhi::TextureDimension::Texture2D)
			.setWidth(newWidth)
			.setHeight(newHeight)
			.setIsUAV(true)
			.setFormat(nvrhi::Format::RGBA32_FLOAT)
			.enableAutomaticStateTracking(nvrhi::ResourceStates::UnorderedAccess)
			.setInitialState(nvrhi::ResourceStates::UnorderedAccess)
			.setKeepInitialState(true)
			.setDebugName("Chronos final Texture");

		m_accumulationTexture = m_device->createTexture(textureDesc);
	}

	{
		auto textureDesc = nvrhi::TextureDesc()
			.setDimension(nvrhi::TextureDimension::Texture2D)
			.setWidth(newWidth)
			.setHeight(newHeight)
			.setIsUAV(true)
			.setFormat(nvrhi::Format::R32_UINT)
			.enableAutomaticStateTracking(nvrhi::ResourceStates::UnorderedAccess)
			.setDebugName("Chronos entity id Texture");

		m_entityIdTexture = m_device->createTexture(textureDesc);
	}
}

bool PathTracer::isWorking() const
{
	return m_renderingThread.get() != nullptr;
}

void PathTracer::stopWork()
{
	_ASSERT(std::this_thread::get_id() == m_renderingThread->get_id());

	std::lock_guard<std::mutex> lock(m_stopMutex);

	m_stopRender = true;
}

void PathTracer::onTryStopRender()
{
	if (!isWorking())
		return;

	_ASSERT(m_renderingThread);
	_ASSERT(std::this_thread::get_id() != m_renderingThread->get_id());

	m_stopMutex.lock();

	m_hasRenderedOnce = false;
	m_stopRender = true;

	if (m_renderingThread->joinable())
	{
		m_stopMutex.unlock();
		m_renderingThread->join();
	}
	else
	{
		_TRACE_STD("PathTracer::onTryStopRender - not joinable");
		m_stopMutex.unlock();
	}

	m_renderingThread.reset();
}

bool PathTracer::hasRenderedOnce() const
{
	return m_hasRenderedOnce;
}

void PathTracer::saveRender(const std::wstring& path) const
{

}

void PathTracer::saveRender(const Graphics::Texture::Image* render, const std::wstring& path) const
{

}

uint32_t PathTracer::getRenderCurrentSample() const
{
	return m_currentSample;
}

void PathTracer::freeTextureMemory()
{
	m_renderingTexture.Reset();
	m_accumulationTexture.Reset();
	m_entityIdTexture.Reset();
	m_readBackTexture.Reset();
	m_outputImage.reset();
	m_transientImage.reset();
}

void PathTracer::freeAccelerationStructureMemory()
{
	m_tlas.Reset();
	m_vertexBuffers.clear();
	m_indexBuffers.clear();
	m_vertexBufferCounts.clear();
	m_indexBufferCounts.clear();
	m_triangles.clear();
	m_blases.clear();
	m_blasDescs.clear();
	m_instanceDescs.clear();

	if (m_instanceTransforms)
		delete[] m_instanceTransforms;
}

void ConvertMat4ToAffineTransform(const glm::mat4& mat, nvrhi::rt::AffineTransform& dest) {

	const Math::Vec3 translation = Math::Vec3(mat[3].x, mat[3].y, mat[3].z);
	dest[0] = mat[0].x; dest[1] = mat[0].y; dest[2] = mat[0].z; dest[3] = translation.x;
	dest[4] = mat[1].x; dest[5] = mat[1].y; dest[6] = mat[1].z; dest[7] = translation.y;
	dest[8] = mat[2].x; dest[9] = mat[2].y; dest[10] = mat[2].z; dest[11] = translation.z;
}

void PathTracer::createAccelerationStructures()
{
	const DX12Model* dx12Model = static_cast<const DX12Model*>(m_currentScene->getModel().get());

	// First find the number of meshes.
	uint32_t meshCount = 0;
	for (auto& meshHandleIt : dx12Model->getMeshHandlesByGroup())
	{
		meshCount += (uint32_t)meshHandleIt.second.size();
	}

	// Allocate memory
	m_vertexBuffers.reserve(meshCount);
	m_indexBuffers.reserve(meshCount);

	m_vertexBufferCounts.reserve(meshCount);
	m_indexBufferCounts.reserve(meshCount);

	m_triangles.reserve(meshCount);
	m_blasDescs.reserve(meshCount);

	m_instanceDescs.reserve(meshCount);
	m_instanceTransforms = new nvrhi::rt::AffineTransform[meshCount];

	std::vector<Math::Mat4>transforms;
	transforms.reserve(meshCount);

	// Build geometries.
	uint32_t i = 0;
	for (auto& meshHandleIt : dx12Model->getMeshHandlesByGroup())
	{
		for (auto* meshHandle : meshHandleIt.second)
		{
			{
				nvrhi::BufferDesc vertexBufferDesc;
				vertexBufferDesc.setIsVertexBuffer(true);
				vertexBufferDesc.setIsAccelStructBuildInput(true);
				vertexBufferDesc.setInitialState(nvrhi::ResourceStates::VertexBuffer | nvrhi::ResourceStates::ConstantBuffer | nvrhi::ResourceStates::NonPixelShaderResource);
				vertexBufferDesc.setKeepInitialState(true);
				vertexBufferDesc.setByteSize(sizeof(FullVertex) * meshHandle->vertexBuffer.count);
				vertexBufferDesc.setStructStride(sizeof(FullVertex));

				m_vertexBuffers.push_back(
					m_device->createHandleForNativeBuffer(
						nvrhi::ObjectTypes::D3D12_Resource,
						static_cast<nvrhi::Object>(meshHandle->vertexBuffer.buffer),
						vertexBufferDesc)
				);
				m_vertexBufferCounts.push_back(meshHandle->vertexBuffer.count);
			}
			{
				nvrhi::BufferDesc indexBufferDesc;
				indexBufferDesc.setIsIndexBuffer(true);
				indexBufferDesc.setIsAccelStructBuildInput(true);
				indexBufferDesc.setInitialState(nvrhi::ResourceStates::VertexBuffer | nvrhi::ResourceStates::ConstantBuffer | nvrhi::ResourceStates::NonPixelShaderResource);
				indexBufferDesc.setKeepInitialState(true);
				indexBufferDesc.setByteSize(sizeof(uint32_t) * meshHandle->indexBuffer.count);
				indexBufferDesc.setStructStride(sizeof(uint32_t));

				m_indexBuffers.push_back(
					m_device->createHandleForNativeBuffer(
						nvrhi::ObjectTypes::D3D12_Resource,
						static_cast<nvrhi::Object>(meshHandle->indexBuffer.buffer),
						indexBufferDesc)
				);

				m_indexBufferCounts.push_back(meshHandle->indexBuffer.count);
			}

			Graphics::Model::DatabaseMeshGroupPtr groupPtr = Graphics::Model::getMeshGroupPtr_FromEntity(meshHandleIt.first);
			const Math::Mat4& glmTransform = groupPtr->getTransform()->getMatrix();
			ConvertMat4ToAffineTransform(glmTransform, m_instanceTransforms[i]);

			++i;
		}
	}

	// Build native triangles.
	for (uint32_t i = 0; i < m_vertexBuffers.size(); ++i)
	{
		nvrhi::rt::GeometryTriangles triangles = nvrhi::rt::GeometryTriangles()
			.setVertexBuffer(m_vertexBuffers[i])
			.setVertexFormat(nvrhi::Format::RGB32_FLOAT)
			.setVertexCount(m_vertexBufferCounts[i])
			.setVertexStride(sizeof(Graphics::FullVertex))
			.setIndexBuffer(m_indexBuffers[i])
			.setIndexCount(m_indexBufferCounts[i])
			.setIndexFormat(nvrhi::Format::R32_UINT);
			
		m_triangles.push_back(triangles);
	}

	// Create blases.
	for (uint32_t i = 0; i < m_vertexBuffers.size(); ++i)
	{
		auto blasDesc = nvrhi::rt::AccelStructDesc()
			.setDebugName("BLAS " + std::to_string(i))
			.setIsTopLevel(false)
			.addBottomLevelGeometry(nvrhi::rt::GeometryDesc().setTriangles(m_triangles[i]));

		m_blases.push_back(m_device->createAccelStruct(blasDesc));
		m_blasDescs.push_back(blasDesc);
	}
	
	// Create the TLAS.
	{
		auto tlasDesc = nvrhi::rt::AccelStructDesc()
			.setDebugName("TLAS")
			.setIsTopLevel(true)
			.setTopLevelMaxInstances(m_vertexBuffers.size());

		m_tlas = m_device->createAccelStruct(tlasDesc);
	}

	// Build Acceleration structures.
	m_commandList->open();

	std::vector<nvrhi::rt::InstanceDesc> instanceDescs(m_vertexBuffers.size());
	for (uint32_t i = 0u; i < instanceDescs.size(); ++i)
	{
		// Build the BLAS using the geometry array populated earlier.
		// It's also possible to obtain the descriptor from the BLAS object using getDesc()
		// and write the vertex and index buffer references into that descriptor again
		// because NVRHI erases those when it creates the AS object.
		m_commandList->buildBottomLevelAccelStruct(m_blases[i],
			m_blasDescs[i].bottomLevelGeometries.data(), m_blasDescs[i].bottomLevelGeometries.size());

		instanceDescs[i] = nvrhi::rt::InstanceDesc()
			.setBLAS(m_blases[i])
			.setFlags(nvrhi::rt::InstanceFlags::None)
			.setTransform(m_instanceTransforms[i])
			.setInstanceID(i);
	}

	m_commandList->buildTopLevelAccelStruct(m_tlas, instanceDescs.data(), instanceDescs.size());

	m_commandList->close();
	m_device->executeCommandList(m_commandList);

}


Managed to have this work. I wasn’t setting the instances instance mask when building the TLAS.
So this:

instanceDescs[i] = nvrhi::rt::InstanceDesc()
            .setBLAS(m_blases[i])
            .setFlags(nvrhi::rt::InstanceFlags::None)
            .setTransform(m_instanceTransforms[i])
            .setInstanceID(i);

Need to be changed to this:

instanceDescs[i] = nvrhi::rt::InstanceDesc()
                .setBLAS(m_blases[i])
                .setFlags(nvrhi::rt::InstanceFlags::None)
                .setTransform(m_instanceTransforms[i])
                .setInstanceID(i)
                .setInstanceMask(1);