Files
DoomRTX/neo/engine/opengl/gl_d3d12raylight.cpp
Justin Marshall c795aba899 Spec fixes
2026-05-28 22:16:09 -07:00

9473 lines
302 KiB
C++

/*
===========================================================================
IceTech GPL Source Code
Copyright (C) 2026 Justin Marshall
This file is part of the IceTech GPL Source Code (?IceTech Source Code?).
IceTech Source Code is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
IceTech Source Code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with IceTech Source Code. If not, see <http://www.gnu.org/licenses/>.
If you have questions concerning this license or the applicable additional terms, you may contact in writing Justin Marshall, justinmarshall20@gmail.com
===========================================================================
*/
#include "opengl.h"
#include <windows.h>
#include <d3d12.h>
#include <dxgi1_6.h>
#include <d3dcompiler.h>
#include <dxcapi.h>
#include <wrl/client.h>
#include <stdint.h>
#include <vector>
#include <thread>
#include <atomic>
#include <mutex>
#include <algorithm>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <wchar.h>
#include <assert.h>
#include <math.h>
#pragma comment(lib, "dxcompiler.lib")
#pragma comment(lib, "d3dcompiler.lib")
using Microsoft::WRL::ComPtr;
extern float com_pathTracingGpuMsec;
// ============================================================
// Logging / checks
// ============================================================
static void glRaytracingLog(const char* fmt, ...)
{
char buffer[4096];
va_list args;
va_start(args, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, args);
va_end(args);
OutputDebugStringA(buffer);
OutputDebugStringA("\n");
}
static void glRaytracingFatal(const char* fmt, ...)
{
char buffer[4096];
va_list args;
va_start(args, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, args);
va_end(args);
OutputDebugStringA(buffer);
OutputDebugStringA("\n");
MessageBoxA(nullptr, buffer, "glRaytracing Fatal", MB_OK | MB_ICONERROR);
DebugBreak();
}
#define GLR_CHECK(x) \
do { HRESULT _hr = (x); if (FAILED(_hr)) { glRaytracingFatal("HRESULT 0x%08X failed at %s:%d", (unsigned)_hr, __FILE__, __LINE__); return 0; } } while (0)
#define GLR_CHECKV(x) \
do { HRESULT _hr = (x); if (FAILED(_hr)) { glRaytracingFatal("HRESULT 0x%08X failed at %s:%d", (unsigned)_hr, __FILE__, __LINE__); return; } } while (0)
static void glRaytracingBeginPixEvent(ID3D12GraphicsCommandList* cl, const wchar_t* name)
{
if (!cl || !name)
return;
cl->BeginEvent(0, name, (UINT)((wcslen(name) + 1) * sizeof(wchar_t)));
}
static void glRaytracingEndPixEvent(ID3D12GraphicsCommandList* cl)
{
if (cl)
cl->EndEvent();
}
static void glRaytracingSetPixMarker(ID3D12GraphicsCommandList* cl, const wchar_t* name)
{
if (!cl || !name)
return;
cl->SetMarker(0, name, (UINT)((wcslen(name) + 1) * sizeof(wchar_t)));
}
static void glRaytracingSetObjectName(ID3D12Object* object, const wchar_t* name)
{
if (object && name)
object->SetName(name);
}
class glRaytracingScopedPixEvent
{
public:
glRaytracingScopedPixEvent(ID3D12GraphicsCommandList* commandList, const wchar_t* name)
: cl(commandList)
{
glRaytracingBeginPixEvent(cl, name);
}
~glRaytracingScopedPixEvent()
{
glRaytracingEndPixEvent(cl);
}
private:
ID3D12GraphicsCommandList* cl;
};
#define GLR_CONCAT_INNER(a, b) a##b
#define GLR_CONCAT(a, b) GLR_CONCAT_INNER(a, b)
#define GLR_PIX_EVENT(cl, name) glRaytracingScopedPixEvent GLR_CONCAT(glrPixEvent_, __LINE__)(cl, name)
#define GLR_PIX_MARKER(cl, name) glRaytracingSetPixMarker(cl, name)
// ============================================================
// Helpers
// ============================================================
static UINT64 glRaytracingAlignUp(UINT64 v, UINT64 a)
{
return (v + (a - 1)) & ~(a - 1);
}
template<typename T>
static T glRaytracingClamp(T v, T lo, T hi)
{
return (v < lo) ? lo : ((v > hi) ? hi : v);
}
static void glRaytracingSetIdentity4x4(float* m)
{
if (!m)
return;
memset(m, 0, sizeof(float) * 16);
m[0] = 1.0f;
m[5] = 1.0f;
m[10] = 1.0f;
m[15] = 1.0f;
}
static int glRaytracingInvertMatrix4x4(const float* m, float* out)
{
if (!m || !out)
return 0;
float a[4][8];
for (int r = 0; r < 4; ++r)
{
for (int c = 0; c < 4; ++c)
a[r][c] = m[r * 4 + c];
for (int c = 0; c < 4; ++c)
a[r][4 + c] = (r == c) ? 1.0f : 0.0f;
}
for (int col = 0; col < 4; ++col)
{
int pivot = col;
float best = a[col][col] < 0.0f ? -a[col][col] : a[col][col];
for (int r = col + 1; r < 4; ++r)
{
const float v = a[r][col] < 0.0f ? -a[r][col] : a[r][col];
if (v > best)
{
best = v;
pivot = r;
}
}
if (best <= 1.0e-8f)
return 0;
if (pivot != col)
{
for (int c = 0; c < 8; ++c)
{
const float tmp = a[col][c];
a[col][c] = a[pivot][c];
a[pivot][c] = tmp;
}
}
const float invPivot = 1.0f / a[col][col];
for (int c = 0; c < 8; ++c)
a[col][c] *= invPivot;
for (int r = 0; r < 4; ++r)
{
if (r == col)
continue;
const float f = a[r][col];
if (f == 0.0f)
continue;
for (int c = 0; c < 8; ++c)
a[r][c] -= f * a[col][c];
}
}
for (int r = 0; r < 4; ++r)
{
for (int c = 0; c < 4; ++c)
out[r * 4 + c] = a[r][4 + c];
}
return 1;
}
static DXGI_FORMAT glRaytracingGetSrvFormatForDepth(DXGI_FORMAT fmt)
{
switch (fmt)
{
case DXGI_FORMAT_D32_FLOAT: return DXGI_FORMAT_R32_FLOAT;
case DXGI_FORMAT_D24_UNORM_S8_UINT: return DXGI_FORMAT_R24_UNORM_X8_TYPELESS;
case DXGI_FORMAT_D16_UNORM: return DXGI_FORMAT_R16_UNORM;
default: return fmt;
}
}
struct glRaytracingBuffer_t
{
ComPtr<ID3D12Resource> resource;
UINT64 size;
D3D12_GPU_VIRTUAL_ADDRESS gpuVA;
glRaytracingBuffer_t()
{
size = 0;
gpuVA = 0;
}
};
static glRaytracingBuffer_t glRaytracingCreateBuffer(
ID3D12Device* device,
UINT64 size,
D3D12_HEAP_TYPE heapType,
D3D12_RESOURCE_STATES initialState,
D3D12_RESOURCE_FLAGS flags)
{
glRaytracingBuffer_t out;
D3D12_HEAP_PROPERTIES hp = {};
hp.Type = heapType;
D3D12_RESOURCE_DESC rd = {};
rd.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
rd.Width = size;
rd.Height = 1;
rd.DepthOrArraySize = 1;
rd.MipLevels = 1;
rd.Format = DXGI_FORMAT_UNKNOWN;
rd.SampleDesc.Count = 1;
rd.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
rd.Flags = flags;
HRESULT hr = device->CreateCommittedResource(
&hp,
D3D12_HEAP_FLAG_NONE,
&rd,
initialState,
nullptr,
IID_PPV_ARGS(&out.resource));
if (FAILED(hr))
{
glRaytracingFatal("CreateCommittedResource failed 0x%08X", (unsigned)hr);
return out;
}
out.size = size;
out.gpuVA = out.resource->GetGPUVirtualAddress();
return out;
}
struct glRaytracingTexture_t
{
ComPtr<ID3D12Resource> resource;
UINT width;
UINT height;
DXGI_FORMAT format;
D3D12_RESOURCE_STATES state;
glRaytracingTexture_t()
{
width = 0;
height = 0;
format = DXGI_FORMAT_UNKNOWN;
state = D3D12_RESOURCE_STATE_COMMON;
}
};
static glRaytracingTexture_t glRaytracingCreateTexture2D(
ID3D12Device* device,
UINT width,
UINT height,
DXGI_FORMAT format,
D3D12_RESOURCE_STATES initialState,
D3D12_RESOURCE_FLAGS flags)
{
glRaytracingTexture_t out;
D3D12_HEAP_PROPERTIES hp = {};
hp.Type = D3D12_HEAP_TYPE_DEFAULT;
D3D12_RESOURCE_DESC rd = {};
rd.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
rd.Width = width;
rd.Height = height;
rd.DepthOrArraySize = 1;
rd.MipLevels = 1;
rd.Format = format;
rd.SampleDesc.Count = 1;
rd.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
rd.Flags = flags;
HRESULT hr = device->CreateCommittedResource(
&hp,
D3D12_HEAP_FLAG_NONE,
&rd,
initialState,
nullptr,
IID_PPV_ARGS(&out.resource));
if (FAILED(hr))
{
glRaytracingFatal("CreateCommittedResource texture failed 0x%08X", (unsigned)hr);
return out;
}
out.width = width;
out.height = height;
out.format = format;
out.state = initialState;
return out;
}
static void glRaytracingMapCopy(ID3D12Resource* res, const void* src, size_t bytes)
{
void* dst = nullptr;
HRESULT hr = res->Map(0, nullptr, &dst);
if (FAILED(hr))
{
glRaytracingFatal("Map failed 0x%08X", (unsigned)hr);
return;
}
memcpy(dst, src, bytes);
res->Unmap(0, nullptr);
}
static void glRaytracingTransition(
ID3D12GraphicsCommandList* cmd,
ID3D12Resource* res,
D3D12_RESOURCE_STATES before,
D3D12_RESOURCE_STATES after)
{
if (!res || before == after)
return;
D3D12_RESOURCE_BARRIER b = {};
b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
b.Transition.pResource = res;
b.Transition.StateBefore = before;
b.Transition.StateAfter = after;
b.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
cmd->ResourceBarrier(1, &b);
}
static D3D12_CPU_DESCRIPTOR_HANDLE glRaytracingOffsetCpu(D3D12_CPU_DESCRIPTOR_HANDLE h, UINT stride, UINT idx)
{
h.ptr += UINT64(stride) * UINT64(idx);
return h;
}
static D3D12_GPU_DESCRIPTOR_HANDLE glRaytracingOffsetGpu(D3D12_GPU_DESCRIPTOR_HANDLE h, UINT stride, UINT idx)
{
h.ptr += UINT64(stride) * UINT64(idx);
return h;
}
// ============================================================
// Shared command context
// ============================================================
#ifndef GL_RAYTRACING_CMD_RING_SIZE
#define GL_RAYTRACING_CMD_RING_SIZE 4
#endif
#if GL_RAYTRACING_CMD_RING_SIZE < 2
#error GL_RAYTRACING_CMD_RING_SIZE must be at least 2
#endif
#ifndef GL_RAYTRACING_FORCE_CPU_SYNC
#define GL_RAYTRACING_FORCE_CPU_SYNC 0
#endif
struct glRaytracingCmdContext_t
{
ComPtr<ID3D12Device5> device;
ComPtr<ID3D12CommandQueue> queue;
// Active command allocator/list for the command currently being recorded.
// The ring below avoids the old every-frame CPU/GPU fence wait.
ComPtr<ID3D12CommandAllocator> cmdAlloc;
ComPtr<ID3D12GraphicsCommandList4> cmdList;
UINT64 cmdLastFenceValue;
ComPtr<ID3D12CommandAllocator> cmdAllocRing[GL_RAYTRACING_CMD_RING_SIZE];
ComPtr<ID3D12GraphicsCommandList4> cmdListRing[GL_RAYTRACING_CMD_RING_SIZE];
UINT64 cmdFenceValueRing[GL_RAYTRACING_CMD_RING_SIZE];
UINT cmdRingIndex;
UINT cmdCurrentSlot;
ComPtr<ID3D12CommandAllocator> blasCmdAlloc;
ComPtr<ID3D12GraphicsCommandList4> blasCmdList;
UINT64 blasLastFenceValue;
ComPtr<ID3D12CommandAllocator> blasCmdAllocRing[GL_RAYTRACING_CMD_RING_SIZE];
ComPtr<ID3D12GraphicsCommandList4> blasCmdListRing[GL_RAYTRACING_CMD_RING_SIZE];
UINT64 blasFenceValueRing[GL_RAYTRACING_CMD_RING_SIZE];
UINT blasRingIndex;
UINT blasCurrentSlot;
ComPtr<ID3D12CommandAllocator> tlasCmdAlloc;
ComPtr<ID3D12GraphicsCommandList4> tlasCmdList;
UINT64 tlasLastFenceValue;
ComPtr<ID3D12CommandAllocator> tlasCmdAllocRing[GL_RAYTRACING_CMD_RING_SIZE];
ComPtr<ID3D12GraphicsCommandList4> tlasCmdListRing[GL_RAYTRACING_CMD_RING_SIZE];
UINT64 tlasFenceValueRing[GL_RAYTRACING_CMD_RING_SIZE];
UINT tlasRingIndex;
UINT tlasCurrentSlot;
ComPtr<ID3D12Fence> fence;
HANDLE fenceEvent;
UINT64 nextFenceValue;
ComPtr<ID3D12QueryHeap> pathTracingTimestampHeap;
glRaytracingBuffer_t pathTracingTimestampReadback;
UINT64 pathTracingTimestampFrequency;
UINT64 pathTracingTimestampFenceRing[GL_RAYTRACING_CMD_RING_SIZE];
bool initialized;
glRaytracingCmdContext_t()
{
cmdLastFenceValue = 0;
blasLastFenceValue = 0;
tlasLastFenceValue = 0;
cmdRingIndex = GL_RAYTRACING_CMD_RING_SIZE - 1;
blasRingIndex = GL_RAYTRACING_CMD_RING_SIZE - 1;
tlasRingIndex = GL_RAYTRACING_CMD_RING_SIZE - 1;
cmdCurrentSlot = 0;
blasCurrentSlot = 0;
tlasCurrentSlot = 0;
for (UINT i = 0; i < GL_RAYTRACING_CMD_RING_SIZE; ++i)
{
cmdFenceValueRing[i] = 0;
blasFenceValueRing[i] = 0;
tlasFenceValueRing[i] = 0;
pathTracingTimestampFenceRing[i] = 0;
}
fenceEvent = nullptr;
nextFenceValue = 0;
pathTracingTimestampFrequency = 0;
initialized = false;
}
};
static glRaytracingCmdContext_t g_glRaytracingCmd;
static std::mutex g_glRaytracingMutex;
static void glRaytracingWaitFenceValue(UINT64 value)
{
if (!value || !g_glRaytracingCmd.fence)
return;
if (g_glRaytracingCmd.fence->GetCompletedValue() >= value)
return;
g_glRaytracingCmd.fence->SetEventOnCompletion(value, g_glRaytracingCmd.fenceEvent);
WaitForSingleObject(g_glRaytracingCmd.fenceEvent, INFINITE);
}
static UINT64 glRaytracingSignalQueue(void)
{
if (!g_glRaytracingCmd.queue || !g_glRaytracingCmd.fence)
return 0;
const UINT64 value = ++g_glRaytracingCmd.nextFenceValue;
g_glRaytracingCmd.queue->Signal(g_glRaytracingCmd.fence.Get(), value);
return value;
}
static void glRaytracingWaitIdle(void)
{
const UINT64 value = glRaytracingSignalQueue();
glRaytracingWaitFenceValue(value);
}
static void glRaytracingPollPathTracingTimestamp(UINT slot)
{
if (!g_glRaytracingCmd.pathTracingTimestampReadback.resource ||
!g_glRaytracingCmd.pathTracingTimestampFrequency ||
slot >= GL_RAYTRACING_CMD_RING_SIZE)
{
return;
}
const UINT64 fenceValue = g_glRaytracingCmd.pathTracingTimestampFenceRing[slot];
if (!fenceValue || !g_glRaytracingCmd.fence || g_glRaytracingCmd.fence->GetCompletedValue() < fenceValue)
return;
const UINT64 offset = sizeof(UINT64) * 2ull * slot;
D3D12_RANGE readRange = { offset, offset + sizeof(UINT64) * 2ull };
UINT64* timestamps = nullptr;
if (SUCCEEDED(g_glRaytracingCmd.pathTracingTimestampReadback.resource->Map(0, &readRange, reinterpret_cast<void**>(&timestamps))) && timestamps != nullptr)
{
const UINT64 begin = timestamps[slot * 2 + 0];
const UINT64 end = timestamps[slot * 2 + 1];
if (end >= begin)
{
com_pathTracingGpuMsec = (float)((double)(end - begin) * 1000.0 / (double)g_glRaytracingCmd.pathTracingTimestampFrequency);
}
D3D12_RANGE writeRange = { 0, 0 };
g_glRaytracingCmd.pathTracingTimestampReadback.resource->Unmap(0, &writeRange);
}
g_glRaytracingCmd.pathTracingTimestampFenceRing[slot] = 0;
}
static int glRaytracingCreatePathTracingTimestamps(void)
{
D3D12_QUERY_HEAP_DESC qh = {};
qh.Type = D3D12_QUERY_HEAP_TYPE_TIMESTAMP;
qh.Count = GL_RAYTRACING_CMD_RING_SIZE * 2;
if (FAILED(g_glRaytracingCmd.device->CreateQueryHeap(&qh, IID_PPV_ARGS(&g_glRaytracingCmd.pathTracingTimestampHeap))))
return 0;
g_glRaytracingCmd.pathTracingTimestampReadback = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
sizeof(UINT64) * GL_RAYTRACING_CMD_RING_SIZE * 2ull,
D3D12_HEAP_TYPE_READBACK,
D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_FLAG_NONE);
if (!g_glRaytracingCmd.pathTracingTimestampReadback.resource)
return 0;
g_glRaytracingCmd.pathTracingTimestampFrequency = 0;
if (FAILED(g_glRaytracingCmd.queue->GetTimestampFrequency(&g_glRaytracingCmd.pathTracingTimestampFrequency)) ||
g_glRaytracingCmd.pathTracingTimestampFrequency == 0)
{
g_glRaytracingCmd.pathTracingTimestampHeap.Reset();
g_glRaytracingCmd.pathTracingTimestampReadback = glRaytracingBuffer_t();
return 0;
}
return 1;
}
static int glRaytracingCreateDirectCommandListPair(
ID3D12Device5* device,
ComPtr<ID3D12CommandAllocator>& allocator,
ComPtr<ID3D12GraphicsCommandList4>& list)
{
GLR_CHECK(device->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(&allocator)));
GLR_CHECK(device->CreateCommandList(
0,
D3D12_COMMAND_LIST_TYPE_DIRECT,
allocator.Get(),
nullptr,
IID_PPV_ARGS(&list)));
GLR_CHECK(list->Close());
return 1;
}
static int glRaytracingInitCmdContext(void)
{
ID3D12Device* baseDevice = QD3D12_GetDevice();
ID3D12CommandQueue* baseQueue = QD3D12_GetQueue();
if (g_glRaytracingCmd.initialized)
{
if (!baseDevice || !baseQueue)
{
glRaytracingFatal("glRaytracingInitCmdContext: missing device or queue");
return 0;
}
ComPtr<ID3D12Device5> currentDevice;
HRESULT hr = baseDevice->QueryInterface(IID_PPV_ARGS(&currentDevice));
if (FAILED(hr) || currentDevice.Get() != g_glRaytracingCmd.device.Get() || baseQueue != g_glRaytracingCmd.queue.Get())
{
glRaytracingFatal("glRaytracingInitCmdContext: D3D12 device/queue changed. DXR state is device-local; create/use all windows with the same D3D12 device and queue, or fully shut down raytracing before switching devices.");
return 0;
}
return 1;
}
if (!baseDevice || !baseQueue)
{
glRaytracingFatal("glRaytracingInitCmdContext: missing device or queue");
return 0;
}
GLR_CHECK(baseDevice->QueryInterface(IID_PPV_ARGS(&g_glRaytracingCmd.device)));
g_glRaytracingCmd.queue = baseQueue;
for (UINT i = 0; i < GL_RAYTRACING_CMD_RING_SIZE; ++i)
{
if (!glRaytracingCreateDirectCommandListPair(
g_glRaytracingCmd.device.Get(),
g_glRaytracingCmd.cmdAllocRing[i],
g_glRaytracingCmd.cmdListRing[i]))
{
return 0;
}
if (!glRaytracingCreateDirectCommandListPair(
g_glRaytracingCmd.device.Get(),
g_glRaytracingCmd.blasCmdAllocRing[i],
g_glRaytracingCmd.blasCmdListRing[i]))
{
return 0;
}
if (!glRaytracingCreateDirectCommandListPair(
g_glRaytracingCmd.device.Get(),
g_glRaytracingCmd.tlasCmdAllocRing[i],
g_glRaytracingCmd.tlasCmdListRing[i]))
{
return 0;
}
}
g_glRaytracingCmd.cmdAlloc = g_glRaytracingCmd.cmdAllocRing[0];
g_glRaytracingCmd.cmdList = g_glRaytracingCmd.cmdListRing[0];
g_glRaytracingCmd.blasCmdAlloc = g_glRaytracingCmd.blasCmdAllocRing[0];
g_glRaytracingCmd.blasCmdList = g_glRaytracingCmd.blasCmdListRing[0];
g_glRaytracingCmd.tlasCmdAlloc = g_glRaytracingCmd.tlasCmdAllocRing[0];
g_glRaytracingCmd.tlasCmdList = g_glRaytracingCmd.tlasCmdListRing[0];
GLR_CHECK(g_glRaytracingCmd.device->CreateFence(
0,
D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&g_glRaytracingCmd.fence)));
g_glRaytracingCmd.fenceEvent = CreateEventA(nullptr, FALSE, FALSE, nullptr);
if (!g_glRaytracingCmd.fenceEvent)
{
glRaytracingFatal("CreateEventA failed");
return 0;
}
if (!glRaytracingCreatePathTracingTimestamps())
{
glRaytracingLog("path tracing timestamp queries unavailable");
}
g_glRaytracingCmd.initialized = true;
return 1;
}
static void glRaytracingShutdownCmdContext(void)
{
if (!g_glRaytracingCmd.initialized)
return;
glRaytracingWaitIdle();
if (g_glRaytracingCmd.fenceEvent)
{
CloseHandle(g_glRaytracingCmd.fenceEvent);
g_glRaytracingCmd.fenceEvent = nullptr;
}
g_glRaytracingCmd = glRaytracingCmdContext_t();
}
static int glRaytracingBeginCmd(void)
{
const UINT slot = (g_glRaytracingCmd.cmdRingIndex + 1u) % GL_RAYTRACING_CMD_RING_SIZE;
glRaytracingWaitFenceValue(g_glRaytracingCmd.cmdFenceValueRing[slot]);
glRaytracingPollPathTracingTimestamp(slot);
g_glRaytracingCmd.cmdRingIndex = slot;
g_glRaytracingCmd.cmdCurrentSlot = slot;
g_glRaytracingCmd.cmdAlloc = g_glRaytracingCmd.cmdAllocRing[slot];
g_glRaytracingCmd.cmdList = g_glRaytracingCmd.cmdListRing[slot];
GLR_CHECK(g_glRaytracingCmd.cmdAlloc->Reset());
GLR_CHECK(g_glRaytracingCmd.cmdList->Reset(g_glRaytracingCmd.cmdAlloc.Get(), nullptr));
return 1;
}
static int glRaytracingEndCmd(void)
{
GLR_CHECK(g_glRaytracingCmd.cmdList->Close());
ID3D12CommandList* lists[] = { g_glRaytracingCmd.cmdList.Get() };
g_glRaytracingCmd.queue->ExecuteCommandLists(1, lists);
g_glRaytracingCmd.cmdLastFenceValue = glRaytracingSignalQueue();
g_glRaytracingCmd.cmdFenceValueRing[g_glRaytracingCmd.cmdCurrentSlot] = g_glRaytracingCmd.cmdLastFenceValue;
#if GL_RAYTRACING_FORCE_CPU_SYNC
glRaytracingWaitFenceValue(g_glRaytracingCmd.cmdLastFenceValue);
#endif
return 1;
}
static int glRaytracingBeginBlasCmd(void)
{
const UINT slot = (g_glRaytracingCmd.blasRingIndex + 1u) % GL_RAYTRACING_CMD_RING_SIZE;
glRaytracingWaitFenceValue(g_glRaytracingCmd.blasFenceValueRing[slot]);
g_glRaytracingCmd.blasRingIndex = slot;
g_glRaytracingCmd.blasCurrentSlot = slot;
g_glRaytracingCmd.blasCmdAlloc = g_glRaytracingCmd.blasCmdAllocRing[slot];
g_glRaytracingCmd.blasCmdList = g_glRaytracingCmd.blasCmdListRing[slot];
GLR_CHECK(g_glRaytracingCmd.blasCmdAlloc->Reset());
GLR_CHECK(g_glRaytracingCmd.blasCmdList->Reset(g_glRaytracingCmd.blasCmdAlloc.Get(), nullptr));
return 1;
}
static UINT64 glRaytracingEndBlasCmd(void)
{
GLR_CHECK(g_glRaytracingCmd.blasCmdList->Close());
ID3D12CommandList* lists[] = { g_glRaytracingCmd.blasCmdList.Get() };
g_glRaytracingCmd.queue->ExecuteCommandLists(1, lists);
g_glRaytracingCmd.blasLastFenceValue = glRaytracingSignalQueue();
g_glRaytracingCmd.blasFenceValueRing[g_glRaytracingCmd.blasCurrentSlot] = g_glRaytracingCmd.blasLastFenceValue;
#if GL_RAYTRACING_FORCE_CPU_SYNC
glRaytracingWaitFenceValue(g_glRaytracingCmd.blasLastFenceValue);
#endif
return g_glRaytracingCmd.blasLastFenceValue;
}
static int glRaytracingBeginTlasCmd(void)
{
const UINT slot = (g_glRaytracingCmd.tlasRingIndex + 1u) % GL_RAYTRACING_CMD_RING_SIZE;
glRaytracingWaitFenceValue(g_glRaytracingCmd.tlasFenceValueRing[slot]);
g_glRaytracingCmd.tlasRingIndex = slot;
g_glRaytracingCmd.tlasCurrentSlot = slot;
g_glRaytracingCmd.tlasCmdAlloc = g_glRaytracingCmd.tlasCmdAllocRing[slot];
g_glRaytracingCmd.tlasCmdList = g_glRaytracingCmd.tlasCmdListRing[slot];
GLR_CHECK(g_glRaytracingCmd.tlasCmdAlloc->Reset());
GLR_CHECK(g_glRaytracingCmd.tlasCmdList->Reset(g_glRaytracingCmd.tlasCmdAlloc.Get(), nullptr));
return 1;
}
static UINT64 glRaytracingEndTlasCmd(void)
{
GLR_CHECK(g_glRaytracingCmd.tlasCmdList->Close());
ID3D12CommandList* lists[] = { g_glRaytracingCmd.tlasCmdList.Get() };
g_glRaytracingCmd.queue->ExecuteCommandLists(1, lists);
g_glRaytracingCmd.tlasLastFenceValue = glRaytracingSignalQueue();
g_glRaytracingCmd.tlasFenceValueRing[g_glRaytracingCmd.tlasCurrentSlot] = g_glRaytracingCmd.tlasLastFenceValue;
#if GL_RAYTRACING_FORCE_CPU_SYNC
glRaytracingWaitFenceValue(g_glRaytracingCmd.tlasLastFenceValue);
#endif
return g_glRaytracingCmd.tlasLastFenceValue;
}
// ============================================================
// Scene builder state
// ============================================================
#ifndef GL_RAYTRACING_MAX_RENDER_WORLDS
#define GL_RAYTRACING_MAX_RENDER_WORLDS 24
#endif
#ifndef GL_RAYTRACING_SCENE_HANDLE_T_DEFINED
typedef uint32_t glRaytracingSceneHandle_t;
#define GL_RAYTRACING_SCENE_HANDLE_T_DEFINED
#endif
// Internal material metadata encoded into D3D12's 24-bit shader-visible
// InstanceID. Lower 16 bits remain caller/user ID; bits 16-23 are material flags.
static const uint32_t GL_RAYTRACING_INSTANCE_USER_ID_MASK = 0x0000FFFFu;
static const uint32_t GL_RAYTRACING_INSTANCE_MATERIAL_SHIFT = 16u;
static const uint32_t GL_RAYTRACING_INSTANCE_MATERIAL_MASK = 0x000000FFu;
struct glRaytracingMeshRecord_t
{
uint32_t handle;
int alive;
glRaytracingMeshDesc_t descCpu;
std::vector<glRaytracingVertex_t> verticesCpu;
std::vector<uint32_t> indicesCpu;
glRaytracingBuffer_t vertexBuffer;
glRaytracingBuffer_t indexBuffer;
glRaytracingBuffer_t blasScratch;
glRaytracingBuffer_t blasResult[2];
UINT64 blasScratchSize;
UINT64 blasResultSize;
int blasBuilt;
int dirty;
UINT64 blasBuildFenceValue;
int currentBlasIndex;
uint32_t materialFlags;
uint32_t procFlags;
int32_t areaNum;
uint32_t materialVertexOffset;
uint32_t materialIndexOffset;
uint32_t materialVertexCount;
uint32_t materialIndexCount;
float averageColor[4];
float emissiveColor[4];
glRaytracingMeshRecord_t()
{
handle = 0;
alive = 0;
memset(&descCpu, 0, sizeof(descCpu));
blasScratchSize = 0;
blasResultSize = 0;
blasBuilt = 0;
dirty = 0;
blasBuildFenceValue = 0;
currentBlasIndex = 0;
materialFlags = 0;
procFlags = 0;
areaNum = -1;
materialVertexOffset = 0;
materialIndexOffset = 0;
materialVertexCount = 0;
materialIndexCount = 0;
averageColor[0] = averageColor[1] = averageColor[2] = averageColor[3] = 1.0f;
emissiveColor[0] = emissiveColor[1] = emissiveColor[2] = emissiveColor[3] = 0.0f;
}
};
struct glRaytracingInstanceRecord_t
{
uint32_t handle;
int alive;
glRaytracingInstanceDesc_t descCpu;
int dirty;
int cachedActive;
D3D12_GPU_VIRTUAL_ADDRESS cachedBlasGpuVA;
D3D12_RAYTRACING_INSTANCE_DESC cachedDescCpu;
glRaytracingInstanceRecord_t()
{
handle = 0;
alive = 0;
memset(&descCpu, 0, sizeof(descCpu));
dirty = 0;
cachedActive = 0;
cachedBlasGpuVA = 0;
memset(&cachedDescCpu, 0, sizeof(cachedDescCpu));
}
};
struct glRaytracingSceneUploadBuffer_t
{
glRaytracingBuffer_t buffer;
UINT64 capacityBytes;
D3D12_RAYTRACING_INSTANCE_DESC* mapped;
glRaytracingSceneUploadBuffer_t()
{
capacityBytes = 0;
mapped = nullptr;
}
};
// One render world owns exactly one TLAS pair and its own list of geometry
// instances. Mesh/BLAS resources stay shared across all worlds.
struct glRaytracingRenderWorld_t
{
uint32_t handle;
int alive;
std::vector<glRaytracingInstanceRecord_t> instances;
std::vector<int> activeInstanceIndices;
std::vector<D3D12_RAYTRACING_INSTANCE_DESC> cpuInstanceDescs;
std::vector<int> instanceHandleToIndex;
uint32_t nextInstanceHandle;
glRaytracingSceneUploadBuffer_t instanceDescUpload[2];
UINT64 instanceDescUploadFenceValue[2];
glRaytracingBuffer_t tlasScratch;
glRaytracingBuffer_t tlasResult[2];
UINT64 tlasScratchSize;
UINT64 tlasResultSize;
UINT activeInstanceCount;
UINT builtInstanceCount;
int tlasBuilt;
int tlasNeedsRebuild;
int tlasNeedsUpdate;
int currentTLASIndex;
glRaytracingRenderWorld_t()
{
handle = 0;
alive = 0;
nextInstanceHandle = 1;
instanceDescUploadFenceValue[0] = 0;
instanceDescUploadFenceValue[1] = 0;
tlasScratchSize = 0;
tlasResultSize = 0;
activeInstanceCount = 0;
builtInstanceCount = 0;
tlasBuilt = 0;
tlasNeedsRebuild = 1;
tlasNeedsUpdate = 1;
currentTLASIndex = 0;
}
};
struct glRaytracingSceneState_t
{
std::vector<glRaytracingMeshRecord_t> meshes;
std::vector<int> meshHandleToIndex;
uint32_t nextMeshHandle;
glRaytracingRenderWorld_t worlds[GL_RAYTRACING_MAX_RENDER_WORLDS];
int initialized;
glRaytracingSceneState_t()
{
nextMeshHandle = 1;
initialized = 0;
}
};
static glRaytracingSceneState_t g_glRaytracingScene;
void glRaytracingClear(void);
static void glRaytracingLightingResetDenoiseHistory(void);
static void glRaytracingReleaseWorldResources(glRaytracingRenderWorld_t* world)
{
if (!world)
return;
for (int i = 0; i < 2; ++i)
{
if (world->instanceDescUpload[i].buffer.resource && world->instanceDescUpload[i].mapped)
world->instanceDescUpload[i].buffer.resource->Unmap(0, nullptr);
world->instanceDescUpload[i] = glRaytracingSceneUploadBuffer_t();
world->tlasResult[i].resource.Reset();
}
world->instanceDescUploadFenceValue[0] = 0;
world->instanceDescUploadFenceValue[1] = 0;
world->tlasScratch.resource.Reset();
world->tlasScratchSize = 0;
world->tlasResultSize = 0;
}
static void glRaytracingResetWorldSlot(glRaytracingRenderWorld_t* world, uint32_t handle, int alive)
{
if (!world)
return;
glRaytracingReleaseWorldResources(world);
*world = glRaytracingRenderWorld_t();
world->handle = handle;
world->alive = alive ? 1 : 0;
world->nextInstanceHandle = 1;
}
static int glRaytracingWorldHandleToSlot(glRaytracingSceneHandle_t worldHandle)
{
if (worldHandle == 0 || worldHandle > GL_RAYTRACING_MAX_RENDER_WORLDS)
return -1;
return (int)(worldHandle - 1);
}
static glRaytracingRenderWorld_t* glRaytracingFindWorld(glRaytracingSceneHandle_t worldHandle)
{
const int slot = glRaytracingWorldHandleToSlot(worldHandle);
if (slot < 0)
return nullptr;
glRaytracingRenderWorld_t& world = g_glRaytracingScene.worlds[slot];
if (!world.alive || world.handle != worldHandle)
return nullptr;
return &world;
}
static const glRaytracingRenderWorld_t* glRaytracingFindWorldConst(glRaytracingSceneHandle_t worldHandle)
{
const int slot = glRaytracingWorldHandleToSlot(worldHandle);
if (slot < 0)
return nullptr;
const glRaytracingRenderWorld_t& world = g_glRaytracingScene.worlds[slot];
if (!world.alive || world.handle != worldHandle)
return nullptr;
return &world;
}
static void glRaytracingClearWorldContents(glRaytracingRenderWorld_t* world)
{
if (!world)
return;
const uint32_t handle = world->handle;
const int alive = world->alive;
glRaytracingReleaseWorldResources(world);
world->instances.clear();
world->activeInstanceIndices.clear();
world->cpuInstanceDescs.clear();
world->instanceHandleToIndex.clear();
world->handle = handle;
world->alive = alive;
world->nextInstanceHandle = 1;
world->instanceDescUploadFenceValue[0] = 0;
world->instanceDescUploadFenceValue[1] = 0;
world->tlasScratchSize = 0;
world->tlasResultSize = 0;
world->activeInstanceCount = 0;
world->builtInstanceCount = 0;
world->tlasBuilt = 0;
world->tlasNeedsRebuild = 1;
world->tlasNeedsUpdate = 1;
world->currentTLASIndex = 0;
}
static void glRaytracingClearAllSceneStateInternal(void)
{
for (int i = 0; i < GL_RAYTRACING_MAX_RENDER_WORLDS; ++i)
glRaytracingReleaseWorldResources(&g_glRaytracingScene.worlds[i]);
const int wasInitialized = g_glRaytracingScene.initialized;
g_glRaytracingScene = glRaytracingSceneState_t();
g_glRaytracingScene.initialized = wasInitialized;
}
static void glRaytracingMarkWorldNeedsRebuild(glRaytracingRenderWorld_t* world)
{
if (!world || !world->alive)
return;
world->tlasNeedsRebuild = 1;
world->tlasNeedsUpdate = 0;
glRaytracingLightingResetDenoiseHistory();
}
static void glRaytracingMarkWorldNeedsUpdate(glRaytracingRenderWorld_t* world)
{
if (!world || !world->alive)
return;
if (!world->tlasNeedsRebuild)
world->tlasNeedsUpdate = 1;
glRaytracingLightingResetDenoiseHistory();
}
static void glRaytracingMarkAllWorldsNeedRebuild(void)
{
for (int i = 0; i < GL_RAYTRACING_MAX_RENDER_WORLDS; ++i)
{
if (g_glRaytracingScene.worlds[i].alive)
glRaytracingMarkWorldNeedsRebuild(&g_glRaytracingScene.worlds[i]);
}
glRaytracingLightingResetDenoiseHistory();
}
static void glRaytracingCopyMeshAverageColor(float outAverageColor[4], const glRaytracingMeshDesc_t* desc)
{
if (!outAverageColor)
return;
outAverageColor[0] = outAverageColor[1] = outAverageColor[2] = outAverageColor[3] = 1.0f;
if (!desc)
return;
const bool hasExplicitAverage =
desc->averageColor[0] != 0.0f ||
desc->averageColor[1] != 0.0f ||
desc->averageColor[2] != 0.0f ||
desc->averageColor[3] != 0.0f;
if (!hasExplicitAverage)
return;
for (int i = 0; i < 4; ++i)
outAverageColor[i] = glRaytracingClamp<float>(desc->averageColor[i], 0.0f, 1.0f);
}
static uint32_t glRaytracingCountAliveInstances(const glRaytracingRenderWorld_t* world)
{
if (!world)
return 0;
uint32_t count = 0;
for (size_t i = 0; i < world->instances.size(); ++i)
{
if (world->instances[i].alive)
++count;
}
return count;
}
static void glRaytracingEnsureMeshHandleTable(uint32_t handle)
{
if (handle >= g_glRaytracingScene.meshHandleToIndex.size())
g_glRaytracingScene.meshHandleToIndex.resize((size_t)handle + 1, -1);
}
static void glRaytracingEnsureInstanceHandleTable(glRaytracingRenderWorld_t* world, uint32_t handle)
{
if (!world)
return;
if (handle >= world->instanceHandleToIndex.size())
world->instanceHandleToIndex.resize((size_t)handle + 1, -1);
}
static glRaytracingBuffer_t* glRaytracingGetMeshCurrentBLAS(glRaytracingMeshRecord_t* mesh)
{
if (!mesh)
return nullptr;
return &mesh->blasResult[mesh->currentBlasIndex & 1];
}
static const glRaytracingBuffer_t* glRaytracingGetMeshCurrentBLASConst(const glRaytracingMeshRecord_t* mesh)
{
if (!mesh)
return nullptr;
return &mesh->blasResult[mesh->currentBlasIndex & 1];
}
static int glRaytracingGetInactiveTLASIndex(const glRaytracingRenderWorld_t* world)
{
if (!world)
return 0;
return world->currentTLASIndex ^ 1;
}
static glRaytracingSceneUploadBuffer_t* glRaytracingGetBuildInstanceUpload(glRaytracingRenderWorld_t* world)
{
return &world->instanceDescUpload[glRaytracingGetInactiveTLASIndex(world)];
}
static glRaytracingBuffer_t* glRaytracingGetCurrentTLASBuffer(glRaytracingRenderWorld_t* world)
{
return &world->tlasResult[world->currentTLASIndex & 1];
}
static const glRaytracingBuffer_t* glRaytracingGetCurrentTLASBufferConst(const glRaytracingRenderWorld_t* world)
{
return &world->tlasResult[world->currentTLASIndex & 1];
}
static glRaytracingBuffer_t* glRaytracingGetBuildTLASBuffer(glRaytracingRenderWorld_t* world)
{
return &world->tlasResult[glRaytracingGetInactiveTLASIndex(world)];
}
static int glRaytracingEnsureTLASBuffers(
glRaytracingRenderWorld_t* world,
const D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS* inputs)
{
if (!world)
return 0;
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO prebuild = {};
g_glRaytracingCmd.device->GetRaytracingAccelerationStructurePrebuildInfo(inputs, &prebuild);
if (prebuild.ResultDataMaxSizeInBytes == 0)
{
glRaytracingFatal("TLAS prebuild size is zero");
return 0;
}
const UINT64 requiredScratch = glRaytracingAlignUp(
prebuild.ScratchDataSizeInBytes,
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BYTE_ALIGNMENT);
const UINT64 requiredResult = glRaytracingAlignUp(
prebuild.ResultDataMaxSizeInBytes,
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BYTE_ALIGNMENT);
const bool resizingScratch = world->tlasScratch.resource &&
world->tlasScratchSize < requiredScratch;
const bool resizingResult = (world->tlasResult[0].resource || world->tlasResult[1].resource) &&
world->tlasResultSize < requiredResult;
if (resizingScratch || resizingResult)
{
// Releasing/reallocating a TLAS resource can invalidate an in-flight ray
// dispatch that still references the old resource. This resize path is rare,
// so prefer correctness over complex deferred destruction.
glRaytracingWaitIdle();
}
if (!world->tlasScratch.resource ||
world->tlasScratchSize < requiredScratch)
{
world->tlasScratch.resource.Reset();
world->tlasScratch = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
requiredScratch,
D3D12_HEAP_TYPE_DEFAULT,
D3D12_RESOURCE_STATE_COMMON,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
if (!world->tlasScratch.resource)
return 0;
world->tlasScratchSize = requiredScratch;
}
for (int i = 0; i < 2; ++i)
{
if (!world->tlasResult[i].resource ||
world->tlasResultSize < requiredResult)
{
world->tlasResult[i].resource.Reset();
world->tlasResult[i] = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
requiredResult,
D3D12_HEAP_TYPE_DEFAULT,
D3D12_RESOURCE_STATE_RAYTRACING_ACCELERATION_STRUCTURE,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
if (!world->tlasResult[i].resource)
return 0;
}
}
world->tlasResultSize = requiredResult;
return 1;
}
static glRaytracingMeshRecord_t* glRaytracingFindMesh(uint32_t handle)
{
if (handle == 0 || handle >= g_glRaytracingScene.meshHandleToIndex.size())
return nullptr;
const int index = g_glRaytracingScene.meshHandleToIndex[handle];
if (index < 0 || (size_t)index >= g_glRaytracingScene.meshes.size())
return nullptr;
glRaytracingMeshRecord_t& mesh = g_glRaytracingScene.meshes[(size_t)index];
if (!mesh.alive || mesh.handle != handle)
return nullptr;
return &mesh;
}
static const glRaytracingMeshRecord_t* glRaytracingFindMeshConst(uint32_t handle)
{
if (handle == 0 || handle >= g_glRaytracingScene.meshHandleToIndex.size())
return nullptr;
const int index = g_glRaytracingScene.meshHandleToIndex[handle];
if (index < 0 || (size_t)index >= g_glRaytracingScene.meshes.size())
return nullptr;
const glRaytracingMeshRecord_t& mesh = g_glRaytracingScene.meshes[(size_t)index];
if (!mesh.alive || mesh.handle != handle)
return nullptr;
return &mesh;
}
static glRaytracingInstanceRecord_t* glRaytracingFindInstance(glRaytracingRenderWorld_t* world, uint32_t handle)
{
if (!world || handle == 0 || handle >= world->instanceHandleToIndex.size())
return nullptr;
const int index = world->instanceHandleToIndex[handle];
if (index < 0 || (size_t)index >= world->instances.size())
return nullptr;
glRaytracingInstanceRecord_t& inst = world->instances[(size_t)index];
if (!inst.alive || inst.handle != handle)
return nullptr;
return &inst;
}
static const glRaytracingInstanceRecord_t* glRaytracingFindInstanceConst(const glRaytracingRenderWorld_t* world, uint32_t handle)
{
if (!world || handle == 0 || handle >= world->instanceHandleToIndex.size())
return nullptr;
const int index = world->instanceHandleToIndex[handle];
if (index < 0 || (size_t)index >= world->instances.size())
return nullptr;
const glRaytracingInstanceRecord_t& inst = world->instances[(size_t)index];
if (!inst.alive || inst.handle != handle)
return nullptr;
return &inst;
}
static int glRaytracingEnsureMeshScratch(glRaytracingMeshRecord_t* mesh, UINT64 requiredScratch)
{
if (!mesh)
return 0;
requiredScratch = glRaytracingAlignUp(requiredScratch, D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BYTE_ALIGNMENT);
if (!mesh->blasScratch.resource || mesh->blasScratchSize < requiredScratch)
{
glRaytracingWaitFenceValue(mesh->blasBuildFenceValue);
mesh->blasScratch.resource.Reset();
mesh->blasScratch = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
requiredScratch,
D3D12_HEAP_TYPE_DEFAULT,
D3D12_RESOURCE_STATE_COMMON,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
if (!mesh->blasScratch.resource)
return 0;
mesh->blasScratchSize = requiredScratch;
}
return 1;
}
static int glRaytracingEnsureMeshResultBuffers(glRaytracingMeshRecord_t* mesh, UINT64 requiredResult)
{
if (!mesh)
return 0;
requiredResult = glRaytracingAlignUp(requiredResult, D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BYTE_ALIGNMENT);
const int resultCount = mesh->descCpu.allowUpdate ? 2 : 1;
for (int i = 0; i < resultCount; ++i)
{
if (!mesh->blasResult[i].resource || mesh->blasResultSize < requiredResult)
{
glRaytracingWaitFenceValue(mesh->blasBuildFenceValue);
mesh->blasResult[i].resource.Reset();
mesh->blasResult[i] = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
requiredResult,
D3D12_HEAP_TYPE_DEFAULT,
D3D12_RESOURCE_STATE_RAYTRACING_ACCELERATION_STRUCTURE,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
if (!mesh->blasResult[i].resource)
return 0;
}
}
if (!mesh->descCpu.allowUpdate && mesh->blasResult[1].resource)
{
glRaytracingWaitFenceValue(mesh->blasBuildFenceValue);
mesh->blasResult[1].resource.Reset();
}
mesh->blasResultSize = requiredResult;
return 1;
}
static inline void glRaytracingBuildInstanceDesc(
D3D12_RAYTRACING_INSTANCE_DESC* outDesc,
const glRaytracingInstanceRecord_t& inst,
uint32_t meshHandle,
uint32_t meshMaterialFlags,
D3D12_GPU_VIRTUAL_ADDRESS blasGpuVA)
{
memcpy(outDesc->Transform, inst.descCpu.transform, sizeof(float) * 12);
// InstanceID is 24-bit in D3D12_RAYTRACING_INSTANCE_DESC. The lower 16 bits
// carry the mesh metadata index, and bits 16-23 carry material flags so the
// any-hit shader can decide whether visibility rays pass through the hit.
//
// Accept both sources:
// - meshMaterialFlags set by glRaytracingSetMeshMaterialFlags()/shim mesh tags
// - already-encoded high InstanceID bits for direct low-level callers
const uint32_t userInstanceId = meshHandle & GL_RAYTRACING_INSTANCE_USER_ID_MASK;
const uint32_t instanceMaterialFlags =
(inst.descCpu.instanceID >> GL_RAYTRACING_INSTANCE_MATERIAL_SHIFT) & GL_RAYTRACING_INSTANCE_MATERIAL_MASK;
const uint32_t combinedMaterialFlags =
(meshMaterialFlags | instanceMaterialFlags) & GL_RAYTRACING_INSTANCE_MATERIAL_MASK;
const uint32_t materialBits = combinedMaterialFlags << GL_RAYTRACING_INSTANCE_MATERIAL_SHIFT;
outDesc->InstanceID = userInstanceId | materialBits;
outDesc->InstanceMask = (UINT8)(inst.descCpu.mask & 0xFFu);
outDesc->InstanceContributionToHitGroupIndex = 0;
outDesc->Flags = D3D12_RAYTRACING_INSTANCE_FLAG_NONE;
outDesc->AccelerationStructure = blasGpuVA;
}
static void glRaytracingInvalidateInstanceCache(glRaytracingInstanceRecord_t* inst)
{
if (!inst)
return;
inst->cachedActive = 0;
inst->cachedBlasGpuVA = 0;
memset(&inst->cachedDescCpu, 0, sizeof(inst->cachedDescCpu));
}
static int glRaytracingResolveInstanceDesc(
glRaytracingInstanceRecord_t* inst,
D3D12_RAYTRACING_INSTANCE_DESC* outDesc,
D3D12_GPU_VIRTUAL_ADDRESS* outBlasGpuVA)
{
if (!inst || !inst->alive)
return 0;
const glRaytracingMeshRecord_t* mesh = glRaytracingFindMeshConst(inst->descCpu.meshHandle);
if (!mesh || !mesh->blasBuilt)
return 0;
const glRaytracingBuffer_t* blas = glRaytracingGetMeshCurrentBLASConst(mesh);
if (!blas || !blas->resource || blas->gpuVA == 0)
return 0;
if (outDesc)
glRaytracingBuildInstanceDesc(outDesc, *inst, mesh->handle, mesh->materialFlags, blas->gpuVA);
if (outBlasGpuVA)
*outBlasGpuVA = blas->gpuVA;
return 1;
}
static int glRaytracingRebuildActiveInstanceCache(glRaytracingRenderWorld_t* world)
{
if (!world)
return 0;
world->activeInstanceIndices.clear();
world->cpuInstanceDescs.clear();
world->activeInstanceIndices.reserve(world->instances.size());
world->cpuInstanceDescs.reserve(world->instances.size());
for (size_t i = 0; i < world->instances.size(); ++i)
{
glRaytracingInstanceRecord_t& inst = world->instances[i];
glRaytracingInvalidateInstanceCache(&inst);
if (!inst.alive)
continue;
D3D12_RAYTRACING_INSTANCE_DESC desc = {};
D3D12_GPU_VIRTUAL_ADDRESS blasGpuVA = 0;
if (!glRaytracingResolveInstanceDesc(&inst, &desc, &blasGpuVA))
continue;
inst.cachedActive = 1;
inst.cachedBlasGpuVA = blasGpuVA;
inst.cachedDescCpu = desc;
inst.dirty = 0;
world->activeInstanceIndices.push_back((int)i);
world->cpuInstanceDescs.push_back(desc);
}
world->activeInstanceCount = (UINT)world->cpuInstanceDescs.size();
return 1;
}
static int glRaytracingRefreshDirtyInstanceCache(glRaytracingRenderWorld_t* world)
{
if (!world)
return 0;
for (size_t listIndex = 0; listIndex < world->activeInstanceIndices.size(); ++listIndex)
{
const int instIndex = world->activeInstanceIndices[listIndex];
if (instIndex < 0 || (size_t)instIndex >= world->instances.size())
return 0;
glRaytracingInstanceRecord_t& inst = world->instances[(size_t)instIndex];
if (!inst.alive)
return 0;
D3D12_RAYTRACING_INSTANCE_DESC desc = {};
D3D12_GPU_VIRTUAL_ADDRESS blasGpuVA = 0;
if (!glRaytracingResolveInstanceDesc(&inst, &desc, &blasGpuVA))
return 0;
if (inst.dirty || !inst.cachedActive || inst.cachedBlasGpuVA != blasGpuVA)
{
inst.cachedActive = 1;
inst.cachedBlasGpuVA = blasGpuVA;
inst.cachedDescCpu = desc;
world->cpuInstanceDescs[listIndex] = desc;
}
inst.dirty = 0;
}
world->activeInstanceCount = (UINT)world->cpuInstanceDescs.size();
return 1;
}
static int glRaytracingEnsureSceneUploadBuffer(glRaytracingRenderWorld_t* world, UINT64 requiredBytes);
static int glRaytracingUploadCachedInstanceDescs(glRaytracingRenderWorld_t* world)
{
if (!world)
return 0;
const UINT activeCount = (UINT)world->cpuInstanceDescs.size();
const UINT64 instBytes = glRaytracingAlignUp(
(UINT64)activeCount * (UINT64)sizeof(D3D12_RAYTRACING_INSTANCE_DESC),
D3D12_RAYTRACING_INSTANCE_DESCS_BYTE_ALIGNMENT);
const int uploadIndex = glRaytracingGetInactiveTLASIndex(world);
glRaytracingWaitFenceValue(world->instanceDescUploadFenceValue[uploadIndex]);
if (!glRaytracingEnsureSceneUploadBuffer(world, instBytes))
return 0;
glRaytracingSceneUploadBuffer_t* upload = glRaytracingGetBuildInstanceUpload(world);
if (!upload->mapped)
return 0;
if (activeCount > 0)
memcpy(upload->mapped, world->cpuInstanceDescs.data(), (size_t)activeCount * sizeof(D3D12_RAYTRACING_INSTANCE_DESC));
return 1;
}
static int glRaytracingUploadMeshBuffers(glRaytracingMeshRecord_t* mesh);
static int glRaytracingBuildDirtyMeshesInternal(void)
{
static thread_local std::vector<glRaytracingMeshRecord_t*> dirtyMeshes;
dirtyMeshes.clear();
dirtyMeshes.reserve(g_glRaytracingScene.meshes.size());
for (size_t i = 0; i < g_glRaytracingScene.meshes.size(); ++i)
{
glRaytracingMeshRecord_t& mesh = g_glRaytracingScene.meshes[i];
if (!mesh.alive)
continue;
if (!mesh.blasBuilt || mesh.dirty)
dirtyMeshes.push_back(&mesh);
}
if (dirtyMeshes.empty())
return 1;
struct glRaytracingMeshBuildInfo_t
{
glRaytracingMeshRecord_t* mesh;
D3D12_RAYTRACING_GEOMETRY_DESC geomDesc;
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS inputs;
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC buildDesc;
ID3D12Resource* barrierResource;
int newBlasIndex;
};
static thread_local std::vector<glRaytracingMeshBuildInfo_t> builds;
builds.clear();
builds.resize(dirtyMeshes.size());
for (size_t i = 0; i < dirtyMeshes.size(); ++i)
{
glRaytracingMeshRecord_t* mesh = dirtyMeshes[i];
if (!mesh->vertexBuffer.resource || !mesh->indexBuffer.resource)
{
if (!glRaytracingUploadMeshBuffers(mesh))
return 0;
}
glRaytracingMeshBuildInfo_t& info = builds[i];
memset(&info, 0, sizeof(info));
info.mesh = mesh;
info.geomDesc.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES;
const bool meshIsGlass = (mesh->materialFlags & GL_RAYTRACING_MATERIAL_FLAG_GLASS) != 0u;
info.geomDesc.Flags = (mesh->descCpu.opaque && !meshIsGlass)
? D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE
: D3D12_RAYTRACING_GEOMETRY_FLAG_NONE;
info.geomDesc.Triangles.Transform3x4 = 0;
info.geomDesc.Triangles.IndexFormat = DXGI_FORMAT_R32_UINT;
info.geomDesc.Triangles.VertexFormat = DXGI_FORMAT_R32G32B32_FLOAT;
info.geomDesc.Triangles.IndexCount = (UINT)mesh->indicesCpu.size();
info.geomDesc.Triangles.VertexCount = (UINT)mesh->verticesCpu.size();
info.geomDesc.Triangles.IndexBuffer = mesh->indexBuffer.gpuVA;
info.geomDesc.Triangles.VertexBuffer.StartAddress = mesh->vertexBuffer.gpuVA;
info.geomDesc.Triangles.VertexBuffer.StrideInBytes = sizeof(glRaytracingVertex_t);
info.inputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL;
info.inputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY;
info.inputs.NumDescs = 1;
info.inputs.pGeometryDescs = &info.geomDesc;
info.inputs.Flags = mesh->descCpu.allowUpdate
? (D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE |
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_ALLOW_UPDATE)
: D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE;
const bool canUpdateInPlace = (mesh->blasBuilt != 0) && (mesh->descCpu.allowUpdate != 0);
if (canUpdateInPlace)
info.inputs.Flags |= D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PERFORM_UPDATE;
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO prebuild = {};
g_glRaytracingCmd.device->GetRaytracingAccelerationStructurePrebuildInfo(&info.inputs, &prebuild);
if (prebuild.ResultDataMaxSizeInBytes == 0)
{
glRaytracingFatal("BLAS prebuild size is zero");
return 0;
}
if (!glRaytracingEnsureMeshScratch(mesh, prebuild.ScratchDataSizeInBytes))
return 0;
if (!glRaytracingEnsureMeshResultBuffers(mesh, prebuild.ResultDataMaxSizeInBytes))
return 0;
const int oldIndex = mesh->currentBlasIndex & 1;
info.newBlasIndex = (mesh->descCpu.allowUpdate && mesh->blasBuilt) ? (oldIndex ^ 1) : oldIndex;
info.buildDesc.Inputs = info.inputs;
info.buildDesc.ScratchAccelerationStructureData = mesh->blasScratch.gpuVA;
info.buildDesc.DestAccelerationStructureData = mesh->blasResult[info.newBlasIndex].gpuVA;
info.buildDesc.SourceAccelerationStructureData = 0;
if (canUpdateInPlace)
info.buildDesc.SourceAccelerationStructureData = mesh->blasResult[oldIndex].gpuVA;
info.barrierResource = mesh->blasResult[info.newBlasIndex].resource.Get();
}
if (!glRaytracingBeginBlasCmd())
return 0;
glRaytracingBeginPixEvent(g_glRaytracingCmd.blasCmdList.Get(), L"GLR Build Dirty Mesh BLAS Batch");
for (size_t i = 0; i < builds.size(); ++i)
{
{
GLR_PIX_EVENT(g_glRaytracingCmd.blasCmdList.Get(), L"GLR Build BLAS");
g_glRaytracingCmd.blasCmdList->BuildRaytracingAccelerationStructure(&builds[i].buildDesc, 0, nullptr);
}
D3D12_RESOURCE_BARRIER uav = {};
uav.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
uav.UAV.pResource = builds[i].barrierResource;
g_glRaytracingCmd.blasCmdList->ResourceBarrier(1, &uav);
}
glRaytracingEndPixEvent(g_glRaytracingCmd.blasCmdList.Get());
const UINT64 blasFenceValue = glRaytracingEndBlasCmd();
if (!blasFenceValue)
return 0;
// Do not block the CPU here. The BLAS build was submitted before any TLAS
// build/lighting work that consumes it, and the shared D3D12 queue preserves
// that order. Command allocator reuse is protected by the ring fence in
// glRaytracingBeginBlasCmd().
for (size_t i = 0; i < builds.size(); ++i)
{
glRaytracingMeshRecord_t* mesh = builds[i].mesh;
mesh->currentBlasIndex = builds[i].newBlasIndex;
mesh->blasBuildFenceValue = blasFenceValue;
mesh->blasBuilt = 1;
mesh->dirty = 0;
}
glRaytracingMarkAllWorldsNeedRebuild();
return 1;
}
static int glRaytracingUploadMeshBuffers(glRaytracingMeshRecord_t* mesh)
{
if (!mesh)
return 0;
if (mesh->verticesCpu.empty() || mesh->indicesCpu.empty())
return 0;
const UINT64 vbBytes = UINT64(mesh->verticesCpu.size()) * sizeof(glRaytracingVertex_t);
const UINT64 ibBytes = UINT64(mesh->indicesCpu.size()) * sizeof(uint32_t);
mesh->vertexBuffer = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
vbBytes,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
D3D12_RESOURCE_FLAG_NONE);
mesh->indexBuffer = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
ibBytes,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
D3D12_RESOURCE_FLAG_NONE);
if (!mesh->vertexBuffer.resource || !mesh->indexBuffer.resource)
return 0;
glRaytracingMapCopy(mesh->vertexBuffer.resource.Get(), mesh->verticesCpu.data(), (size_t)vbBytes);
glRaytracingMapCopy(mesh->indexBuffer.resource.Get(), mesh->indicesCpu.data(), (size_t)ibBytes);
return 1;
}
static int glRaytracingBuildMeshInternal(glRaytracingMeshRecord_t* mesh)
{
if (!mesh)
return 0;
const int oldDirty = mesh->dirty;
mesh->dirty = 1;
const int ok = glRaytracingBuildDirtyMeshesInternal();
if (!ok)
mesh->dirty = oldDirty;
return ok;
}
static int glRaytracingEnsureSceneUploadBuffer(glRaytracingRenderWorld_t* world, UINT64 requiredBytes)
{
if (!world)
return 0;
glRaytracingSceneUploadBuffer_t* upload = glRaytracingGetBuildInstanceUpload(world);
if (requiredBytes == 0)
requiredBytes = D3D12_RAYTRACING_INSTANCE_DESCS_BYTE_ALIGNMENT;
requiredBytes = glRaytracingAlignUp(
requiredBytes,
D3D12_RAYTRACING_INSTANCE_DESCS_BYTE_ALIGNMENT);
if (upload->buffer.resource &&
upload->capacityBytes >= requiredBytes &&
upload->mapped)
{
return 1;
}
if (upload->buffer.resource && upload->mapped)
upload->buffer.resource->Unmap(0, nullptr);
upload->mapped = nullptr;
upload->buffer.resource.Reset();
upload->capacityBytes = 0;
upload->buffer = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
requiredBytes,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
D3D12_RESOURCE_FLAG_NONE);
if (!upload->buffer.resource)
return 0;
void* mapped = nullptr;
D3D12_RANGE readRange = {};
if (FAILED(upload->buffer.resource->Map(0, &readRange, &mapped)) || !mapped)
{
upload->buffer.resource.Reset();
return 0;
}
upload->mapped = (D3D12_RAYTRACING_INSTANCE_DESC*)mapped;
upload->capacityBytes = requiredBytes;
return 1;
}
static int glRaytracingBuildSceneInternal(glRaytracingRenderWorld_t* world)
{
if (!world || !world->alive)
return 0;
UINT aliveCount = 0;
int anyDirty = 0;
int needsRebuild = world->tlasNeedsRebuild;
int needsUpdate = world->tlasNeedsUpdate;
for (size_t i = 0; i < world->instances.size(); ++i)
{
const glRaytracingInstanceRecord_t& inst = world->instances[i];
if (!inst.alive)
continue;
++aliveCount;
if (inst.dirty)
anyDirty = 1;
}
if (aliveCount == 0)
{
world->activeInstanceIndices.clear();
world->cpuInstanceDescs.clear();
world->activeInstanceCount = 0;
world->builtInstanceCount = 0;
world->tlasBuilt = 0;
world->tlasNeedsRebuild = 0;
world->tlasNeedsUpdate = 0;
return 1;
}
if (!world->tlasBuilt)
needsRebuild = 1;
if ((UINT)world->activeInstanceIndices.size() != world->builtInstanceCount)
needsRebuild = 1;
if (needsRebuild)
{
if (!glRaytracingRebuildActiveInstanceCache(world))
return 0;
}
else
{
if (!needsUpdate && !anyDirty)
{
world->activeInstanceCount = (UINT)world->cpuInstanceDescs.size();
return 1;
}
if (!glRaytracingRefreshDirtyInstanceCache(world))
{
world->tlasNeedsRebuild = 1;
if (!glRaytracingRebuildActiveInstanceCache(world))
return 0;
needsRebuild = 1;
}
}
const UINT activeCount = (UINT)world->cpuInstanceDescs.size();
if (activeCount == 0)
{
world->activeInstanceCount = 0;
world->builtInstanceCount = 0;
world->tlasBuilt = 0;
world->tlasNeedsRebuild = 0;
world->tlasNeedsUpdate = 0;
return 1;
}
if (!world->tlasBuilt || activeCount != world->builtInstanceCount)
needsRebuild = 1;
if (!glRaytracingUploadCachedInstanceDescs(world))
return 0;
glRaytracingSceneUploadBuffer_t* upload = glRaytracingGetBuildInstanceUpload(world);
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS inputs = {};
inputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL;
inputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY;
inputs.NumDescs = activeCount;
inputs.InstanceDescs = upload->buffer.gpuVA;
inputs.Flags =
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE |
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_ALLOW_UPDATE;
if (!glRaytracingEnsureTLASBuffers(world, &inputs))
return 0;
const int buildTLASIndex = glRaytracingGetInactiveTLASIndex(world);
glRaytracingBuffer_t* dstTLAS = glRaytracingGetBuildTLASBuffer(world);
const glRaytracingBuffer_t* srcTLAS = glRaytracingGetCurrentTLASBufferConst(world);
// The TLAS build is queued after any pending BLAS builds on the same D3D12
// queue, so GPU ordering is sufficient and a CPU wait would stall the frame.
if (!glRaytracingBeginTlasCmd())
return 0;
glRaytracingBeginPixEvent(g_glRaytracingCmd.tlasCmdList.Get(), needsRebuild ? L"GLR Build TLAS" : L"GLR Update TLAS");
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC buildDesc = {};
buildDesc.Inputs = inputs;
buildDesc.ScratchAccelerationStructureData = world->tlasScratch.gpuVA;
buildDesc.DestAccelerationStructureData = dstTLAS->gpuVA;
buildDesc.SourceAccelerationStructureData = 0;
if (!needsRebuild && world->tlasBuilt)
{
buildDesc.Inputs.Flags |= D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PERFORM_UPDATE;
buildDesc.SourceAccelerationStructureData = srcTLAS->gpuVA;
}
g_glRaytracingCmd.tlasCmdList->BuildRaytracingAccelerationStructure(&buildDesc, 0, nullptr);
D3D12_RESOURCE_BARRIER uav = {};
uav.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
uav.UAV.pResource = dstTLAS->resource.Get();
g_glRaytracingCmd.tlasCmdList->ResourceBarrier(1, &uav);
glRaytracingEndPixEvent(g_glRaytracingCmd.tlasCmdList.Get());
const UINT64 tlasFenceValue = glRaytracingEndTlasCmd();
if (!tlasFenceValue)
return 0;
// Keep TLAS builds asynchronous. Later ray dispatches are submitted to the
// same queue after this command list, so the GPU sees a complete TLAS before
// tracing without forcing the CPU to wait every update. The upload buffer is
// fence-tagged so the CPU does not overwrite instance descriptors still being
// consumed by an in-flight TLAS build.
world->instanceDescUploadFenceValue[buildTLASIndex] = tlasFenceValue;
world->currentTLASIndex = buildTLASIndex;
world->activeInstanceCount = activeCount;
world->builtInstanceCount = activeCount;
world->tlasBuilt = 1;
world->tlasNeedsRebuild = 0;
world->tlasNeedsUpdate = 0;
for (size_t i = 0; i < world->instances.size(); ++i)
{
if (world->instances[i].alive)
world->instances[i].dirty = 0;
}
return 1;
}
static void glRaytracingInvalidateInstancesForMesh(uint32_t meshHandle, int deleteInstances)
{
for (int w = 0; w < GL_RAYTRACING_MAX_RENDER_WORLDS; ++w)
{
glRaytracingRenderWorld_t& world = g_glRaytracingScene.worlds[w];
if (!world.alive)
continue;
int touched = 0;
for (size_t i = 0; i < world.instances.size(); ++i)
{
glRaytracingInstanceRecord_t& inst = world.instances[i];
if (inst.alive && inst.descCpu.meshHandle == meshHandle)
{
glRaytracingInvalidateInstanceCache(&inst);
inst.dirty = 1;
touched = 1;
if (deleteInstances)
{
inst.alive = 0;
if (inst.handle < world.instanceHandleToIndex.size())
world.instanceHandleToIndex[inst.handle] = -1;
}
}
}
if (touched)
glRaytracingMarkWorldNeedsRebuild(&world);
}
}
// ============================================================
// Scene public API
// ============================================================
int glRaytracingInit(void)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (g_glRaytracingScene.initialized)
return 1;
if (!glRaytracingInitCmdContext())
return 0;
g_glRaytracingScene.initialized = 1;
glRaytracingLog("glRaytracingInit ok");
return 1;
}
void glRaytracingShutdown(void)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized)
return;
glRaytracingWaitIdle();
glRaytracingClearAllSceneStateInternal();
g_glRaytracingScene = glRaytracingSceneState_t();
glRaytracingShutdownCmdContext();
}
void glRaytracingClear(void)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
glRaytracingWaitIdle();
glRaytracingClearAllSceneStateInternal();
}
glRaytracingSceneHandle_t glRaytracingCreateScene(void)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized)
return 0;
for (uint32_t i = 0; i < GL_RAYTRACING_MAX_RENDER_WORLDS; ++i)
{
glRaytracingRenderWorld_t& world = g_glRaytracingScene.worlds[i];
if (!world.alive)
{
const uint32_t handle = i + 1;
glRaytracingResetWorldSlot(&world, handle, 1);
return handle;
}
}
return 0;
}
void glRaytracingClearScene(glRaytracingSceneHandle_t worldHandle)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
glRaytracingRenderWorld_t* world = glRaytracingFindWorld(worldHandle);
if (!world)
return;
glRaytracingWaitIdle();
glRaytracingClearWorldContents(world);
}
void glRaytracingDeleteScene(glRaytracingSceneHandle_t worldHandle)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
glRaytracingRenderWorld_t* world = glRaytracingFindWorld(worldHandle);
if (!world)
return;
glRaytracingWaitIdle();
glRaytracingReleaseWorldResources(world);
*world = glRaytracingRenderWorld_t();
}
uint32_t glRaytracingGetSceneCount(void)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
uint32_t count = 0;
for (int i = 0; i < GL_RAYTRACING_MAX_RENDER_WORLDS; ++i)
{
if (g_glRaytracingScene.worlds[i].alive)
++count;
}
return count;
}
glRaytracingMeshHandle_t glRaytracingCreateMesh(const glRaytracingMeshDesc_t* desc)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized || !desc)
return 0;
if (!desc->vertices || !desc->indices || desc->vertexCount == 0 || desc->indexCount == 0)
return 0;
glRaytracingMeshRecord_t mesh;
mesh.handle = g_glRaytracingScene.nextMeshHandle++;
mesh.alive = 1;
mesh.descCpu = *desc;
mesh.verticesCpu.assign(desc->vertices, desc->vertices + desc->vertexCount);
mesh.indicesCpu.assign(desc->indices, desc->indices + desc->indexCount);
mesh.descCpu.vertices = nullptr;
mesh.descCpu.indices = nullptr;
glRaytracingCopyMeshAverageColor(mesh.averageColor, desc);
mesh.dirty = 1;
g_glRaytracingScene.meshes.push_back(mesh);
const size_t newIndex = g_glRaytracingScene.meshes.size() - 1;
glRaytracingEnsureMeshHandleTable(mesh.handle);
g_glRaytracingScene.meshHandleToIndex[mesh.handle] = (int)newIndex;
return mesh.handle;
}
int glRaytracingUpdateMesh(glRaytracingMeshHandle_t meshHandle, const glRaytracingMeshDesc_t* desc)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized || !desc)
return 0;
glRaytracingMeshRecord_t* mesh = glRaytracingFindMesh(meshHandle);
if (!mesh)
return 0;
if (!desc->vertices || !desc->indices || desc->vertexCount == 0 || desc->indexCount == 0)
return 0;
const UINT64 vbBytes = UINT64(desc->vertexCount) * sizeof(glRaytracingVertex_t);
const UINT64 ibBytes = UINT64(desc->indexCount) * sizeof(uint32_t);
const bool sameVertexData =
mesh->verticesCpu.size() == desc->vertexCount &&
memcmp(mesh->verticesCpu.data(), desc->vertices, (size_t)vbBytes) == 0;
const bool sameIndexData =
mesh->indicesCpu.size() == desc->indexCount &&
memcmp(mesh->indicesCpu.data(), desc->indices, (size_t)ibBytes) == 0;
const bool sameDesc =
mesh->descCpu.vertexCount == desc->vertexCount &&
mesh->descCpu.indexCount == desc->indexCount &&
mesh->descCpu.allowUpdate == desc->allowUpdate &&
mesh->descCpu.opaque == desc->opaque;
float descAverageColor[4];
glRaytracingCopyMeshAverageColor(descAverageColor, desc);
const bool sameAverageColor = memcmp(mesh->averageColor, descAverageColor, sizeof(mesh->averageColor)) == 0;
if (sameDesc && sameVertexData && sameIndexData)
{
if (!sameAverageColor)
{
memcpy(mesh->averageColor, descAverageColor, sizeof(mesh->averageColor));
glRaytracingLightingResetDenoiseHistory();
}
return 1;
}
const bool canUpdateExistingBuffers =
desc->allowUpdate != 0 &&
mesh->descCpu.allowUpdate != 0 &&
mesh->descCpu.opaque == desc->opaque &&
mesh->blasBuilt != 0 &&
mesh->vertexBuffer.resource &&
mesh->indexBuffer.resource &&
mesh->vertexBuffer.size >= vbBytes &&
mesh->indexBuffer.size >= ibBytes &&
mesh->verticesCpu.size() == desc->vertexCount &&
mesh->indicesCpu.size() == desc->indexCount;
mesh->descCpu = *desc;
mesh->verticesCpu.assign(desc->vertices, desc->vertices + desc->vertexCount);
mesh->indicesCpu.assign(desc->indices, desc->indices + desc->indexCount);
mesh->descCpu.vertices = nullptr;
mesh->descCpu.indices = nullptr;
memcpy(mesh->averageColor, descAverageColor, sizeof(mesh->averageColor));
if (canUpdateExistingBuffers)
{
glRaytracingMapCopy(mesh->vertexBuffer.resource.Get(), mesh->verticesCpu.data(), (size_t)vbBytes);
if (!sameIndexData)
glRaytracingMapCopy(mesh->indexBuffer.resource.Get(), mesh->indicesCpu.data(), (size_t)ibBytes);
mesh->dirty = 1;
glRaytracingInvalidateInstancesForMesh(meshHandle, 0);
return 1;
}
// Updating a mesh destroys/replaces resources that an already submitted frame
// may still reference. Wait only for this destructive path; steady-state
// rendering remains asynchronous.
glRaytracingWaitIdle();
mesh->vertexBuffer.resource.Reset();
mesh->indexBuffer.resource.Reset();
mesh->blasScratch.resource.Reset();
mesh->blasResult[0].resource.Reset();
mesh->blasResult[1].resource.Reset();
mesh->blasScratchSize = 0;
mesh->blasResultSize = 0;
mesh->blasBuildFenceValue = 0;
mesh->blasBuilt = 0;
mesh->dirty = 1;
mesh->currentBlasIndex = 0;
glRaytracingInvalidateInstancesForMesh(meshHandle, 0);
glRaytracingMarkAllWorldsNeedRebuild();
return 1;
}
uint32_t glRaytracingGetMeshMaterialFlags(glRaytracingMeshHandle_t meshHandle)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
const glRaytracingMeshRecord_t* mesh = glRaytracingFindMeshConst(meshHandle);
if (!mesh)
return 0;
return mesh->materialFlags;
}
void glRaytracingSetMeshMaterialFlags(glRaytracingMeshHandle_t meshHandle, uint32_t materialFlags)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
glRaytracingMeshRecord_t* mesh = glRaytracingFindMesh(meshHandle);
if (!mesh)
return;
materialFlags &= GL_RAYTRACING_INSTANCE_MATERIAL_MASK;
if (mesh->materialFlags == materialFlags)
return;
mesh->materialFlags = materialFlags;
// The glass bit also represents shim-auto-tagged alpha-blended surfaces.
// It changes whether BLAS geometry is opaque, so force a full BLAS rebuild.
// The TLAS is also rebuilt so InstanceID carries the material bit.
mesh->blasBuilt = 0;
mesh->dirty = 1;
glRaytracingInvalidateInstancesForMesh(meshHandle, 0);
glRaytracingMarkAllWorldsNeedRebuild();
}
void glRaytracingSetMeshGlass(glRaytracingMeshHandle_t meshHandle, int isGlass)
{
uint32_t flags = glRaytracingGetMeshMaterialFlags(meshHandle);
if (isGlass)
flags |= GL_RAYTRACING_MATERIAL_FLAG_GLASS;
else
flags &= ~GL_RAYTRACING_MATERIAL_FLAG_GLASS;
glRaytracingSetMeshMaterialFlags(meshHandle, flags);
}
void glRaytracingSetMeshAverageColor(glRaytracingMeshHandle_t meshHandle, float r, float g, float b, float a)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
glRaytracingMeshRecord_t* mesh = glRaytracingFindMesh(meshHandle);
if (!mesh)
return;
const float averageColor[4] =
{
glRaytracingClamp<float>(r, 0.0f, 1.0f),
glRaytracingClamp<float>(g, 0.0f, 1.0f),
glRaytracingClamp<float>(b, 0.0f, 1.0f),
glRaytracingClamp<float>(a, 0.0f, 1.0f)
};
if (memcmp(mesh->averageColor, averageColor, sizeof(averageColor)) == 0)
return;
memcpy(mesh->averageColor, averageColor, sizeof(mesh->averageColor));
glRaytracingLightingResetDenoiseHistory();
}
void glRaytracingSetMeshProcMetadata(glRaytracingMeshHandle_t meshHandle, uint32_t procFlags, int areaNum, float emissiveR, float emissiveG, float emissiveB, float emissivePower)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
glRaytracingMeshRecord_t* mesh = glRaytracingFindMesh(meshHandle);
if (!mesh)
return;
const float emissiveColor[4] =
{
glRaytracingClamp<float>(emissiveR, 0.0f, 64.0f),
glRaytracingClamp<float>(emissiveG, 0.0f, 64.0f),
glRaytracingClamp<float>(emissiveB, 0.0f, 64.0f),
glRaytracingClamp<float>(emissivePower, 0.0f, 64.0f)
};
const int32_t clampedAreaNum = (areaNum >= 0) ? (int32_t)areaNum : -1;
if (mesh->procFlags == procFlags &&
mesh->areaNum == clampedAreaNum &&
memcmp(mesh->emissiveColor, emissiveColor, sizeof(emissiveColor)) == 0)
{
return;
}
mesh->procFlags = procFlags;
mesh->areaNum = clampedAreaNum;
memcpy(mesh->emissiveColor, emissiveColor, sizeof(mesh->emissiveColor));
glRaytracingLightingResetDenoiseHistory();
}
void glRaytracingDeleteMesh(glRaytracingMeshHandle_t meshHandle)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
glRaytracingMeshRecord_t* mesh = glRaytracingFindMesh(meshHandle);
if (!mesh)
return;
glRaytracingWaitIdle();
glRaytracingInvalidateInstancesForMesh(meshHandle, 1);
mesh->alive = 0;
mesh->vertexBuffer.resource.Reset();
mesh->indexBuffer.resource.Reset();
mesh->blasScratch.resource.Reset();
mesh->blasResult[0].resource.Reset();
mesh->blasResult[1].resource.Reset();
mesh->blasScratchSize = 0;
mesh->blasResultSize = 0;
mesh->blasBuildFenceValue = 0;
mesh->blasBuilt = 0;
mesh->dirty = 0;
if (meshHandle < g_glRaytracingScene.meshHandleToIndex.size())
g_glRaytracingScene.meshHandleToIndex[meshHandle] = -1;
glRaytracingMarkAllWorldsNeedRebuild();
}
static inline uint32_t glRaytracingNormalizeVisibleInstanceMask(uint32_t mask)
{
mask &= 0xFFu;
return mask ? mask : 0xFFu;
}
glRaytracingInstanceHandle_t glRaytracingCreateInstanceInScene(glRaytracingSceneHandle_t worldHandle, const glRaytracingInstanceDesc_t* desc)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized || !desc)
return 0;
glRaytracingRenderWorld_t* world = glRaytracingFindWorld(worldHandle);
if (!world)
return 0;
if (!glRaytracingFindMeshConst(desc->meshHandle))
return 0;
glRaytracingInstanceRecord_t inst;
inst.handle = world->nextInstanceHandle++;
inst.alive = 1;
inst.descCpu = *desc;
inst.descCpu.mask = glRaytracingNormalizeVisibleInstanceMask(inst.descCpu.mask);
inst.dirty = 1;
world->instances.push_back(inst);
const size_t newIndex = world->instances.size() - 1;
glRaytracingEnsureInstanceHandleTable(world, inst.handle);
world->instanceHandleToIndex[inst.handle] = (int)newIndex;
glRaytracingMarkWorldNeedsRebuild(world);
return inst.handle;
}
int glRaytracingUpdateInstanceInScene(glRaytracingSceneHandle_t worldHandle, glRaytracingInstanceHandle_t instanceHandle, const glRaytracingInstanceDesc_t* desc)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized || !desc)
return 0;
glRaytracingRenderWorld_t* world = glRaytracingFindWorld(worldHandle);
if (!world)
return 0;
if (!glRaytracingFindMeshConst(desc->meshHandle))
return 0;
glRaytracingInstanceRecord_t* inst = glRaytracingFindInstance(world, instanceHandle);
if (!inst)
return 0;
const uint32_t oldMeshHandle = inst->descCpu.meshHandle;
const uint32_t oldMask = ((uint32_t)inst->descCpu.mask) & 0xFFu;
const int wasHidden = (oldMask == 0u);
glRaytracingInstanceDesc_t newDesc = *desc;
// If this instance is hidden, keep it hidden even if the caller's
// transform-update helper sends mask = 0xFF again.
newDesc.mask = wasHidden
? 0u
: glRaytracingNormalizeVisibleInstanceMask(newDesc.mask);
if (memcmp(&inst->descCpu, &newDesc, sizeof(newDesc)) == 0)
return 1;
inst->descCpu = newDesc;
inst->dirty = 1;
if (oldMeshHandle != desc->meshHandle)
{
glRaytracingInvalidateInstanceCache(inst);
glRaytracingMarkWorldNeedsRebuild(world);
}
else
{
glRaytracingMarkWorldNeedsUpdate(world);
}
return 1;
}
void glRaytracingDeleteInstanceInScene(glRaytracingSceneHandle_t worldHandle, glRaytracingInstanceHandle_t instanceHandle)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
glRaytracingRenderWorld_t* world = glRaytracingFindWorld(worldHandle);
if (!world)
return;
glRaytracingInstanceRecord_t* inst = glRaytracingFindInstance(world, instanceHandle);
if (!inst)
return;
glRaytracingInvalidateInstanceCache(inst);
inst->alive = 0;
if (instanceHandle < world->instanceHandleToIndex.size())
world->instanceHandleToIndex[instanceHandle] = -1;
glRaytracingMarkWorldNeedsRebuild(world);
}
int glRaytracingBuildMesh(glRaytracingMeshHandle_t meshHandle)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized)
return 0;
glRaytracingMeshRecord_t* mesh = glRaytracingFindMesh(meshHandle);
if (!mesh)
return 0;
return glRaytracingBuildMeshInternal(mesh);
}
int glRaytracingBuildAllMeshes(void)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized)
return 0;
return glRaytracingBuildDirtyMeshesInternal();
}
int glRaytracingBuildSceneForHandle(glRaytracingSceneHandle_t worldHandle)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized)
return 0;
glRaytracingRenderWorld_t* world = glRaytracingFindWorld(worldHandle);
if (!world)
return 0;
if (!glRaytracingBuildDirtyMeshesInternal())
return 0;
return glRaytracingBuildSceneInternal(world);
}
ID3D12Resource* glRaytracingGetTopLevelASForScene(glRaytracingSceneHandle_t worldHandle)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized)
return nullptr;
glRaytracingRenderWorld_t* world = glRaytracingFindWorld(worldHandle);
if (!world)
return nullptr;
if (!glRaytracingBuildDirtyMeshesInternal())
return nullptr;
if (!glRaytracingBuildSceneInternal(world))
return nullptr;
if (!world->tlasBuilt)
return nullptr;
return glRaytracingGetCurrentTLASBuffer(world)->resource.Get();
}
uint32_t glRaytracingGetMeshCount(void)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
uint32_t count = 0;
for (size_t i = 0; i < g_glRaytracingScene.meshes.size(); ++i)
{
if (g_glRaytracingScene.meshes[i].alive)
++count;
}
return count;
}
uint32_t glRaytracingGetInstanceCountForScene(glRaytracingSceneHandle_t worldHandle)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
const glRaytracingRenderWorld_t* world = glRaytracingFindWorldConst(worldHandle);
return glRaytracingCountAliveInstances(world);
}
// ============================================================
// Lighting state
// ============================================================
struct glRaytracingLightingConstants_t
{
float invViewProj[16];
float invViewMatrix[16];
float viewProj[16];
float cameraPos[4];
float ambientColor[4];
float screenSize[4];
// Keep this CPU layout 16-byte aligned with the HLSL cbuffer.
float normalReconstructZ;
uint32_t lightCount;
uint32_t enableSpecular;
uint32_t enableHalfLambert;
float shadowBias;
uint32_t frameIndex;
uint32_t samplesPerPixel;
uint32_t maxBounces;
uint32_t enableDenoiser;
uint32_t denoisePassIndex;
float denoiseStepWidth;
float denoiseStrength;
float denoisePhiColor;
float denoisePhiNormal;
float denoisePhiPosition;
float bumpStrength;
int32_t secondaryLightBudget;
uint32_t secondaryCorrectionBudget;
float secondaryLightCompensation;
uint32_t historyReset;
uint32_t externalDenoiser;
float giAmount;
float worldGrimeAmount;
uint32_t pad2;
};
struct glRaytracingMeshMetadata_t
{
float averageColor[4];
float emissiveColor[4];
uint32_t procFlags;
int32_t areaNum;
uint32_t vertexOffset;
uint32_t indexOffset;
uint32_t vertexCount;
uint32_t indexCount;
uint32_t pad[2];
};
static const uint32_t GL_RAYTRACING_MAX_IES_TEXTURES = 8;
struct glRaytracingLightingState_t
{
std::vector<glRaytracingLight_t> cpuLights;
std::vector<glRaytracingLight_t> uploadLights;
glRaytracingLightingConstants_t constants;
ComPtr<ID3D12DescriptorHeap> descriptorHeap;
ComPtr<ID3D12DescriptorHeap> descriptorHeapRing[GL_RAYTRACING_CMD_RING_SIZE];
UINT descriptorStride;
glRaytracingBuffer_t constantBuffer;
glRaytracingBuffer_t lightBuffer;
glRaytracingBuffer_t meshMetadataBuffer;
glRaytracingBuffer_t materialVertexBuffer;
glRaytracingBuffer_t materialIndexBuffer;
void* constantBufferMapped;
void* lightBufferMapped;
void* meshMetadataBufferMapped;
glRaytracingBuffer_t constantBufferRing[GL_RAYTRACING_CMD_RING_SIZE];
glRaytracingBuffer_t lightBufferRing[GL_RAYTRACING_CMD_RING_SIZE];
glRaytracingBuffer_t meshMetadataBufferRing[GL_RAYTRACING_CMD_RING_SIZE];
glRaytracingBuffer_t materialVertexBufferRing[GL_RAYTRACING_CMD_RING_SIZE];
glRaytracingBuffer_t materialIndexBufferRing[GL_RAYTRACING_CMD_RING_SIZE];
uint32_t materialVertexCapacityRing[GL_RAYTRACING_CMD_RING_SIZE];
uint32_t materialIndexCapacityRing[GL_RAYTRACING_CMD_RING_SIZE];
uint32_t materialVertexCount;
uint32_t materialIndexCount;
void* constantBufferMappedRing[GL_RAYTRACING_CMD_RING_SIZE];
void* lightBufferMappedRing[GL_RAYTRACING_CMD_RING_SIZE];
void* meshMetadataBufferMappedRing[GL_RAYTRACING_CMD_RING_SIZE];
ComPtr<ID3D12RootSignature> globalRootSig;
ComPtr<ID3D12RootSignature> localRootSig;
ComPtr<ID3D12StateObject> rtStateObject;
ComPtr<ID3D12StateObjectProperties> rtStateProps;
glRaytracingBuffer_t raygenTable;
glRaytracingBuffer_t missTable;
glRaytracingBuffer_t hitTable;
ComPtr<ID3D12PipelineState> denoisePSO;
ComPtr<ID3D12PipelineState> temporalPSO;
glRaytracingTexture_t pathTraceTexture[2];
glRaytracingTexture_t temporalTexture;
glRaytracingTexture_t historyTexture[2];
glRaytracingTexture_t denoiseTemp[2];
uint32_t currentHistoryIndex;
uint32_t currentPathTraceIndex;
glRaytracingBuffer_t denoiseConstantBuffer[3];
glRaytracingBuffer_t denoiseConstantBufferRing[GL_RAYTRACING_CMD_RING_SIZE][3];
void* denoiseConstantBufferMapped[3];
void* denoiseConstantBufferMappedRing[GL_RAYTRACING_CMD_RING_SIZE][3];
UINT denoiseWidth;
UINT denoiseHeight;
DXGI_FORMAT denoiseFormat;
uint32_t frameCounter;
bool externalDenoiser;
ID3D12Resource* emissiveTexture;
DXGI_FORMAT emissiveFormat;
ID3D12Resource* specularTexture;
DXGI_FORMAT specularFormat;
uint32_t iesTextureIds[GL_RAYTRACING_MAX_IES_TEXTURES];
ID3D12Resource* iesTextures[GL_RAYTRACING_MAX_IES_TEXTURES];
DXGI_FORMAT iesFormats[GL_RAYTRACING_MAX_IES_TEXTURES];
bool uploadToCurrentFrameResource;
bool initialized;
glRaytracingSceneHandle_t lastProfileSceneHandle;
uint32_t lastProfileInstanceCount;
uint32_t lastProfileWidth;
uint32_t lastProfileHeight;
glRaytracingLightingState_t()
{
memset(&constants, 0, sizeof(constants));
descriptorStride = 0;
constantBufferMapped = nullptr;
lightBufferMapped = nullptr;
meshMetadataBufferMapped = nullptr;
for (UINT frame = 0; frame < GL_RAYTRACING_CMD_RING_SIZE; ++frame)
{
constantBufferMappedRing[frame] = nullptr;
lightBufferMappedRing[frame] = nullptr;
meshMetadataBufferMappedRing[frame] = nullptr;
materialVertexCapacityRing[frame] = 0;
materialIndexCapacityRing[frame] = 0;
for (int i = 0; i < 3; ++i)
denoiseConstantBufferMappedRing[frame][i] = nullptr;
}
materialVertexCount = 0;
materialIndexCount = 0;
for (int i = 0; i < 3; ++i)
denoiseConstantBufferMapped[i] = nullptr;
denoiseWidth = 0;
denoiseHeight = 0;
denoiseFormat = DXGI_FORMAT_UNKNOWN;
currentHistoryIndex = 0;
currentPathTraceIndex = 0;
frameCounter = 0;
externalDenoiser = false;
emissiveTexture = nullptr;
emissiveFormat = DXGI_FORMAT_R16G16B16A16_FLOAT;
specularTexture = nullptr;
specularFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
for (uint32_t i = 0; i < GL_RAYTRACING_MAX_IES_TEXTURES; ++i)
{
iesTextureIds[i] = 0;
iesTextures[i] = nullptr;
iesFormats[i] = DXGI_FORMAT_R8G8B8A8_UNORM;
}
uploadToCurrentFrameResource = false;
initialized = false;
lastProfileSceneHandle = 0;
lastProfileInstanceCount = 0;
lastProfileWidth = 0;
lastProfileHeight = 0;
}
};
static glRaytracingLightingState_t g_glRaytracingLighting;
static const DXGI_FORMAT GL_RAYTRACING_DENOISE_FORMAT = DXGI_FORMAT_R16G16B16A16_FLOAT;
enum glRaytracingLightingDescriptorIndex_t
{
GLR_DESC_LIGHTS_SRV = 0,
GLR_DESC_ALBEDO_SRV = 1,
GLR_DESC_DEPTH_SRV = 2,
GLR_DESC_NORMAL_SRV = 3,
GLR_DESC_POSITION_SRV = 4,
GLR_DESC_TLAS_SRV = 5,
GLR_DESC_PATHTRACE_SRV = 6,
GLR_DESC_DENOISE_A_SRV = 7,
GLR_DESC_DENOISE_B_SRV = 8,
GLR_DESC_HISTORY_SRV = 9,
GLR_DESC_TEMPORAL_SRV = 10,
GLR_DESC_EMISSIVE_SRV = 11,
GLR_DESC_SPECULAR_SRV = 12,
GLR_DESC_MESH_METADATA_SRV = 13,
GLR_DESC_IES_TEXTURES_SRV = 14,
GLR_DESC_VELOCITY_SRV = GLR_DESC_IES_TEXTURES_SRV + GL_RAYTRACING_MAX_IES_TEXTURES,
GLR_DESC_MATERIAL_VERTICES_SRV = GLR_DESC_VELOCITY_SRV + 1,
GLR_DESC_MATERIAL_INDICES_SRV = GLR_DESC_VELOCITY_SRV + 2,
GLR_DESC_PATHTRACE_UAV = GLR_DESC_VELOCITY_SRV + 3,
GLR_DESC_DENOISE_A_UAV = GLR_DESC_PATHTRACE_UAV + 1,
GLR_DESC_DENOISE_B_UAV = GLR_DESC_PATHTRACE_UAV + 2,
GLR_DESC_OUTPUT_UAV = GLR_DESC_PATHTRACE_UAV + 3,
GLR_DESC_TEMPORAL_UAV = GLR_DESC_PATHTRACE_UAV + 4,
GLR_DESC_HISTORY_UAV = GLR_DESC_PATHTRACE_UAV + 5,
GLR_DESC_COUNT = GLR_DESC_PATHTRACE_UAV + 6,
GLR_DESC_SRV_COUNT = GLR_DESC_PATHTRACE_UAV,
GLR_DESC_UAV_COUNT = 6
};
static void glRaytracingLightingResetDenoiseHistory(void)
{
// Kept under the old name so existing call sites keep compiling. This now
// invalidates the temporal GI accumulator as well as restarting stochastic
// sample indexing after material/camera/light/scene changes.
g_glRaytracingLighting.frameCounter = 0;
g_glRaytracingLighting.constants.frameIndex = 0;
}
static const char* g_glRaytracingLightingHlsl = R"(
struct Light
{
float3 position;
float radius;
float3 color;
float intensity;
float3 normal;
uint type;
float3 axisU;
float halfWidth;
float3 axisV;
float halfHeight;
uint samples;
uint twoSided;
float persistant;
// Reuses the old pad1 slot in glRaytracingLight_t. Keeping this in the
// same 16-byte lane preserves the CPU StructuredBuffer stride while giving
// point/spot lights an explicit volumetric scattering control.
// <= 0 disables the effect. Values around 0.25-1.0 are useful in Doom 3 units.
float volumetricScattering;
// For point lights, this is the axis-aligned XYZ attenuation radius.
// For spot lights, pointRadius.x stores the near clip plane.
// The scalar radius above is still kept as a max/fallback range for point lights,
// as the influence range for rect lights, and as the far clip distance for spot lights.
float3 pointRadius;
float pointRadiusPad;
uint iesTextureIndex; // 0 = none, 1..8 = gIesTextures index + 1
float iesStrength;
int areaNum;
float iesPad;
};
struct MeshMetadata
{
float4 averageColor;
float4 emissiveColor;
uint procFlags;
int areaNum;
uint vertexOffset;
uint indexOffset;
uint vertexCount;
uint indexCount;
uint2 pad;
};
struct RayMaterialVertex
{
float3 position;
float3 normal;
float2 texCoord;
};
struct ShadowPayload
{
uint hit;
};
struct BouncePayload
{
uint hit;
float hitT;
uint materialFlags;
uint meshIndex;
float3 hitNormal;
float2 hitTexCoord;
};
cbuffer LightingCB : register(b0)
{
float4x4 gInvViewProj;
float4x4 gInvViewMatrix;
float4x4 gViewProj;
float4 gCameraPos;
float4 gAmbientColor;
float4 gScreenSize;
float gNormalReconstructZ;
uint gLightCount;
uint gEnableSpecular;
uint gEnableHalfLambert;
float gShadowBias;
uint gFrameIndex;
uint gSamplesPerPixel;
uint gMaxBounces;
uint gEnableDenoiser;
uint gDenoisePassIndex;
float gDenoiseStepWidth;
float gDenoiseStrength;
float gDenoisePhiColor;
float gDenoisePhiNormal;
float gDenoisePhiPosition;
float gBumpStrength;
int gSecondaryLightBudget;
uint gSecondaryCorrectionBudget;
float gSecondaryLightCompensation;
uint gHistoryReset;
uint gExternalDenoiser;
float gGiAmount;
float gWorldGrimeAmount;
uint gPadding2;
};
StructuredBuffer<Light> gLights : register(t0);
Texture2D<float4> gAlbedoTex : register(t1);
Texture2D<float> gDepthTex : register(t2);
Texture2D<float4> gNormalTex : register(t3);
Texture2D<float4> gPositionTex : register(t4);
RaytracingAccelerationStructure gSceneBVH : register(t5);
Texture2D<float4> gEmissiveTex : register(t11);
Texture2D<float4> gSpecularTex : register(t12);
StructuredBuffer<MeshMetadata> gMeshMetadata : register(t13);
Texture2D<float4> gIesTextures[8] : register(t14);
Texture2D<float4> gVelocityTex : register(t22);
StructuredBuffer<RayMaterialVertex> gMaterialVertices : register(t23);
StructuredBuffer<uint> gMaterialIndices : register(t24);
RWTexture2D<float4> gOutputTex : register(u0);
static const uint GL_RAYTRACING_LIGHT_TYPE_POINT = 0;
static const uint GL_RAYTRACING_LIGHT_TYPE_RECT = 1;
static const uint GL_RAYTRACING_LIGHT_TYPE_SPOT = 2;
static const uint GEOMETRY_FLAG_NONE = 0;
static const uint GEOMETRY_FLAG_SKELETAL = 1;
static const uint GEOMETRY_FLAG_UNLIT = 2;
static const uint GEOMETRY_FLAG_GLASS = 4;
static const uint GL_RAYTRACING_PROC_SURFACE_EMISSIVE = 0x00000001u;
static const uint GL_RAYTRACING_PROC_SURFACE_UNLIT = 0x00000002u;
static const uint GL_RAYTRACING_PROC_SURFACE_SKY = 0x00000004u;
static const uint GL_RAYTRACING_PROC_SURFACE_ALPHA_TESTED = 0x00000008u;
static const uint GL_RAYTRACING_PROC_SURFACE_TWO_SIDED = 0x00000010u;
// The shim encodes per-instance material flags into the upper bits of
// D3D12_RAYTRACING_INSTANCE_DESC::InstanceID so any-hit shaders can make
// visibility decisions without binding a separate material table.
static const uint GL_RAYTRACING_INSTANCE_USER_ID_MASK = 0x0000FFFFu;
static const uint GL_RAYTRACING_INSTANCE_MATERIAL_SHIFT = 16u;
static const uint GL_RAYTRACING_INSTANCE_MATERIAL_MASK = 0x000000FFu;
static const uint GL_RAYTRACING_MATERIAL_FLAG_GLASS = 0x00000001u;
uint DecodeInstanceMaterialFlags()
{
return (InstanceID() >> GL_RAYTRACING_INSTANCE_MATERIAL_SHIFT) &
GL_RAYTRACING_INSTANCE_MATERIAL_MASK;
}
bool CurrentRayHitIsGlass()
{
return (DecodeInstanceMaterialFlags() & GL_RAYTRACING_MATERIAL_FLAG_GLASS) != 0u;
}
float3 GetMeshAverageColor(uint meshIndex)
{
float3 c = gMeshMetadata[meshIndex].averageColor.rgb;
return max(saturate(c), float3(0.04, 0.04, 0.04));
}
MeshMetadata GetMeshMetadataSafe(uint meshIndex)
{
return gMeshMetadata[meshIndex];
}
uint MeshMetadataGeometryFlags(MeshMetadata meta, uint materialFlags)
{
uint geoFlags = GEOMETRY_FLAG_NONE;
if ((meta.procFlags & (GL_RAYTRACING_PROC_SURFACE_UNLIT | GL_RAYTRACING_PROC_SURFACE_SKY)) != 0u)
geoFlags |= GEOMETRY_FLAG_UNLIT;
if ((materialFlags & GL_RAYTRACING_MATERIAL_FLAG_GLASS) != 0u)
geoFlags |= GEOMETRY_FLAG_GLASS;
return geoFlags;
}
float3 GetMeshEmissiveRadiance(MeshMetadata meta)
{
if ((meta.procFlags & GL_RAYTRACING_PROC_SURFACE_EMISSIVE) == 0u)
return 0.0;
float power = max(meta.emissiveColor.a, 1.0);
float3 e = max(meta.emissiveColor.rgb * power, 0.0);
float peak = max(max(e.r, e.g), e.b);
if (peak > 8.0 && peak > 1e-5)
e *= 8.0 / peak;
return e;
}
uint DecodeGeometryFlag(float geoFlag)
{
// position.w comes from a render target / buffer path, so do not require exact
// float equality. Values like 0.999, 1.001, 1.99, 2.02 should decode correctly.
//
// Clamp negative garbage to 0, then round to nearest integer flag.
float f = max(geoFlag, 0.0);
return (uint)floor(f + 0.5);
}
bool GeometryFlagEquals(float geoFlag, uint expectedFlag)
{
return DecodeGeometryFlag(geoFlag) == expectedFlag;
}
bool GeometryFlagHas(float geoFlag, uint expectedFlag)
{
// Supports both current single-value usage and future bitmask usage.
uint decoded = DecodeGeometryFlag(geoFlag);
return (decoded & expectedFlag) != 0u;
}
float3 LoadScenePosition(uint2 pixel)
{
float4 p = gPositionTex.Load(int3(pixel, 0));
return p.xyz;
}
float4 LoadSceneNormal(uint2 pixel)
{
float4 nSample = gNormalTex.Load(int3(pixel, 0));
return nSample;
}
float LoadSceneSurfaceRoughness(float4 normalSample)
{
// The raster G-buffer stores authored/material roughness in normal.w.
// Keep a small floor so glossy materials do not become perfect mirrors.
return clamp(normalSample.w, 0.05, 1.0);
}
float SafeLengthSq(float3 v)
{
return max(dot(v, v), 1e-8);
}
float3 SafeNormalizeLocal(float3 v, float3 fallback)
{
float lenSq = dot(v, v);
return (lenSq > 1e-8) ? (v * rsqrt(lenSq)) : fallback;
}
float BumpLuminance(float3 c)
{
return dot(c, float3(0.299, 0.587, 0.114));
}
float3 EnhanceBumpNormal(uint2 pixel, float3 worldPos, float3 baseAlbedo, bool isSkeletal)
{
float3 N = SafeNormalizeLocal(LoadSceneNormal(pixel).xyz, float3(0.0, 0.0, 1.0));
// Character albedo has skin, cloth, and painted detail that should not be
// interpreted as height. The synthetic bump pass looks good on old walls,
// but it makes animated models crawl and self-shadow like noisy relief maps.
if (isSkeletal)
return N;
float strength = max(gBumpStrength, 0.0);
if (strength <= 0.001)
return N;
int2 p = int2(pixel);
int2 maxP = int2((int)gScreenSize.x - 1, (int)gScreenSize.y - 1);
int2 pxm = clamp(p + int2(-1, 0), int2(0, 0), maxP);
int2 pxp = clamp(p + int2( 1, 0), int2(0, 0), maxP);
int2 pym = clamp(p + int2( 0, -1), int2(0, 0), maxP);
int2 pyp = clamp(p + int2( 0, 1), int2(0, 0), maxP);
float3 posL = gPositionTex.Load(int3(pxm, 0)).xyz;
float3 posR = gPositionTex.Load(int3(pxp, 0)).xyz;
float3 posU = gPositionTex.Load(int3(pym, 0)).xyz;
float3 posD = gPositionTex.Load(int3(pyp, 0)).xyz;
float3 T = posR - posL;
float3 B = posD - posU;
// Fall back to a stable tangent basis when the position buffer is flat or invalid.
if (dot(T, T) <= 1e-8 || dot(B, B) <= 1e-8)
{
float3 up = (abs(N.z) < 0.999) ? float3(0.0, 0.0, 1.0) : float3(0.0, 1.0, 0.0);
T = SafeNormalizeLocal(cross(up, N), float3(1.0, 0.0, 0.0));
B = cross(N, T);
}
else
{
T = SafeNormalizeLocal(T - N * dot(N, T), float3(1.0, 0.0, 0.0));
B = SafeNormalizeLocal(B - N * dot(N, B), float3(0.0, 1.0, 0.0));
}
float hL = BumpLuminance(saturate(gAlbedoTex.Load(int3(pxm, 0)).rgb));
float hR = BumpLuminance(saturate(gAlbedoTex.Load(int3(pxp, 0)).rgb));
float hU = BumpLuminance(saturate(gAlbedoTex.Load(int3(pym, 0)).rgb));
float hD = BumpLuminance(saturate(gAlbedoTex.Load(int3(pyp, 0)).rgb));
// Height-gradient bump from the diffuse texture. The scale is intentionally
// aggressive because the current renderer has no dedicated height/normal map
// slot here, and old idTech/Build textures need the fake relief to read.
float dhdx = (hR - hL);
float dhdy = (hD - hU);
float3 heightNormal = SafeNormalizeLocal(N - (T * dhdx + B * dhdy) * (strength * 4.25), N);
float3 nL = SafeNormalizeLocal(gNormalTex.Load(int3(pxm, 0)).xyz, N);
float3 nR = SafeNormalizeLocal(gNormalTex.Load(int3(pxp, 0)).xyz, N);
float3 nU = SafeNormalizeLocal(gNormalTex.Load(int3(pym, 0)).xyz, N);
float3 nD = SafeNormalizeLocal(gNormalTex.Load(int3(pyp, 0)).xyz, N);
float3 avgN = SafeNormalizeLocal((nL + nR + nU + nD) * 0.25, N);
// Amplify real G-buffer normal-map variation as well.
float3 detailN = SafeNormalizeLocal(N + (N - avgN) * (strength * 1.75), N);
float3 outN = SafeNormalizeLocal(lerp(detailN, heightNormal, 0.65), N);
// Avoid flipping normals so far that shadows/specular explode.
if (dot(outN, N) < 0.25)
outN = SafeNormalizeLocal(lerp(N, outN, 0.45), N);
return outN;
}
float3 StabilizeSkeletalNormal(uint2 pixel, float3 worldPos, float3 N)
{
int2 p = int2(pixel);
int2 maxP = int2((int)gScreenSize.x - 1, (int)gScreenSize.y - 1);
float3 accum = N * 3.0;
float weightSum = 3.0;
[unroll]
for (int y = -1; y <= 1; ++y)
{
[unroll]
for (int x = -1; x <= 1; ++x)
{
if (x == 0 && y == 0)
continue;
int2 sp = clamp(p + int2(x, y), int2(0, 0), maxP);
float4 samplePos = gPositionTex.Load(int3(sp, 0));
uint sampleGeoFlag = DecodeGeometryFlag(samplePos.w);
if ((sampleGeoFlag & GEOMETRY_FLAG_SKELETAL) == 0u)
continue;
float3 sampleDelta = samplePos.xyz - worldPos;
float distSq = dot(sampleDelta, sampleDelta);
if (distSq > 16.0)
continue;
float3 sampleN = SafeNormalizeLocal(gNormalTex.Load(int3(sp, 0)).xyz, N);
if (dot(sampleN, N) < 0.0)
sampleN = -sampleN;
float nd = saturate(dot(sampleN, N));
if (nd < 0.20)
continue;
float w = nd * rcp(1.0 + distSq * 0.35);
accum += sampleN * w;
weightSum += w;
}
}
float3 smoothed = SafeNormalizeLocal(accum / max(weightSum, 1.0e-4), N);
return SafeNormalizeLocal(lerp(N, smoothed, 0.70), N);
}
float3 TransformObjectNormalToWorld(float3 n)
{
float3x4 objectToWorld = ObjectToWorld3x4();
float3 worldN = float3(
dot(objectToWorld[0].xyz, n),
dot(objectToWorld[1].xyz, n),
dot(objectToWorld[2].xyz, n));
return SafeNormalizeLocal(worldN, n);
}
void DecodeRayMaterialAtHit(uint meshIndex, BuiltInTriangleIntersectionAttributes attr, out float3 hitNormal, out float2 hitTexCoord)
{
MeshMetadata meta = GetMeshMetadataSafe(meshIndex);
uint localTriangleIndex = PrimitiveIndex() * 3u;
hitNormal = float3(0.0, 0.0, 0.0);
hitTexCoord = float2(0.0, 0.0);
if (meta.indexCount < localTriangleIndex + 3u || meta.vertexCount == 0u)
return;
uint i0 = gMaterialIndices[meta.indexOffset + localTriangleIndex + 0u];
uint i1 = gMaterialIndices[meta.indexOffset + localTriangleIndex + 1u];
uint i2 = gMaterialIndices[meta.indexOffset + localTriangleIndex + 2u];
RayMaterialVertex v0 = gMaterialVertices[i0];
RayMaterialVertex v1 = gMaterialVertices[i1];
RayMaterialVertex v2 = gMaterialVertices[i2];
float3 bary = float3(1.0 - attr.barycentrics.x - attr.barycentrics.y, attr.barycentrics.x, attr.barycentrics.y);
float3 localN = SafeNormalizeLocal(v0.normal * bary.x + v1.normal * bary.y + v2.normal * bary.z, float3(0.0, 0.0, 1.0));
hitNormal = TransformObjectNormalToWorld(localN);
hitTexCoord = v0.texCoord * bary.x + v1.texCoord * bary.y + v2.texCoord * bary.z;
}
[shader("miss")]
void ShadowMiss(inout ShadowPayload payload)
{
payload.hit = 0;
}
[shader("anyhit")]
void ShadowAnyHit(inout ShadowPayload payload, in BuiltInTriangleIntersectionAttributes attr)
{
// Glass/alpha-blended surfaces should participate in the primary/raster image,
// but visibility rays must continue through them. Mark the BLAS geometry
// non-opaque on the CPU side so this any-hit shader runs, then IgnoreHit()
// lets the ray keep going to whatever is behind the pane/sprite.
if (CurrentRayHitIsGlass())
{
IgnoreHit();
return;
}
}
[shader("closesthit")]
void ShadowClosestHit(inout ShadowPayload payload, in BuiltInTriangleIntersectionAttributes attr)
{
// Safety fallback for incorrectly-built glass geometry. Correct glass meshes
// are non-opaque and are ignored by ShadowAnyHit() above.
if (CurrentRayHitIsGlass())
{
payload.hit = 0;
return;
}
payload.hit = 1;
}
[shader("miss")]
void BounceMiss(inout BouncePayload payload)
{
payload.hit = 0;
payload.hitT = 0.0;
payload.materialFlags = 0;
payload.meshIndex = 0;
payload.hitNormal = 0.0;
payload.hitTexCoord = 0.0;
}
[shader("anyhit")]
void BounceAnyHit(inout BouncePayload payload, in BuiltInTriangleIntersectionAttributes attr)
{
// Secondary diffuse rays should see through the same transparent surfaces that
// shadow rays see through. This keeps a pane/sprite from killing all bounced
// light behind it.
if (CurrentRayHitIsGlass())
{
IgnoreHit();
return;
}
}
[shader("closesthit")]
void BounceClosestHit(inout BouncePayload payload, in BuiltInTriangleIntersectionAttributes attr)
{
// Fallback for transparent geometry that was accidentally built opaque.
// Correctly tagged transparent surfaces are ignored in BounceAnyHit() above.
if (CurrentRayHitIsGlass())
{
payload.hit = 0;
payload.hitT = 0.0;
payload.materialFlags = DecodeInstanceMaterialFlags();
payload.meshIndex = InstanceID() & GL_RAYTRACING_INSTANCE_USER_ID_MASK;
payload.hitNormal = 0.0;
payload.hitTexCoord = 0.0;
return;
}
payload.hit = 1;
payload.hitT = RayTCurrent();
payload.materialFlags = DecodeInstanceMaterialFlags();
payload.meshIndex = InstanceID() & GL_RAYTRACING_INSTANCE_USER_ID_MASK;
DecodeRayMaterialAtHit(payload.meshIndex, attr, payload.hitNormal, payload.hitTexCoord);
}
)"
R"(
[shader("closesthit")]
void ReflectionClosestHit(inout BouncePayload payload, in BuiltInTriangleIntersectionAttributes attr)
{
// Specular rays are view rays, not diffuse/visibility rays. They should
// report the first reflected surface even when that surface was tagged as
// glass/alpha, otherwise glass panes/sprites vanish from mirror-like hits.
payload.hit = 1;
payload.hitT = RayTCurrent();
payload.materialFlags = DecodeInstanceMaterialFlags();
payload.meshIndex = InstanceID() & GL_RAYTRACING_INSTANCE_USER_ID_MASK;
DecodeRayMaterialAtHit(payload.meshIndex, attr, payload.hitNormal, payload.hitTexCoord);
}
)"
R"(
float TraceShadow(float3 origin, float3 dir, float maxT)
{
RayDesc ray;
ray.Origin = origin;
ray.Direction = dir;
ray.TMin = 5.0;
ray.TMax = maxT;
ShadowPayload payload;
payload.hit = 0;
TraceRay(
gSceneBVH,
RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH,
0xFF,
0,
0,
0,
ray,
payload);
return (payload.hit != 0) ? 0.0 : 1.0;
}
bool TraceBounce(float3 origin, float3 dir, float maxT, out float hitT, out uint materialFlags, out uint meshIndex, out float3 hitNormal, out float2 hitTexCoord)
{
RayDesc ray;
ray.Origin = origin;
ray.Direction = dir;
ray.TMin = 0.001;
ray.TMax = maxT;
BouncePayload payload;
payload.hit = 0;
payload.hitT = 0.0;
payload.materialFlags = 0;
payload.meshIndex = 0;
payload.hitNormal = 0.0;
payload.hitTexCoord = 0.0;
TraceRay(
gSceneBVH,
RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH,
0xFF,
1,
0,
1,
ray,
payload);
hitT = payload.hitT;
materialFlags = payload.materialFlags;
meshIndex = payload.meshIndex;
hitNormal = payload.hitNormal;
hitTexCoord = payload.hitTexCoord;
return payload.hit != 0;
}
bool TraceSpecularReflection(float3 origin, float3 dir, float maxT, out float hitT, out uint materialFlags, out uint meshIndex, out float3 hitNormal, out float2 hitTexCoord)
{
RayDesc ray;
ray.Origin = origin;
ray.Direction = dir;
ray.TMin = 0.001;
ray.TMax = maxT;
BouncePayload payload;
payload.hit = 0;
payload.hitT = 0.0;
payload.materialFlags = 0;
payload.meshIndex = 0;
payload.hitNormal = 0.0;
payload.hitTexCoord = 0.0;
TraceRay(
gSceneBVH,
RAY_FLAG_NONE,
0xFF,
2,
0,
1,
ray,
payload);
hitT = payload.hitT;
materialFlags = payload.materialFlags;
meshIndex = payload.meshIndex;
hitNormal = payload.hitNormal;
hitTexCoord = payload.hitTexCoord;
return payload.hit != 0;
}
)"
R"(
float Hash12(float2 p)
{
float3 p3 = frac(float3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return frac((p3.x + p3.y) * p3.z);
}
float Hash13(float3 p)
{
p = frac(p * 0.1031);
p += dot(p, p.yzx + 33.33);
return frac((p.x + p.y) * p.z);
}
float ValueNoise3D(float3 p)
{
float3 i = floor(p);
float3 f = frac(p);
f = f * f * (3.0 - 2.0 * f);
float n000 = Hash13(i + float3(0, 0, 0));
float n100 = Hash13(i + float3(1, 0, 0));
float n010 = Hash13(i + float3(0, 1, 0));
float n110 = Hash13(i + float3(1, 1, 0));
float n001 = Hash13(i + float3(0, 0, 1));
float n101 = Hash13(i + float3(1, 0, 1));
float n011 = Hash13(i + float3(0, 1, 1));
float n111 = Hash13(i + float3(1, 1, 1));
float nx00 = lerp(n000, n100, f.x);
float nx10 = lerp(n010, n110, f.x);
float nx01 = lerp(n001, n101, f.x);
float nx11 = lerp(n011, n111, f.x);
float nxy0 = lerp(nx00, nx10, f.y);
float nxy1 = lerp(nx01, nx11, f.y);
return lerp(nxy0, nxy1, f.z);
}
void ApplyWorldGrime(float3 worldPos, float cavity, float ao, bool isSkeletal, inout float3 baseAlbedo, inout float3 specularAlbedo)
{
if (isSkeletal || gWorldGrimeAmount <= 0.0001)
return;
float crevice = saturate((1.0 - cavity) * 1.35 + (1.0 - ao) * 0.55);
float largeNoise = ValueNoise3D(worldPos * 0.025);
float fineNoise = ValueNoise3D(worldPos * 0.11 + float3(17.0, 41.0, 9.0));
float grimeMask = saturate(crevice * (0.65 + 0.55 * largeNoise) + fineNoise * 0.18);
float amount = saturate(grimeMask * gWorldGrimeAmount);
float3 dirtTint = float3(0.36, 0.31, 0.24);
baseAlbedo = lerp(baseAlbedo, baseAlbedo * dirtTint, amount);
specularAlbedo *= lerp(1.0, 0.55, amount);
}
float2 Hammersley2D(uint i, uint N, float rand)
{
float e1 = frac((float)i / (float)N + rand);
uint bits = i;
bits = (bits << 16) | (bits >> 16);
bits = ((bits & 0x55555555u) << 1) | ((bits & 0xAAAAAAAAu) >> 1);
bits = ((bits & 0x33333333u) << 2) | ((bits & 0xCCCCCCCCu) >> 2);
bits = ((bits & 0x0F0F0F0Fu) << 4) | ((bits & 0xF0F0F0F0u) >> 4);
bits = ((bits & 0x00FF00FFu) << 8) | ((bits & 0xFF00FF00u) >> 8);
float e2 = (float)bits * 2.3283064365386963e-10;
return float2(e1, e2);
}
float2 ConcentricSampleDisk(float2 u)
{
float2 uOffset = 2.0 * u - 1.0;
if (uOffset.x == 0.0 && uOffset.y == 0.0)
return float2(0.0, 0.0);
float r, theta;
if (abs(uOffset.x) > abs(uOffset.y))
{
r = uOffset.x;
theta = (3.14159265 / 4.0) * (uOffset.y / uOffset.x);
}
else
{
r = uOffset.y;
theta = (3.14159265 / 2.0) - (3.14159265 / 4.0) * (uOffset.x / uOffset.y);
}
return r * float2(cos(theta), sin(theta));
}
void BuildOrthonormalBasis(float3 n, out float3 t, out float3 b)
{
float3 up = (abs(n.z) < 0.999) ? float3(0.0, 0.0, 1.0) : float3(0.0, 1.0, 0.0);
t = normalize(cross(up, n));
b = cross(n, t);
}
float3 CosineSampleHemisphere(float2 u)
{
float2 d = ConcentricSampleDisk(u);
float z = sqrt(saturate(1.0 - dot(d, d)));
return float3(d.x, d.y, z);
}
float3 GetPointLightRadius(Light Lgt)
{
float scalarRadius = max(abs(Lgt.radius), 1e-4);
float3 r = abs(Lgt.pointRadius);
// Allow older/zero-initialized light records to behave like the old scalar radius.
if (max(max(r.x, r.y), r.z) <= 1e-4)
{
r = float3(scalarRadius, scalarRadius, scalarRadius);
}
return max(r, float3(1e-4, 1e-4, 1e-4));
}
float GetPointLightMaxRadius(Light Lgt)
{
float3 r = GetPointLightRadius(Lgt);
return max(max(r.x, r.y), r.z);
}
)"
R"(
float3 Doom3SafeNormalizeOr(float3 v, float3 fallbackDir)
{
float lenSq = dot(v, v);
return (lenSq > 1e-8) ? (v * rsqrt(lenSq)) : fallbackDir;
}
)"
R"(
float Doom3QuadraticFalloffImage(float texCoord)
{
// Math version of Doom 3 BFG's built-in _quadratic lookup table.
// The source table is 32 texels wide, brightest at the center and clamped
// to black outside the light volume.
if (texCoord <= 0.0 || texCoord >= 1.0)
return 0.0;
const float QUADRATIC_WIDTH = 32.0;
// Convert a normalized lookup coordinate to the source generator's texel-space
// x value, then apply the same centered squared ramp used by R_QuadraticImage().
float x = texCoord * QUADRATIC_WIDTH - 0.5;
float d = x - (QUADRATIC_WIDTH * 0.5 - 0.5);
d = abs(d);
d -= 0.5;
d /= (QUADRATIC_WIDTH * 0.5);
d = 1.0 - d;
d = saturate(d);
return d * d;
}
)"
R"(
float Doom3QuadraticCentered(float centeredCoord)
{
// centeredCoord is -1 at one side of the light volume, 0 at the light center,
// and +1 at the opposite side.
return Doom3QuadraticFalloffImage(centeredCoord * 0.5 + 0.5);
}
float Doom3ProjectionTexture2D(float2 centeredCoord)
{
// Doom 3 multiplies a projected light image by a separate falloff image. This
// renderer does not bind Doom light materials/cookies, so use the same built-in
// quadratic shape on S/T as a neutral default projection texture approximation.
if (abs(centeredCoord.x) >= 1.0 || abs(centeredCoord.y) >= 1.0)
return 0.0;
return Doom3QuadraticCentered(centeredCoord.x) * Doom3QuadraticCentered(centeredCoord.y);
}
int WrapIESHorizontalCoord(int x)
{
x = x % 512;
return (x < 0) ? (x + 512) : x;
}
float SampleIESTexture(Light Lgt, float3 lightToSurface)
{
if (Lgt.iesTextureIndex == 0u || Lgt.iesStrength <= 0.0)
return 1.0;
float3 dir = Doom3SafeNormalizeOr(lightToSurface, Doom3SafeNormalizeOr(Lgt.normal, float3(0.0, 0.0, 1.0)));
float3 axisW = Doom3SafeNormalizeOr(Lgt.normal, float3(0.0, 0.0, 1.0));
float3 axisU = Doom3SafeNormalizeOr(Lgt.axisU, float3(1.0, 0.0, 0.0));
float3 axisV = Doom3SafeNormalizeOr(Lgt.axisV, float3(0.0, 1.0, 0.0));
float localX = dot(dir, axisU);
float localY = dot(dir, axisV);
float localZ = dot(dir, axisW);
float horizontal = atan2(localY, localX) * (0.15915494309189535) + 0.5;
float vertical = acos(clamp(localZ, -1.0, 1.0)) * (0.3183098861837907);
uint texIndex = min(Lgt.iesTextureIndex - 1u, 7u);
float u = frac(horizontal) * 512.0 - 0.5;
float v = saturate(vertical) * 256.0 - 0.5;
int x0 = (int)floor(u);
int y0 = (int)floor(v);
float tx = frac(u);
float ty = frac(v);
int x1 = x0 + 1;
int y1 = y0 + 1;
x0 = WrapIESHorizontalCoord(x0);
x1 = WrapIESHorizontalCoord(x1);
y0 = clamp(y0, 0, 255);
y1 = clamp(y1, 0, 255);
float p00 = gIesTextures[texIndex].Load(int3(x0, y0, 0)).r;
float p10 = gIesTextures[texIndex].Load(int3(x1, y0, 0)).r;
float p01 = gIesTextures[texIndex].Load(int3(x0, y1, 0)).r;
float p11 = gIesTextures[texIndex].Load(int3(x1, y1, 0)).r;
float profile = lerp(lerp(p00, p10, tx), lerp(p01, p11, tx), ty);
return lerp(1.0, profile, saturate(Lgt.iesStrength));
}
float ApplyIESProfile(float attenuation, Light Lgt, float3 lightToSurface)
{
if (attenuation <= 0.0 || Lgt.iesTextureIndex == 0u || Lgt.iesStrength <= 0.0)
return attenuation;
return attenuation * SampleIESTexture(Lgt, lightToSurface);
}
)"
R"(
float Doom3ProjectedDepthFalloff(float depth, float nearClip, float farClip)
{
// For spot/projected lights, the renderer API supplies a conventional near/far
// range. Sample the bright-to-far half of Doom's centered falloff image so the
// light is strongest at the projector and fades out at the far plane.
float depth01 = saturate((depth - nearClip) / max(farClip - nearClip, 1e-4));
return Doom3QuadraticFalloffImage(0.5 + depth01 * 0.5);
}
float ComputePointLightAttenuation(float3 worldPos, Light Lgt)
{
float3 radii = GetPointLightRadius(Lgt);
float3 offset = worldPos - Lgt.position;
// Doom 3 point lights are box/projector lights, not inverse-square or
// ellipsoidal distance lights. S/T sample the projected light image and the
// third axis samples lightFalloffImage. Use the light axes when provided so
// rectangular radii behave like idTech4 light volumes.
float3 axisU = Doom3SafeNormalizeOr(Lgt.axisU, float3(1.0, 0.0, 0.0));
float3 axisV = Doom3SafeNormalizeOr(Lgt.axisV, float3(0.0, 1.0, 0.0));
float3 axisW = Doom3SafeNormalizeOr(Lgt.normal, float3(0.0, 0.0, 1.0));
float u = dot(offset, axisU) / radii.x;
float v = dot(offset, axisV) / radii.y;
float w = dot(offset, axisW) / radii.z;
if (abs(u) >= 1.0 || abs(v) >= 1.0 || abs(w) >= 1.0)
return 0.0;
float projection = Doom3ProjectionTexture2D(float2(u, v));
float falloff = Doom3QuadraticCentered(w);
return projection * falloff;
}
)"
R"(
float ComputeSpotLightAttenuation(float3 worldPos, Light Lgt)
{
float3 lightToSurface = worldPos - Lgt.position;
float nearClip = max(Lgt.pointRadius.x, 0.0);
float farClip = max(Lgt.radius, nearClip + 1e-4);
float3 spotDir = Doom3SafeNormalizeOr(Lgt.normal, float3(0.0, 0.0, 1.0));
float depth = dot(lightToSurface, spotDir);
if (Lgt.iesTextureIndex != 0u && Lgt.iesStrength > 0.0)
{
if (depth <= 1e-4 || depth >= farClip)
return 0.0;
float t = saturate(depth / max(farClip, 1e-4));
return 1.0 - smoothstep(0.985, 1.0, t);
}
if (depth <= nearClip || depth >= farClip)
return 0.0;
float3 axisU = Doom3SafeNormalizeOr(Lgt.axisU, float3(1.0, 0.0, 0.0));
float3 axisV = Doom3SafeNormalizeOr(Lgt.axisV, float3(0.0, 1.0, 0.0));
float invDepth = 1.0 / max(depth, 1e-4);
float halfU = max(abs(Lgt.halfWidth), 1e-4);
float halfV = max(abs(Lgt.halfHeight), 1e-4);
float signedU = (dot(lightToSurface, axisU) * invDepth) / halfU;
float signedV = (dot(lightToSurface, axisV) * invDepth) / halfV;
if (abs(signedU) >= 1.0 || abs(signedV) >= 1.0)
return 0.0;
float projection = Doom3ProjectionTexture2D(float2(signedU, signedV));
float range = max(farClip - nearClip, 1e-4);
float t = saturate((depth - nearClip) / range);
/*
Stronger Doom 3-style projected-light behavior:
almost no depth attenuation, only a tiny fade at the very end.
*/
float falloff = 1.0 - smoothstep(0.985, 1.0, t);
/*
Slight projected-light boost.
This helps match Doom 3's aggressive light volumes without changing shape.
*/
float boost = 1.35;
return saturate(projection * falloff * boost);
}
float TraceSpotShadow(float3 worldPos, float3 N, float3 toLight, float dist)
{
float3 L = toLight / max(dist, 1e-6);
float NdotLRaw = saturate(dot(N, L));
float normalBias = lerp(gShadowBias * 3.0, gShadowBias * 0.75, NdotLRaw);
float3 shadowOrigin = worldPos + N * normalBias + L * (gShadowBias * 0.5);
float shadowTMax = max(dist - gShadowBias * 0.5, 0.001);
return TraceShadow(shadowOrigin, L, shadowTMax);
}
float TraceSoftShadow(float3 worldPos, float3 N, Light Lgt, float3 toLight, float dist)
{
const uint SHADOW_SAMPLES = 4;
float3 L = toLight / max(dist, 1e-6);
float3 tangent, bitangent;
BuildOrthonormalBasis(L, tangent, bitangent);
float areaRadius = max(GetPointLightMaxRadius(Lgt) * 0.03, 0.12);
float shadowAccum = 0.0;
float rand = Hash12(worldPos.xy + float2(worldPos.z, dist));
[unroll]
for (uint s = 0; s < SHADOW_SAMPLES; ++s)
{
float2 xi = Hammersley2D(s, SHADOW_SAMPLES, rand);
float2 d = ConcentricSampleDisk(xi) * areaRadius;
float3 sampleLightPos = Lgt.position + tangent * d.x + bitangent * d.y;
float3 sampleVec = sampleLightPos - worldPos;
float sampleDist = length(sampleVec);
if (sampleDist <= 1e-4)
{
shadowAccum += 1.0;
continue;
}
float3 sampleDir = sampleVec / sampleDist;
float NdotLRaw = saturate(dot(N, sampleDir));
float normalBias = lerp(gShadowBias * 3.0, gShadowBias * 0.75, NdotLRaw);
float3 shadowOrigin = worldPos + N * normalBias + sampleDir * (gShadowBias * 0.5);
float shadowTMax = max(sampleDist - gShadowBias * 0.5, 0.001);
shadowAccum += TraceShadow(shadowOrigin, sampleDir, shadowTMax);
}
return shadowAccum / (float)SHADOW_SAMPLES;
}
)"
R"(
float RectLightShadow(float3 worldPos, float3 N, Light Lgt, uint2 pixel)
{
uint sampleCount = max(Lgt.samples, 1u);
sampleCount = min(sampleCount, 4u);
float visibility = 0.0;
float rand = Hash12((float2)pixel + worldPos.xy + float2(worldPos.z, dot(N.xy, N.xy)));
float NoL_center = saturate(dot(N, normalize(Lgt.position - worldPos)));
float normalBias = lerp(gShadowBias * 4.0, gShadowBias * 0.75, NoL_center);
float3 baseOrigin = worldPos + N * normalBias;
[loop]
for (uint s = 0; s < sampleCount; ++s)
{
float2 xi = Hammersley2D(s, sampleCount, rand);
float2 uv = xi * 2.0 - 1.0;
float3 sampleLightPos =
Lgt.position +
Lgt.axisU * (uv.x * Lgt.halfWidth) +
Lgt.axisV * (uv.y * Lgt.halfHeight);
float3 toLight = sampleLightPos - baseOrigin;
float distToLight = length(toLight);
if (distToLight <= 1e-4)
{
visibility += 1.0;
continue;
}
float3 L = toLight / distToLight;
float NdotL = dot(N, L);
if (NdotL <= 0.0)
{
continue;
}
float emitTerm = (Lgt.twoSided != 0)
? abs(dot(Lgt.normal, -L))
: dot(Lgt.normal, -L);
if (emitTerm <= 0.0)
{
continue;
}
float3 shadowOrigin = baseOrigin + L * (gShadowBias * 0.5);
float shadowTMax = max(distToLight - gShadowBias, 0.001);
visibility += TraceShadow(shadowOrigin, L, shadowTMax);
}
return visibility / (float)sampleCount;
}
float ComputeAmbientOcclusion(float3 worldPos, float3 N, uint2 pixel, bool isSkeletal)
{
const uint AO_SAMPLES = 8;
float aoRadius = isSkeletal ? 18.0 : 32.0;
float3 tangent, bitangent;
BuildOrthonormalBasis(N, tangent, bitangent);
float rand = Hash12((float2)pixel + worldPos.xy + worldPos.zz);
float visibility = 0.0;
[unroll]
for (uint i = 0; i < AO_SAMPLES; ++i)
{
float2 xi = Hammersley2D(i, AO_SAMPLES, rand);
float3 h = CosineSampleHemisphere(xi);
float3 aoDir =
tangent * h.x +
bitangent * h.y +
N * h.z;
aoDir = normalize(aoDir);
float3 aoOrigin = worldPos + N * (gShadowBias * (isSkeletal ? 0.75 : 0.15));
visibility += TraceShadow(aoOrigin, aoDir, aoRadius);
}
visibility /= (float)AO_SAMPLES;
visibility = saturate(pow(visibility, 1.5));
if (isSkeletal)
visibility = lerp(visibility, 1.0, 0.45);
return visibility;
}
float3 GetSkyLightDirection10AM()
{
// Direction from the shaded point TO the sky/sun.
// 10:00 AM style: angled, not straight vertical.
//
// Flip X/Y signs if you want the shadows cast the opposite horizontal way.
return normalize(float3(-0.55, -0.25, 0.80));
}
)"
R"(
float ComputeSkyVisibility(float3 worldPos, float3 N, uint2 pixel)
{
const uint SKY_SAMPLES = 4;
const float SKY_TMAX = 1000000.0;
// Soft angular size of the sky/sun shadow cone.
// Larger = softer shadows, but more chance of light leaking.
const float SKY_SOFTNESS = 0.085;
float3 skyCenterDir = GetSkyLightDirection10AM();
float NoSky = dot(N, skyCenterDir);
// Mostly back-facing relative to the sky direction.
// Return black visibility instead of casting unstable grazing rays.
if (NoSky <= -0.35)
{
return 0.0;
}
float3 tangent, bitangent;
BuildOrthonormalBasis(skyCenterDir, tangent, bitangent);
float normalBias = lerp(gShadowBias * 4.0, gShadowBias * 1.0, saturate(NoSky));
float3 baseOrigin =
worldPos +
N * normalBias +
skyCenterDir * (gShadowBias * 2.0);
float visibility = 0.0;
// IMPORTANT:
// No per-pixel random rotation here.
// The old noise came from random hemisphere sky sampling.
// This keeps the soft shadow sampling pattern stable per pixel/frame.
[unroll]
for (uint i = 0; i < SKY_SAMPLES; ++i)
{
float2 xi = Hammersley2D(i, SKY_SAMPLES, 0.0);
float2 d = ConcentricSampleDisk(xi) * SKY_SOFTNESS;
float3 skyDir = normalize(
skyCenterDir +
tangent * d.x +
bitangent * d.y);
// Do not shoot rays below the world horizon.
if (skyDir.z <= 0.02)
{
visibility += 0.0;
continue;
}
float sampleFacing = dot(N, skyDir);
// Avoid very noisy grazing rays on back-facing surfaces.
if (sampleFacing <= -0.35)
{
visibility += 0.0;
continue;
}
float3 skyOrigin =
worldPos +
N * normalBias +
skyDir * (gShadowBias * 2.0);
visibility += TraceShadow(skyOrigin, skyDir, SKY_TMAX);
}
visibility /= (float)SKY_SAMPLES;
// Slightly smooth the binary ray result so it does not look harsh.
return saturate(visibility);
}
float ComputeCavity(uint2 pixel, float3 worldPos, float3 N)
{
static const int2 taps[12] =
{
int2(-2, 0), int2( 2, 0),
int2( 0, -2), int2( 0, 2),
int2(-2, -2), int2( 2, -2),
int2(-2, 2), int2( 2, 2),
int2(-4, 0), int2( 4, 0),
int2( 0, -4), int2( 0, 4)
};
float accum = 0.0;
float weightSum = 0.0;
[unroll]
for (int i = 0; i < 12; ++i)
{
int2 sp = int2(pixel) + taps[i];
if (sp.x < 0 || sp.y < 0 || sp.x >= (int)gScreenSize.x || sp.y >= (int)gScreenSize.y)
continue;
float3 samplePos = gPositionTex.Load(int3(sp, 0)).xyz;
float3 sampleN = normalize(gNormalTex.Load(int3(sp, 0)).xyz);
float3 d = samplePos - worldPos;
float distSq = dot(d, d);
if (distSq > (24.0 * 24.0))
continue;
float nd = dot(N, sampleN);
if (nd < 0.65)
continue;
float curvature = 1.0 - saturate(nd);
float w = 1.0 / (1.0 + distSq * 0.02);
accum += curvature * w;
weightSum += w;
}
float cavity = (weightSum > 0.0) ? (accum / weightSum) : 0.0;
cavity = saturate(cavity * 2.0);
return 1.0 - cavity * 0.18;
}
float Doom3SpecularLookup(float x)
{
// Doom 3 used a lookup table for specular falloff. A single high-power
// lobe is too binary with this G-buffer path: small normal-map/grazing
// differences make some materials lose specular completely. Use a broad
// plastic lobe plus a tighter hot spot so highlights stay readable without
// turning into a flat additive wash.
x = saturate(x);
float broad = pow(x, 12.0);
float tight = pow(x, 48.0);
return saturate(broad * 0.55 + tight * 0.85);
}
float SpecularPeak3(float3 c)
{
return max(max(c.r, c.g), c.b);
}
bool LooksLikeAuthoredSpecularSample(float4 specSample, float3 baseAlbedo)
{
// Normal path: alpha is the raster G-buffer validity bit.
if (specSample.a > 0.5)
return true;
// Tolerant path: some raster paths/specular inputs write RGB but leave the
// validity alpha at zero. Do not treat the fallback albedo descriptor as a
// spec map; when no specular texture is bound, specSample.rgb == baseAlbedo.
float3 specRgb = saturate(specSample.rgb);
float3 baseRgb = saturate(baseAlbedo);
float rgbPeak = SpecularPeak3(specRgb);
float rgbDiff = length(specRgb - baseRgb);
return rgbPeak > 0.025 && rgbDiff > 0.035;
}
float EstimateSpecularNormalVariance(uint2 pixel, float3 centerN)
{
centerN = SafeNormalizeLocal(centerN, float3(0.0, 0.0, 1.0));
int2 p = int2(pixel);
int2 maxP = int2((int)gScreenSize.x - 1, (int)gScreenSize.y - 1);
int2 pxp = clamp(p + int2( 1, 0), int2(0, 0), maxP);
int2 pyp = clamp(p + int2( 0, 1), int2(0, 0), maxP);
float3 nR = SafeNormalizeLocal(gNormalTex.Load(int3(pxp, 0)).xyz, centerN);
float3 nD = SafeNormalizeLocal(gNormalTex.Load(int3(pyp, 0)).xyz, centerN);
float v =
(1.0 - saturate(dot(centerN, nR))) +
(1.0 - saturate(dot(centerN, nD)));
return saturate(v * 1.70);
}
float StabilizeSpecularSurfaceRoughness(uint2 pixel, float3 N, float surfaceRoughness)
{
float normalVariance = EstimateSpecularNormalVariance(pixel, N);
// Doom 3 normal maps can be extremely high contrast. Let clean painted metal
// and wet floors stay glossy, but force rougher lobes on noisy pixels so GGX
// does not turn texel-scale normal detail into broken sparkles.
float varianceFloor = lerp(0.12, 0.46, smoothstep(0.08, 0.42, normalVariance));
return clamp(max(surfaceRoughness, varianceFloor), 0.05, 1.0);
}
float3 Doom3SyntheticSpecularMask(uint2 pixel, float3 baseAlbedo)
{
// Missing Doom 3 specular maps used to get a bright Phong-era fallback. Build
// a small dielectric F0 instead, then damp it with normal-map roughness/cavity
// so cracks, noisy concrete, and damaged surfaces do not become glossy plastic.
float3 albedo = saturate(baseAlbedo);
float lum = dot(albedo, float3(0.299, 0.587, 0.114));
float albedoMax = max(max(albedo.r, albedo.g), albedo.b);
float albedoMin = min(min(albedo.r, albedo.g), albedo.b);
float saturation = (albedoMax > 1.0e-4) ? ((albedoMax - albedoMin) / albedoMax) : 0.0;
float3 centerN = SafeNormalizeLocal(gNormalTex.Load(int3(pixel, 0)).xyz, float3(0.0, 0.0, 1.0));
float normalVariance = EstimateSpecularNormalVariance(pixel, centerN);
float cleanDiffuse = lerp(0.62, 1.08, smoothstep(0.05, 0.62, lum));
cleanDiffuse *= lerp(1.0, 0.78, saturate(saturation * 1.25));
float cavityDamping = lerp(1.0, 0.48, smoothstep(0.03, 0.32, normalVariance));
float roughSurfaceDamping = lerp(1.0, 0.58, smoothstep(0.08, 0.45, normalVariance));
float f0 = 0.045 * cleanDiffuse * cavityDamping * roughSurfaceDamping;
f0 = clamp(f0, 0.012, 0.070);
float tintAmount = 0.10 * saturate(saturation);
float3 neutralSpec = float3(f0, f0, f0);
float3 tintedSpec = lerp(neutralSpec, albedo * f0, tintAmount);
return max(tintedSpec, float3(0.006, 0.006, 0.006));
}
float3 ShapeAuthoredDoom3Specular(uint2 pixel, float3 baseAlbedo, float3 authoredSpecular)
{
float3 authored = saturate(authoredSpecular);
float authoredPeak = SpecularPeak3(authored);
// Authored black still means "no specular" for Doom materials.
if (authoredPeak <= 0.002)
return 0.0;
float3 synthetic = Doom3SyntheticSpecularMask(pixel, baseAlbedo);
// Doom/idTech spec maps were painted for Phong, often as broad intensity masks.
// Treat them as strength/tint hints over a physical dielectric baseline instead
// of feeding their RGB straight into F0.
float authoredStrength = smoothstep(0.018, 0.68, authoredPeak);
float strengthScale = lerp(0.85, 4.65, authoredStrength);
float3 shaped = synthetic * strengthScale;
float3 authoredTint = authored / max(authoredPeak, 1.0e-4);
float tintTrust = saturate((authoredPeak - 0.05) * 0.85);
shaped = lerp(shaped, authoredTint * SpecularPeak3(shaped), tintTrust);
float peak = SpecularPeak3(shaped);
if (peak > 0.30 && peak > 1.0e-5)
shaped *= 0.30 / peak;
return max(shaped, float3(0.004, 0.004, 0.004));
}
float3 LoadSceneSpecularAlbedo(uint2 pixel, float3 baseAlbedo)
{
float4 specSample = gSpecularTex.Load(int3(pixel, 0));
// The raster G-buffer writer stores alpha as a validity bit. This matters
// for black specular maps: black should mean zero specular, not "missing map".
// Also accept RGB-only specular inputs when they are clearly not the fallback
// albedo descriptor, which fixes materials whose specular buffer forgot to
// set the alpha-valid bit.
if (LooksLikeAuthoredSpecularSample(specSample, baseAlbedo))
return ShapeAuthoredDoom3Specular(pixel, baseAlbedo, specSample.rgb);
return Doom3SyntheticSpecularMask(pixel, baseAlbedo);
}
float ComputeDiffuseLightingTerm(float3 N, float3 L)
{
// Keep the original light volume/range attenuation exactly where it is, but
// make the surface response less gamey. The old 0.28-0.32 Half-Lambert wrap
// pushed too much light around silhouettes and into back-facing normal-map
// detail. This smaller squared wrap keeps Doom/idTech readability while
// giving a more Lambert-like, physically plausible rolloff.
float rawNoL = dot(normalize(N), normalize(L));
if (gEnableHalfLambert != 0u)
{
const float REALISTIC_WRAP = 0.12;
float wrapped = saturate((rawNoL + REALISTIC_WRAP) / (1.0 + REALISTIC_WRAP));
return wrapped * wrapped;
}
return saturate(rawNoL);
}
float EstimateSpecularRoughness(float3 specularAlbedo, float surfaceRoughness)
{
// Doom 3 spec maps are art-directed intensity/tint masks, not roughness maps.
// Use the authored/material roughness packed by the raster path, with spec
// strength only nudging the lobe sharper for bright painted highlights.
float peak = SpecularPeak3(saturate(specularAlbedo));
float strength = smoothstep(0.010, 0.145, peak);
float specDrivenRoughness = lerp(0.68, 0.22, strength);
float materialRoughness = clamp(surfaceRoughness, 0.05, 1.0);
float roughness = lerp(specDrivenRoughness, min(specDrivenRoughness, materialRoughness), 0.72);
return clamp(roughness, 0.18, 0.86);
}
float3 LimitSpecularPeak(float3 c, float peakLimit)
{
c = max(c, 0.0);
float peak = SpecularPeak3(c);
if (peak > peakLimit && peak > 1.0e-5)
c *= peakLimit / peak;
return c;
}
float3 ComputeSpecularShaped(
float3 N,
float3 V,
float3 L,
float3 lightColor,
float lightIntensity,
float atten,
float shadow,
float3 specularAlbedo,
float roughnessBias,
float specularWrap,
float f0Blend,
float fresnelToWhiteScale,
float energyScale,
float peakLimit,
float surfaceRoughness)
{
if (gEnableSpecular == 0)
return 0.0;
if (atten <= 0.0 || shadow <= 0.0)
return 0.0;
N = normalize(N);
V = normalize(V);
L = normalize(L);
float rawNoL = dot(N, L);
float NoL = (specularWrap > 0.0)
? saturate((rawNoL + specularWrap) / (1.0 + specularWrap))
: saturate(rawNoL);
float NoV = saturate(dot(N, V));
if (NoL <= 1.0e-4 || NoV <= 1.0e-4)
return 0.0;
float3 H = Doom3SafeNormalizeOr(L + V, N);
float NoH = saturate(dot(N, H));
float VoH = saturate(dot(V, H));
if (NoH <= 1.0e-4 || VoH <= 1.0e-4)
return 0.0;
float3 specMask = saturate(specularAlbedo);
float specPeak = SpecularPeak3(specMask);
if (specPeak <= 0.001)
return 0.0;
float roughness = clamp(EstimateSpecularRoughness(specMask, surfaceRoughness) + roughnessBias, 0.18, 0.92);
float a = roughness * roughness;
float a2 = max(a * a, 1.0e-4);
const float PI = 3.14159265;
// GGX/Trowbridge-Reitz distribution with Smith masking and Schlick Fresnel.
// This is still intentionally stylized for the existing idTech-style assets,
// but it produces more believable view-dependent highlights than the previous
// additive lookup lobe and does not alter light attenuation distance.
float dDenom = NoH * NoH * (a2 - 1.0) + 1.0;
float D = a2 / max(PI * dDenom * dDenom, 1.0e-4);
float k = ((roughness + 1.0) * (roughness + 1.0)) * 0.125;
float Gv = NoV / max(NoV * (1.0 - k) + k, 1.0e-4);
float Gl = NoL / max(NoL * (1.0 - k) + k, 1.0e-4);
float G = Gv * Gl;
float3 F0 = saturate(lerp(float3(0.025, 0.025, 0.025), specMask, saturate(f0Blend)));
float3 F = F0 + (1.0 - F0) * (pow(1.0 - VoH, 5.0) * saturate(fresnelToWhiteScale));
float specTerm = (D * G) / max(4.0 * NoL * NoV, 1.0e-4);
specTerm = min(specTerm, 2.85);
const float SPECULAR_ENERGY_SCALE = 3.15;
float3 specular =
lightColor *
lightIntensity *
atten *
shadow *
NoL *
F *
specTerm *
(SPECULAR_ENERGY_SCALE * max(energyScale, 0.0));
float legacyLobe = Doom3SpecularLookup(NoH) * saturate(1.0 - roughness * 0.55);
specular +=
lightColor *
lightIntensity *
atten *
shadow *
NoL *
specMask *
legacyLobe *
(0.72 * max(energyScale, 0.0));
float clearcoatMask = smoothstep(0.075, 0.22, specPeak) * saturate((0.58 - roughness) / 0.42);
if (clearcoatMask > 0.0)
{
float coatRoughness = clamp(roughness * 0.55, 0.14, 0.36);
float coatA = coatRoughness * coatRoughness;
float coatA2 = max(coatA * coatA, 1.0e-4);
float coatDenom = NoH * NoH * (coatA2 - 1.0) + 1.0;
float coatD = coatA2 / max(PI * coatDenom * coatDenom, 1.0e-4);
float coatK = ((coatRoughness + 1.0) * (coatRoughness + 1.0)) * 0.125;
float coatGv = NoV / max(NoV * (1.0 - coatK) + coatK, 1.0e-4);
float coatGl = NoL / max(NoL * (1.0 - coatK) + coatK, 1.0e-4);
float coatF = 0.04 + 0.96 * pow(1.0 - VoH, 5.0);
float coatTerm = min((coatD * coatGv * coatGl) / max(4.0 * NoL * NoV, 1.0e-4), 2.75);
specular +=
lightColor *
lightIntensity *
atten *
shadow *
NoL *
coatF *
coatTerm *
(0.30 * clearcoatMask * max(energyScale, 0.0));
}
return LimitSpecularPeak(specular, peakLimit);
}
float3 ComputeSpecular(
float3 N,
float3 V,
float3 L,
float3 lightColor,
float lightIntensity,
float atten,
float shadow,
float3 specularAlbedo,
float surfaceRoughness)
{
return ComputeSpecularShaped(
N,
V,
L,
lightColor,
lightIntensity,
atten,
shadow,
specularAlbedo,
-0.04,
0.12,
0.82,
1.0,
1.70,
6.5,
surfaceRoughness);
}
float EstimateSkeletalSubsurfaceMask(float3 baseAlbedo, float3 specularAlbedo)
{
// Doom 3 characters have no authored SSS/thickness maps. Use a conservative
// skin/flesh proxy from the visible albedo and damp it on shiny/metal-like
// pieces so armor and gear do not glow like wax.
baseAlbedo = saturate(baseAlbedo);
float luma = dot(baseAlbedo, float3(0.299, 0.587, 0.114));
float warm = saturate((baseAlbedo.r - baseAlbedo.b * 0.55) * 2.35);
float redBias = saturate((baseAlbedo.r - max(baseAlbedo.g, baseAlbedo.b)) * 3.25 + 0.18);
float fleshRange = smoothstep(0.035, 0.18, luma) * (1.0 - smoothstep(0.82, 1.0, luma));
float matte = 1.0 - saturate(SpecularPeak3(specularAlbedo) * 0.85);
return saturate((0.08 + warm * 0.42 + redBias * 0.38) * fleshRange * lerp(0.55, 1.0, matte));
}
)"
R"(
float3 ComputeSkeletalSpecularAlbedo(float3 baseAlbedo, float3 specularAlbedo)
{
float subsurfaceMask = EstimateSkeletalSubsurfaceMask(baseAlbedo, specularAlbedo);
// Old character spec maps were authored for Doom 3's stylized interaction
// lights. In the path tracer they behave like wet plastic unless we make the
// BRDF much softer and cap the F0 on skin/flesh-like areas.
float3 spec = saturate(specularAlbedo);
float3 softSpec = spec * lerp(0.48, 0.24, subsurfaceMask);
float cap = lerp(0.16, 0.075, subsurfaceMask);
return min(softSpec, float3(cap, cap, cap));
}
)"
R"(
float3 ComputeSkeletalSpecular(
float3 N,
float3 V,
float3 L,
float3 baseAlbedo,
float3 lightColor,
float lightIntensity,
float atten,
float shadow,
float3 specularAlbedo,
float surfaceRoughness)
{
float subsurfaceMask = EstimateSkeletalSubsurfaceMask(baseAlbedo, specularAlbedo);
float3 characterSpec = ComputeSkeletalSpecularAlbedo(baseAlbedo, specularAlbedo);
// Character MD5 specular maps often encode art-directed shine, not material
// F0. Shape the whole BRDF, including grazing Fresnel, so faces and bodies
// keep a soft oily skin response instead of reflecting like sealed plastic.
return ComputeSpecularShaped(
N,
V,
L,
lightColor,
lightIntensity,
atten,
shadow,
characterSpec,
lerp(0.10, 0.20, subsurfaceMask),
0.0,
0.58,
lerp(0.32, 0.14, subsurfaceMask),
lerp(0.40, 0.24, subsurfaceMask),
lerp(0.55, 0.18, subsurfaceMask),
max(surfaceRoughness, 0.48));
}
float ComputeSubsurfaceShape(float3 N, float3 V, float3 L)
{
N = normalize(N);
V = normalize(V);
L = normalize(L);
float NoL = dot(N, L);
float wrap = saturate((NoL + 0.58) / 1.58);
wrap = wrap * wrap;
// Transmission becomes visible around the terminator and when the light is
// behind the viewed surface, which is the bit missing most painfully on MD5s.
float back = saturate((-NoL + 0.22) / 1.22);
float forward = pow(saturate(dot(-L, V)), 2.25);
float rim = pow(saturate(1.0 - abs(dot(N, V))), 1.60);
return saturate(wrap * 0.42 + back * (0.55 + forward * 0.85) + rim * wrap * 0.18);
}
)"
R"(
float3 ComputeSkeletalSubsurfaceRadiance(
float3 N,
float3 V,
float3 L,
float3 baseAlbedo,
float3 lightColor,
float lightIntensity,
float atten,
float shadow,
float subsurfaceMask)
{
if (subsurfaceMask <= 0.001 || atten <= 0.0 || lightIntensity <= 0.0)
return 0.0;
float shape = ComputeSubsurfaceShape(N, V, L);
if (shape <= 0.001)
return 0.0;
float softShadow = lerp(shadow, 1.0, 0.18 * subsurfaceMask);
float3 scatterTint = saturate(lerp(
float3(1.0, 0.30, 0.18),
saturate(baseAlbedo) * float3(1.22, 0.68, 0.52) + float3(0.10, 0.03, 0.02),
0.58));
const float SUBSURFACE_DIRECT_SCALE = 0.34;
return clamp(
lightColor *
lightIntensity *
atten *
softShadow *
scatterTint *
shape *
(subsurfaceMask * SUBSURFACE_DIRECT_SCALE),
0.0,
2.25);
}
)"
R"(
float TraceStraightUpToSky(float3 worldPos, float3 N)
{
const float SKY_TMAX = 1000000.0;
float3 skyDir = float3(0.0, 0.0, 1.0);
float NoSky = dot(N, skyDir);
float normalBias = lerp(gShadowBias * 4.0, gShadowBias * 1.0, saturate(NoSky));
float3 skyOrigin =
worldPos +
N * normalBias +
skyDir * (gShadowBias * 2.0);
return TraceShadow(skyOrigin, skyDir, SKY_TMAX);
}
uint PcgHash(uint input)
{
uint state = input * 747796405u + 2891336453u;
uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
return (word >> 22u) ^ word;
}
uint InitRng(uint2 pixel, uint frameIndex, uint sampleIndex)
{
uint seed = pixel.x * 1973u;
seed ^= pixel.y * 9277u;
seed ^= frameIndex * 26699u;
seed ^= sampleIndex * 374761393u;
return PcgHash(seed) | 1u;
}
uint InitSpatialRng(uint2 pixel, float3 worldPos, uint salt)
{
uint seed = pixel.x * 1973u;
seed ^= pixel.y * 9277u;
seed ^= asuint(worldPos.x * 0.03125) * 1597334677u;
seed ^= asuint(worldPos.y * 0.03125) * 3812015801u;
seed ^= asuint(worldPos.z * 0.03125) * 2798796415u;
seed ^= salt * 374761393u;
return PcgHash(seed) | 1u;
}
uint InitSurfaceStableRng(uint2 pixel, float3 worldPos, float3 normal, uint sampleIndex, uint frameIndex)
{
if (gExternalDenoiser == 0u)
return InitRng(pixel, frameIndex, sampleIndex);
uint3 cell = (uint3)floor(abs(worldPos) * 16.0 + 0.5);
uint3 ncell = (uint3)floor(saturate(normal * 0.5 + 0.5) * 255.0 + 0.5);
uint seed =
cell.x * 73856093u ^
cell.y * 19349663u ^
cell.z * 83492791u ^
ncell.x * 1597334677u ^
ncell.y * 3812015801u ^
ncell.z * 2798796415u ^
sampleIndex * 0x9E3779B9u;
return PcgHash(seed) | 1u;
}
float Rand(inout uint rng)
{
rng = PcgHash(rng);
return (float)rng * 2.3283064365386963e-10;
}
float2 Rand2(inout uint rng)
{
return float2(Rand(rng), Rand(rng));
}
float3 SampleCosineWorld(float3 N, inout uint rng)
{
float3 tangent, bitangent;
BuildOrthonormalBasis(N, tangent, bitangent);
float3 localDir = CosineSampleHemisphere(Rand2(rng));
return normalize(
tangent * localDir.x +
bitangent * localDir.y +
N * localDir.z);
}
float3 SampleCosineWorldXi(float3 N, float2 xi)
{
float3 tangent, bitangent;
BuildOrthonormalBasis(N, tangent, bitangent);
float3 localDir = CosineSampleHemisphere(xi);
return normalize(
tangent * localDir.x +
bitangent * localDir.y +
N * localDir.z);
}
float3 SampleConeWorld(float3 centerDir, float coneRadius, inout uint rng)
{
float3 tangent, bitangent;
BuildOrthonormalBasis(centerDir, tangent, bitangent);
float2 d = ConcentricSampleDisk(Rand2(rng)) * coneRadius;
return normalize(centerDir + tangent * d.x + bitangent * d.y);
}
float3 GetSkyRadiance(float3 dir)
{
float upness = saturate(dir.z * 0.5 + 0.5);
float3 warmSky = float3(0.98, 0.55, 0.35);
float3 coolSky = float3(0.30, 0.40, 0.62);
float3 sky = lerp(warmSky * 0.22, coolSky * 0.55, upness);
float sunAmount = pow(saturate(dot(dir, GetSkyLightDirection10AM())), 96.0);
sky += warmSky * (sunAmount * 2.25);
return sky;
}
float TraceVisibilityBiased(float3 worldPos, float3 N, float3 dir, float maxT)
{
float NoD = saturate(dot(N, dir));
float normalBias = lerp(gShadowBias * 3.0, gShadowBias * 0.75, NoD);
float3 origin = worldPos + N * normalBias + dir * (gShadowBias * 0.5);
return TraceShadow(origin, dir, max(maxT - gShadowBias * 0.5, 0.001));
}
float3 CompressEmissiveRadiance(float3 e, float peakLimit)
{
e = max(e, 0.0);
float peak = max(max(e.r, e.g), e.b);
if (peak > peakLimit && peak > 1e-5)
e *= peakLimit / peak;
return e;
}
float3 LimitStochasticRadiance(float3 c, float lumaLimit, float peakLimit)
{
c = max(c, 0.0);
float luma = dot(c, float3(0.2126, 0.7152, 0.0722));
if (luma > lumaLimit && luma > 1.0e-5)
c *= lumaLimit / luma;
float peak = max(max(c.r, c.g), c.b);
if (peak > peakLimit && peak > 1.0e-5)
c *= peakLimit / peak;
return c;
}
float3 LoadEmissiveRadianceClamped(int2 p)
{
int2 maxPixel = int2((int)gScreenSize.x - 1, (int)gScreenSize.y - 1);
p = clamp(p, int2(0, 0), maxPixel);
float3 radiance = CompressEmissiveRadiance(gEmissiveTex.Load(int3(p, 0)).rgb, 18.0);
return min(radiance + sqrt(max(radiance, 0.0)) * 0.35, 22.0);
}
float3 EstimateEmissiveBloomAtPixel(uint2 pixel)
{
int2 p = int2(pixel);
// Strong threshold-free bloom in the ray output. Particle emissive lighting
// is injected separately below; this is the visible camera halo.
float3 bloom = 0.0;
bloom += LoadEmissiveRadianceClamped(p) * 0.320;
bloom += LoadEmissiveRadianceClamped(p + int2( 1, 0)) * 0.420;
bloom += LoadEmissiveRadianceClamped(p + int2(-1, 0)) * 0.420;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 1)) * 0.420;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -1)) * 0.420;
bloom += LoadEmissiveRadianceClamped(p + int2( 2, 2)) * 0.260;
bloom += LoadEmissiveRadianceClamped(p + int2(-2, 2)) * 0.260;
bloom += LoadEmissiveRadianceClamped(p + int2( 2, -2)) * 0.260;
bloom += LoadEmissiveRadianceClamped(p + int2(-2, -2)) * 0.260;
bloom += LoadEmissiveRadianceClamped(p + int2( 4, 0)) * 0.180;
bloom += LoadEmissiveRadianceClamped(p + int2(-4, 0)) * 0.180;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 4)) * 0.180;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -4)) * 0.180;
bloom += LoadEmissiveRadianceClamped(p + int2( 8, 0)) * 0.120;
bloom += LoadEmissiveRadianceClamped(p + int2(-8, 0)) * 0.120;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 8)) * 0.120;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -8)) * 0.120;
bloom += LoadEmissiveRadianceClamped(p + int2( 14, 0)) * 0.075;
bloom += LoadEmissiveRadianceClamped(p + int2(-14, 0)) * 0.075;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 14)) * 0.075;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -14)) * 0.075;
bloom += LoadEmissiveRadianceClamped(p + int2( 24, 0)) * 0.045;
bloom += LoadEmissiveRadianceClamped(p + int2(-24, 0)) * 0.045;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 24)) * 0.045;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -24)) * 0.045;
bloom += LoadEmissiveRadianceClamped(p + int2( 42, 0)) * 0.025;
bloom += LoadEmissiveRadianceClamped(p + int2(-42, 0)) * 0.025;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 42)) * 0.025;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -42)) * 0.025;
return min(bloom * 1.45, 28.0);
}
float3 SafeNormalizeOr(float3 v, float3 fallback)
{
float lenSq = dot(v, v);
if (lenSq <= 1e-8)
return fallback;
return v * rsqrt(lenSq);
}
bool TryScreenParticleWorldCandidate(float4 clip, float3 receiverPos, inout float bestDistSq, inout float3 bestWorld)
{
float4 worldA = mul(clip, gInvViewProj);
if (abs(worldA.w) > 1e-6)
{
float3 candidate = worldA.xyz / worldA.w;
float distSq = dot(candidate - receiverPos, candidate - receiverPos);
if (distSq < bestDistSq && distSq > 1e-4 && distSq < 147456.0)
{
bestDistSq = distSq;
bestWorld = candidate;
}
}
float4 worldB = mul(gInvViewProj, clip);
if (abs(worldB.w) > 1e-6)
{
float3 candidate = worldB.xyz / worldB.w;
float distSq = dot(candidate - receiverPos, candidate - receiverPos);
if (distSq < bestDistSq && distSq > 1e-4 && distSq < 147456.0)
{
bestDistSq = distSq;
bestWorld = candidate;
}
}
return bestDistSq < 147456.0;
}
bool TryReconstructScreenParticleWorld(uint2 particlePixel, float particleDepth, float3 receiverPos, out float3 particleWorld)
{
float2 uv = ((float2)particlePixel + 0.5) / max(gScreenSize.xy, float2(1.0, 1.0));
float x = uv.x * 2.0 - 1.0;
float yUp = 1.0 - uv.y * 2.0;
float yDown = uv.y * 2.0 - 1.0;
float bestDistSq = 1.0e30;
particleWorld = receiverPos;
TryScreenParticleWorldCandidate(float4(x, yUp, particleDepth, 1.0), receiverPos, bestDistSq, particleWorld);
TryScreenParticleWorldCandidate(float4(x, yDown, particleDepth, 1.0), receiverPos, bestDistSq, particleWorld);
return bestDistSq < 147456.0;
}
float3 EstimateScreenSpaceEmissiveParticleLighting(uint2 pixel, float3 worldPos, float3 N)
{
static const int2 kParticleLightTaps[17] =
{
int2( 0, 0),
int2( 7, 0), int2( -7, 0), int2( 0, 7), int2( 0, -7),
int2( 10, 10), int2(-10, 10), int2( 10, -10), int2(-10, -10),
int2( 18, 4), int2(-18, -4), int2( 4, 18), int2( -4, -18),
int2( 30, 0), int2(-30, 0), int2( 0, 30), int2( 0, -30)
};
int2 maxPixel = int2((int)gScreenSize.x - 1, (int)gScreenSize.y - 1);
float pixelScale = max(1.0, round(min(gScreenSize.x, gScreenSize.y) / 720.0));
float3 lighting = 0.0;
[unroll]
for (uint i = 0u; i < 17u; ++i)
{
int2 sp = int2(pixel) + int2(round((float2)kParticleLightTaps[i] * pixelScale));
sp = clamp(sp, int2(0, 0), maxPixel);
float4 particleEmission = gEmissiveTex.Load(int3(sp, 0));
if (particleEmission.a <= 1.5)
continue;
float particleDepth = saturate(particleEmission.a - 2.0);
float3 particleWorld;
if (!TryReconstructScreenParticleWorld(uint2(sp), particleDepth, worldPos, particleWorld))
continue;
float3 toParticle = particleWorld - worldPos;
float distSq = dot(toParticle, toParticle);
if (distSq <= 1e-4 || distSq >= 147456.0)
continue;
float dist = sqrt(distSq);
float3 L = toParticle / dist;
float NoL = saturate(dot(N, L));
if (NoL <= 0.01)
continue;
float visibility = TraceVisibilityBiased(worldPos, N, L, dist);
if (visibility <= 0.0)
continue;
float distanceFade = saturate(1.0 - dist / 640.0);
distanceFade *= distanceFade;
float attenuation = distanceFade / (1.0 + distSq * 0.000075);
lighting += CompressEmissiveRadiance(particleEmission.rgb, 24.0) * (NoL * visibility * attenuation);
}
return lighting * 9.0;
}
bool ProjectClipToGBufferCandidate(
float4 clipPos,
bool flipY,
float3 rayHitPos,
inout float bestDistSq,
inout uint2 bestPixel,
inout float3 bestPos,
inout float3 bestNormal,
inout float3 bestAlbedo,
inout uint bestGeoFlag)
{
if (abs(clipPos.w) <= 1e-6)
return false;
float3 ndc = clipPos.xyz / clipPos.w;
if (ndc.x < -1.0 || ndc.x > 1.0 ||
ndc.y < -1.0 || ndc.y > 1.0 ||
ndc.z < 0.0 || ndc.z > 1.0)
{
return false;
}
float2 uv;
uv.x = ndc.x * 0.5 + 0.5;
uv.y = flipY ? (0.5 - ndc.y * 0.5) : (ndc.y * 0.5 + 0.5);
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)
return false;
int2 ip = int2(uv * gScreenSize.xy);
ip = clamp(ip, int2(0, 0), int2((int)gScreenSize.x - 1, (int)gScreenSize.y - 1));
float depth = gDepthTex.Load(int3(ip, 0));
if (depth <= 0.0 || depth >= 1.0)
return false;
float4 posSample = gPositionTex.Load(int3(ip, 0));
float3 gbufPos = posSample.xyz;
float3 delta = gbufPos - rayHitPos;
float distSq = dot(delta, delta);
if (distSq >= bestDistSq)
return false;
float4 normalSample = LoadSceneNormal(uint2(ip));
float3 gbufNormal = SafeNormalizeOr(normalSample.xyz, float3(0.0, 0.0, 1.0));
float3 gbufAlbedo = saturate(gAlbedoTex.Load(int3(ip, 0)).rgb);
bestDistSq = distSq;
bestPixel = uint2(ip);
bestPos = gbufPos;
bestNormal = gbufNormal;
bestAlbedo = gbufAlbedo;
bestGeoFlag = DecodeGeometryFlag(posSample.w);
return true;
}
bool TryFetchGBufferAtRayHit(
float3 rayHitPos,
out uint2 hitPixel,
out float3 hitPos,
out float3 hitNormal,
out float3 hitAlbedo,
out uint hitGeoFlag)
{
float bestDistSq = 1.0e30;
uint2 bestPixel = uint2(0, 0);
float3 bestPos = rayHitPos;
float3 bestNormal = float3(0.0, 0.0, 1.0);
float3 bestAlbedo = float3(0.5, 0.5, 0.5);
uint bestGeoFlag = GEOMETRY_FLAG_NONE;
// CPU computes gViewProj from the inverse view-projection matrix. Try both
// matrix-vector orders and both Y conventions so this remains tolerant of
// row/column-major engine uploads and render-target origin conventions.
float4 wpos = float4(rayHitPos, 1.0);
float4 clipA = mul(wpos, gViewProj);
float4 clipB = mul(gViewProj, wpos);
bool found = false;
found = ProjectClipToGBufferCandidate(clipA, true, rayHitPos, bestDistSq, bestPixel, bestPos, bestNormal, bestAlbedo, bestGeoFlag) || found;
found = ProjectClipToGBufferCandidate(clipA, false, rayHitPos, bestDistSq, bestPixel, bestPos, bestNormal, bestAlbedo, bestGeoFlag) || found;
found = ProjectClipToGBufferCandidate(clipB, true, rayHitPos, bestDistSq, bestPixel, bestPos, bestNormal, bestAlbedo, bestGeoFlag) || found;
found = ProjectClipToGBufferCandidate(clipB, false, rayHitPos, bestDistSq, bestPixel, bestPos, bestNormal, bestAlbedo, bestGeoFlag) || found;
if (!found)
{
hitPixel = uint2(0, 0);
hitPos = rayHitPos;
hitNormal = bestNormal;
hitAlbedo = bestAlbedo;
hitGeoFlag = GEOMETRY_FLAG_NONE;
return false;
}
// The TLAS hit is exact, but the material data comes from the camera G-buffer.
// Reject projections that land on an unrelated visible surface.
float viewDist = length(rayHitPos - gCameraPos.xyz);
float maxPositionError = max(24.0, viewDist * 0.035);
if (bestDistSq > maxPositionError * maxPositionError)
{
hitPixel = bestPixel;
hitPos = bestPos;
hitNormal = bestNormal;
hitAlbedo = bestAlbedo;
hitGeoFlag = bestGeoFlag;
return false;
}
hitPixel = bestPixel;
hitPos = bestPos;
hitNormal = bestNormal;
hitAlbedo = bestAlbedo;
hitGeoFlag = bestGeoFlag;
return true;
}
float3 EstimatePathTracedSky(float3 worldPos, float3 N, inout uint rng)
{
const float SKY_TMAX = 1000000.0;
uint bounceSamples = max(gMaxBounces, 1u);
bounceSamples = min(bounceSamples, 4u);
float3 accum = 0.0;
[loop]
for (uint b = 0; b < bounceSamples; ++b)
{
float3 dir = SampleCosineWorld(N, rng);
float NoD = saturate(dot(N, dir));
float visibility = TraceVisibilityBiased(worldPos, N, dir, SKY_TMAX);
// This is a G-buffer path-traced approximation: secondary hits are used as
// occluders because this pass does not bind per-triangle material data yet.
accum += GetSkyRadiance(dir) * visibility * NoD;
}
accum /= (float)bounceSamples;
return accum * 0.55;
}
)"
R"(
float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, float surfaceRoughness, bool isSkeletal, float subsurfaceMask, Light Lgt, inout uint rng, out float3 specularOut, out float3 subsurfaceOut)
{
specularOut = 0.0;
subsurfaceOut = 0.0;
float3 toCenter = Lgt.position - worldPos;
float centerDist = length(toCenter);
if (centerDist <= 0.01)
return 0.0;
float3 centerDir = toCenter / centerDist;
float3 tangent, bitangent;
BuildOrthonormalBasis(centerDir, tangent, bitangent);
float atten = ApplyIESProfile(ComputePointLightAttenuation(worldPos, Lgt), Lgt, worldPos - Lgt.position);
if (atten <= 0.0)
return 0.0;
uint sampleCount = max(Lgt.samples, 1u);
sampleCount = min(sampleCount, 4u);
// One random area-light sample per frame was one of the visible noise sources.
// Use a deterministic low-discrepancy pattern instead. With a single sample,
// use the light center so default point lights are hard-shadowed and stable.
float areaRadius = (Lgt.samples > 1u) ? max(GetPointLightMaxRadius(Lgt) * 0.03, 0.12) : 0.0;
float rand = Hash12((float2)pixel + worldPos.xy + float2(worldPos.z, centerDist));
float3 diffuseAccum = 0.0;
float3 specAccum = 0.0;
float3 sssAccum = 0.0;
[loop]
for (uint s = 0u; s < sampleCount; ++s)
{
float2 disk = float2(0.0, 0.0);
if (areaRadius > 0.0)
disk = ConcentricSampleDisk(Hammersley2D(s, sampleCount, rand)) * areaRadius;
float3 sampleLightPos = Lgt.position + tangent * disk.x + bitangent * disk.y;
float3 toLight = sampleLightPos - worldPos;
float dist = length(toLight);
if (dist <= 0.01)
continue;
float3 L = toLight / dist;
float diffuseNoL = ComputeDiffuseLightingTerm(N, L);
float shadow = 1.0;
if (Lgt.samples != 0u && (diffuseNoL > 0.0001 || subsurfaceMask > 0.001))
shadow = TraceVisibilityBiased(worldPos, N, L, dist);
specAccum += isSkeletal
? ComputeSkeletalSpecular(N, V, L, baseAlbedo, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo, surfaceRoughness)
: ComputeSpecular(N, V, L, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo, surfaceRoughness);
sssAccum += ComputeSkeletalSubsurfaceRadiance(N, V, L, baseAlbedo, Lgt.color, Lgt.intensity, atten, shadow, subsurfaceMask);
diffuseAccum += Lgt.color * (Lgt.intensity * atten * diffuseNoL * shadow);
}
float invSamples = 1.0 / (float)sampleCount;
specularOut = specAccum * invSamples;
subsurfaceOut = sssAccum * invSamples;
return diffuseAccum * invSamples;
}
)"
R"(
float3 PathTraceDirectSpotLight(float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, float surfaceRoughness, bool isSkeletal, float subsurfaceMask, Light Lgt, inout uint rng, out float3 specularOut, out float3 subsurfaceOut)
{
specularOut = 0.0;
subsurfaceOut = 0.0;
float3 toLight = Lgt.position - worldPos;
float dist = length(toLight);
if (dist <= 0.01)
return 0.0;
float3 L = toLight / dist;
float atten = ApplyIESProfile(ComputeSpotLightAttenuation(worldPos, Lgt), Lgt, worldPos - Lgt.position);
float diffuseNoL = ComputeDiffuseLightingTerm(N, L);
float shadow = 1.0;
if (Lgt.samples != 0u && (diffuseNoL > 0.0001 || subsurfaceMask > 0.001) && atten > 0.0)
shadow = TraceVisibilityBiased(worldPos, N, L, dist);
specularOut = isSkeletal
? ComputeSkeletalSpecular(N, V, L, baseAlbedo, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo, surfaceRoughness)
: ComputeSpecular(N, V, L, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo, surfaceRoughness);
subsurfaceOut = ComputeSkeletalSubsurfaceRadiance(N, V, L, baseAlbedo, Lgt.color, Lgt.intensity, atten, shadow, subsurfaceMask);
return Lgt.color * (Lgt.intensity * atten * diffuseNoL * shadow);
}
)"
R"(
float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, float surfaceRoughness, bool isSkeletal, float subsurfaceMask, Light Lgt, inout uint rng, out float3 specularOut, out float3 subsurfaceOut)
{
specularOut = 0.0;
subsurfaceOut = 0.0;
float3 toCenter = Lgt.position - worldPos;
float centerDist = length(toCenter);
if (centerDist <= 0.01)
return 0.0;
float attenRadius = max(Lgt.radius, 1e-4);
float atten = saturate((attenRadius - centerDist) / attenRadius);
atten = atten * atten * atten * atten;
if (atten <= 0.0)
return 0.0;
uint sampleCount = max(Lgt.samples, 1u);
sampleCount = min(sampleCount, 4u);
float rand = Hash12((float2)pixel + worldPos.xy + float2(centerDist, worldPos.z));
float3 diffuseAccum = 0.0;
float3 specAccum = 0.0;
float3 sssAccum = 0.0;
[loop]
for (uint s = 0u; s < sampleCount; ++s)
{
float2 uv = (sampleCount == 1u)
? float2(0.0, 0.0)
: (Hammersley2D(s, sampleCount, rand) * 2.0 - 1.0);
float3 sampleLightPos =
Lgt.position +
Lgt.axisU * (uv.x * Lgt.halfWidth) +
Lgt.axisV * (uv.y * Lgt.halfHeight);
float3 sampleVec = sampleLightPos - worldPos;
float sampleDist = length(sampleVec);
if (sampleDist <= 0.01)
continue;
float3 L = sampleVec / sampleDist;
float NdotL = ComputeDiffuseLightingTerm(N, L);
float sssShape = (subsurfaceMask > 0.001) ? ComputeSubsurfaceShape(N, V, L) : 0.0;
if (NdotL <= 0.0 && sssShape <= 0.001)
continue;
float faceTerm = (Lgt.twoSided != 0)
? abs(dot(-L, Lgt.normal))
: saturate(dot(-L, Lgt.normal));
if (faceTerm <= 0.0)
continue;
float shadow = 1.0;
if (Lgt.samples != 0u)
shadow = TraceVisibilityBiased(worldPos, N, L, sampleDist);
specAccum += (isSkeletal
? ComputeSkeletalSpecular(
N,
V,
L,
baseAlbedo,
Lgt.color,
Lgt.intensity * faceTerm,
1.0,
shadow,
specularAlbedo,
surfaceRoughness)
: ComputeSpecular(
N,
V,
L,
Lgt.color,
Lgt.intensity * faceTerm,
1.0,
shadow,
specularAlbedo,
surfaceRoughness)) * atten;
sssAccum += ComputeSkeletalSubsurfaceRadiance(
N,
V,
L,
baseAlbedo,
Lgt.color,
Lgt.intensity * faceTerm,
atten,
shadow,
subsurfaceMask);
diffuseAccum += clamp(Lgt.color * (Lgt.intensity * NdotL * faceTerm * atten * shadow), 0.0, 4.0);
}
float invSamples = 1.0 / (float)sampleCount;
specularOut = specAccum * invSamples;
subsurfaceOut = sssAccum * invSamples;
return diffuseAccum * invSamples;
}
float HenyeyGreensteinPhase(float cosTheta, float g)
{
g = clamp(g, -0.85, 0.85);
float g2 = g * g;
float denom = max(1.0 + g2 - 2.0 * g * cosTheta, 1e-3);
return (1.0 - g2) / max(4.0 * 3.14159265 * pow(denom, 1.5), 1e-3);
}
float ComputeLightVolumeAttenuation(float3 samplePos, Light Lgt)
{
if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_POINT)
return ComputePointLightAttenuation(samplePos, Lgt);
if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT)
return ComputeSpotLightAttenuation(samplePos, Lgt);
return 0.0;
}
float EstimateVolumeDensityFromLight(Light Lgt)
{
// Doom 3 world units are large. Tie the default participating-medium density
// to light range so the caller only needs one artist-facing attribute.
float range = (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_POINT)
? GetPointLightMaxRadius(Lgt)
: max(Lgt.radius, 1.0);
return clamp(2.25 / max(range, 32.0), 0.0015, 0.035);
}
)"
R"(
float3 EstimateSingleLightVolumetricScattering(uint2 pixel, float3 cameraPos, float3 worldPos, Light Lgt, inout uint rng)
{
if (Lgt.volumetricScattering <= 0.0)
return 0.0;
if (Lgt.type != GL_RAYTRACING_LIGHT_TYPE_POINT && Lgt.type != GL_RAYTRACING_LIGHT_TYPE_SPOT)
return 0.0;
float3 cameraToSurface = worldPos - cameraPos;
float viewDist = length(cameraToSurface);
if (viewDist <= 0.01)
return 0.0;
float3 viewDir = cameraToSurface / viewDist;
uint stepCount = min(max(Lgt.samples, 3u), 6u);
float stepLen = viewDist / (float)stepCount;
// Do not frame-jitter the march. This renderer has no temporal GI history,
// so varying the volume sample positions every frame creates visible sparkle.
// A centered deterministic slice is stable and the spatial denoiser can smooth it.
float jitter = 0.5;
float density = EstimateVolumeDensityFromLight(Lgt);
float anisotropy = (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT) ? 0.55 : 0.35;
float3 accum = 0.0;
[loop]
for (uint s = 0u; s < stepCount; ++s)
{
float t = ((float)s + jitter) * stepLen;
t = min(t, viewDist - 0.001);
float3 samplePos = cameraPos + viewDir * t;
float atten = ComputeLightVolumeAttenuation(samplePos, Lgt);
if (atten <= 0.0)
continue;
float3 toLight = Lgt.position - samplePos;
float lightDist = length(toLight);
if (lightDist <= 0.01)
continue;
float3 L = toLight / lightDist;
float3 shadowOrigin = samplePos + L * (gShadowBias * 0.75) + viewDir * (gShadowBias * 0.15);
float visibility = TraceShadow(shadowOrigin, L, max(lightDist - gShadowBias, 0.001));
if (visibility <= 0.0)
continue;
float phase = HenyeyGreensteinPhase(dot(L, -viewDir), anisotropy);
float transmittance = exp(-density * t);
float slice = density * stepLen;
accum += Lgt.color * (Lgt.intensity * atten * visibility * phase * transmittance * slice);
}
// Scale from normalized phase-function energy into a game-facing glow term.
// The user-facing light attribute still controls the final strength.
const float DOOM3_VOLUME_SCALE = 7.5;
return clamp(accum * max(Lgt.volumetricScattering, 0.0) * DOOM3_VOLUME_SCALE, 0.0, 12.0);
}
float3 EstimatePathTracedVolumetricScattering(uint2 pixel, float3 worldPos, inout uint rng)
{
float3 volume = 0.0;
[loop]
for (uint i = 0; i < gLightCount; ++i)
{
Light Lgt = gLights[i];
if (Lgt.volumetricScattering <= 0.0)
continue;
if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_POINT || Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT)
volume += EstimateSingleLightVolumetricScattering(pixel, gCameraPos.xyz, worldPos, Lgt, rng);
}
return volume;
}
)"
R"(
float3 EstimateFastBounceLight(float3 hitPos, float3 hitN, Light Lgt)
{
// Cheap unshadowed estimate used as the all-lights baseline for secondary
// GI. A small shadowed subset below corrects this baseline so every light
// still bounces, but important occlusion is no longer missing from GI.
if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_POINT)
{
float3 toLight = Lgt.position - hitPos;
float dist = length(toLight);
if (dist <= 0.01)
return 0.0;
float3 L = toLight / dist;
float atten = ComputePointLightAttenuation(hitPos, Lgt);
if (atten <= 0.0)
return 0.0;
float nDotL = ComputeDiffuseLightingTerm(hitN, L);
return clamp(Lgt.color * (Lgt.intensity * atten * nDotL), 0.0, 8.0);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT)
{
float3 toLight = Lgt.position - hitPos;
float dist = length(toLight);
if (dist <= 0.01)
return 0.0;
float3 L = toLight / dist;
float atten = ComputeSpotLightAttenuation(hitPos, Lgt);
if (atten <= 0.0)
return 0.0;
float nDotL = ComputeDiffuseLightingTerm(hitN, L);
return clamp(Lgt.color * (Lgt.intensity * atten * nDotL), 0.0, 8.0);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_RECT)
{
float3 toCenter = Lgt.position - hitPos;
float centerDist = length(toCenter);
if (centerDist <= 0.01)
return 0.0;
float attenRadius = max(Lgt.radius, 1e-4);
float atten = saturate((attenRadius - centerDist) / attenRadius);
atten = atten * atten;
if (atten <= 0.0)
return 0.0;
float3 L = toCenter / centerDist;
float nDotL = ComputeDiffuseLightingTerm(hitN, L);
if (nDotL <= 0.0)
return 0.0;
float faceTerm = (Lgt.twoSided != 0)
? abs(dot(-L, Lgt.normal))
: saturate(dot(-L, Lgt.normal));
if (faceTerm <= 0.0)
return 0.0;
return clamp(Lgt.color * (Lgt.intensity * nDotL * faceTerm * atten), 0.0, 8.0);
}
return 0.0;
}
bool SecondaryLightMatchesArea(Light Lgt, int hitAreaNum)
{
// Unknown areas stay permissive so dynamic entities, particles, and legacy
// proc files do not disappear from GI. Known world/static hits prefer lights
// tagged to the same portal area, which reduces room-to-room bounce leaks.
return hitAreaNum < 0 || Lgt.areaNum < 0 || Lgt.areaNum == hitAreaNum;
}
uint CountSecondaryBounceAreaLights(int hitAreaNum)
{
uint count = 0u;
[loop]
for (uint i = 0u; i < gLightCount; ++i)
{
if (SecondaryLightMatchesArea(gLights[i], hitAreaNum))
++count;
}
return count;
}
uint GetSecondaryBounceFastLightBudget(uint candidateCount)
{
if (candidateCount == 0u)
return 0u;
if (gSecondaryLightBudget < 0)
return candidateCount;
if (gSecondaryLightBudget > 0)
return min(candidateCount, (uint)gSecondaryLightBudget);
if (candidateCount > 16u)
return 8u;
if (candidateCount > 10u)
return 7u;
if (candidateCount > 6u)
return 6u;
return candidateCount;
}
uint GetSecondaryBounceCorrectionBudget(uint candidateCount)
{
if (candidateCount == 0u)
return 0u;
if (gSecondaryCorrectionBudget > 0u)
return min(candidateCount, gSecondaryCorrectionBudget);
uint correctionBudget = (candidateCount <= 2u) ? 1u : 0u;
if (gSamplesPerPixel >= 4u)
correctionBudget = min(candidateCount, max(correctionBudget, 1u));
if (gSamplesPerPixel >= 6u && candidateCount <= 4u)
correctionBudget = min(candidateCount, correctionBudget + 1u);
return correctionBudget;
}
float GetSecondaryBounceFastLightScale(uint fastBudget, uint candidateCount)
{
if (fastBudget == 0u || fastBudget >= candidateCount)
return 1.0;
// The uploaded light list is CPU-sorted by broad importance. Use a small
// energy compensation for skipped low-importance lights without pretending
// the top few lights represent the entire room perfectly.
float fullScale = (float)candidateCount / (float)fastBudget;
return lerp(1.0, fullScale, saturate(gSecondaryLightCompensation));
}
float3 EstimateUnresolvedBounceRadiance(float3 hitPos, float3 hitN, float3 albedo, int hitAreaNum)
{
hitN = SafeNormalizeOr(hitN, float3(0.0, 0.0, 1.0));
albedo = max(saturate(albedo), float3(0.03, 0.03, 0.03));
float upness = saturate(hitN.z * 0.5 + 0.5);
float3 lighting = gAmbientColor.rgb * (gAmbientColor.a * 0.025);
lighting += GetSkyRadiance(hitN) * (0.035 + 0.035 * upness);
float3 fastAllLights = 0.0;
uint candidateCount = CountSecondaryBounceAreaLights(hitAreaNum);
uint fastBudget = GetSecondaryBounceFastLightBudget(candidateCount);
uint accepted = 0u;
[loop]
for (uint i = 0; i < gLightCount; ++i)
{
if (accepted >= fastBudget)
break;
if (!SecondaryLightMatchesArea(gLights[i], hitAreaNum))
continue;
fastAllLights += EstimateFastBounceLight(hitPos, hitN, gLights[i]);
++accepted;
}
lighting += fastAllLights * (0.85 * GetSecondaryBounceFastLightScale(accepted, candidateCount));
return clamp(albedo * max(lighting, 0.0), 0.0, 8.0);
}
float3 EstimateShadowedBounceLight(uint2 hitPixel, float3 hitPos, float3 hitN, float3 hitV, float3 hitAlbedo, bool hitIsSkeletal, Light Lgt, inout uint rng)
{
float3 spec = 0.0;
float3 sss = 0.0;
float3 diffuse = 0.0;
float3 hitSpecularAlbedo = LoadSceneSpecularAlbedo(hitPixel, hitAlbedo);
float hitSurfaceRoughness = StabilizeSpecularSurfaceRoughness(hitPixel, hitN, LoadSceneSurfaceRoughness(LoadSceneNormal(hitPixel)));
float subsurfaceMask = hitIsSkeletal ? EstimateSkeletalSubsurfaceMask(hitAlbedo, hitSpecularAlbedo) * 0.45 : 0.0;
// Use the same visibility-capable direct-light samplers as the primary hit.
// This supplies proper next-event estimation at secondary hits instead of the
// old unoccluded light-list approximation.
if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_POINT)
{
diffuse = PathTraceDirectPointLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, hitSurfaceRoughness, hitIsSkeletal, subsurfaceMask, Lgt, rng, spec, sss);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT)
{
diffuse = PathTraceDirectSpotLight(hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, hitSurfaceRoughness, hitIsSkeletal, subsurfaceMask, Lgt, rng, spec, sss);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_RECT)
{
diffuse = PathTraceDirectRectLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, hitSurfaceRoughness, hitIsSkeletal, subsurfaceMask, Lgt, rng, spec, sss);
}
return clamp(diffuse + sss * 0.35, 0.0, 12.0);
}
struct LightReservoir
{
uint lightIndex;
float weightSum;
float selectedWeight;
uint candidateCount;
};
float EstimateBounceLightReservoirWeight(float3 hitPos, float3 hitN, Light Lgt)
{
float3 fast = EstimateFastBounceLight(hitPos, hitN, Lgt);
float lum = dot(max(fast, 0.0), float3(0.2126, 0.7152, 0.0722));
float peak = max(max(fast.r, fast.g), fast.b);
return max(lum + peak * 0.35, 1.0e-5);
}
void UpdateLightReservoir(inout LightReservoir reservoir, uint lightIndex, float weight, inout uint rng)
{
if (weight <= 0.0)
return;
reservoir.weightSum += weight;
reservoir.candidateCount += 1u;
if (Rand(rng) * reservoir.weightSum <= weight)
{
reservoir.lightIndex = lightIndex;
reservoir.selectedWeight = weight;
}
}
LightReservoir BuildBounceLightReservoir(float3 hitPos, float3 hitN, int hitAreaNum, uint seed, inout uint rng)
{
LightReservoir reservoir;
reservoir.lightIndex = 0u;
reservoir.weightSum = 0.0;
reservoir.selectedWeight = 0.0;
reservoir.candidateCount = 0u;
uint localRng = PcgHash(rng ^ seed ^ 0xA511E9B3u);
[loop]
for (uint i = 0u; i < gLightCount; ++i)
{
Light Lgt = gLights[i];
if (!SecondaryLightMatchesArea(Lgt, hitAreaNum))
continue;
float weight = EstimateBounceLightReservoirWeight(hitPos, hitN, Lgt);
UpdateLightReservoir(reservoir, i, weight, localRng);
}
rng = PcgHash(localRng + reservoir.lightIndex + reservoir.candidateCount * 17u);
return reservoir;
}
float3 EstimateReservoirShadowCorrection(
uint2 hitPixel,
float3 hitPos,
float3 hitN,
float3 hitV,
float3 hitAlbedo,
bool hitIsSkeletal,
int hitAreaNum,
float fastLightScale,
uint fastBudget,
uint sampleIndex,
inout uint rng)
{
LightReservoir reservoir = BuildBounceLightReservoir(
hitPos,
hitN,
hitAreaNum,
PcgHash(sampleIndex * 0x9E3779B9u + 0xD1B54A35u),
rng);
if (reservoir.candidateCount == 0u || reservoir.selectedWeight <= 0.0)
return 0.0;
Light selected = gLights[reservoir.lightIndex];
float3 fast = 0.0;
uint fastRank = 0u;
[loop]
for (uint rankIndex = 0u; rankIndex <= reservoir.lightIndex && rankIndex < gLightCount; ++rankIndex)
{
if (SecondaryLightMatchesArea(gLights[rankIndex], hitAreaNum))
++fastRank;
}
if (fastRank > 0u && fastRank <= fastBudget)
fast = EstimateFastBounceLight(hitPos, hitN, selected) * fastLightScale;
float3 shadowed = EstimateShadowedBounceLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, hitIsSkeletal, selected, rng);
// Convert the selected reservoir light into a gentle many-light correction.
// The fast baseline still carries the full cheap estimate; the reservoir only
// spends visibility rays on the lights most likely to matter.
float selectionPdf = reservoir.selectedWeight / max(reservoir.weightSum, 1.0e-5);
float correctionScale = min(1.0 / max(selectionPdf, 1.0e-3), 2.0);
return (shadowed - fast) * correctionScale;
}
float3 EstimateBounceSkyLighting(float3 hitPos, float3 hitN, inout uint rng)
{
const float SKY_TMAX = 1000000.0;
uint skySamples = (gSamplesPerPixel >= 4u) ? 2u : 1u;
float3 accum = 0.0;
[loop]
for (uint i = 0u; i < skySamples; ++i)
{
float3 skyDir = (i == 0u)
? SampleConeWorld(GetSkyLightDirection10AM(), 0.18, rng)
: SampleCosineWorld(hitN, rng);
float NoSky = saturate(dot(hitN, skyDir));
if (NoSky <= 0.0)
continue;
float visibility = TraceVisibilityBiased(hitPos, hitN, skyDir, SKY_TMAX);
accum += GetSkyRadiance(skyDir) * visibility * NoSky;
}
return accum / (float)skySamples;
}
float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3 hitN, float3 hitV, float3 hitAlbedo, uint hitGeoFlag, int hitAreaNum, inout uint rng)
{
bool hitIsSkeletal = (hitGeoFlag & GEOMETRY_FLAG_SKELETAL) != 0u;
bool hitIsUnlit = (hitGeoFlag & GEOMETRY_FLAG_UNLIT) != 0u;
uint lightingRng = InitSpatialRng(hitPixel, hitPos, 0xB04157u);
hitN = SafeNormalizeOr(hitN, float3(0.0, 0.0, 1.0));
if (hitIsSkeletal)
hitN = StabilizeSkeletalNormal(hitPixel, hitPos, hitN);
hitV = SafeNormalizeOr(hitV, -hitN);
hitAlbedo = saturate(hitAlbedo);
// Treat unlit G-buffer surfaces as simple bounce cards. Glow-map emissive is
// intentionally excluded here so visible emissive no longer casts GI/light.
if (hitIsUnlit)
return clamp(hitAlbedo * 2.0 + GetSkyRadiance(hitN) * 0.04, 0.0, 6.0);
float upness = saturate(hitN.z * 0.5 + 0.5);
float3 lighting = gAmbientColor.rgb * (gAmbientColor.a * 0.035);
// Real occluded sky contribution at the secondary hit. The previous GI path
// used a fixed sky term, so corners/cavities received too much indirect light.
lighting += EstimateBounceSkyLighting(hitPos, hitN, lightingRng) * (0.30 + 0.18 * upness);
// Baseline: secondary GI uses a capped important-light set. Primary direct
// lighting still evaluates every light, but bounce hits were multiplying the
// full light list by every stochastic path depth.
float3 fastAllLights = 0.0;
uint candidateCount = CountSecondaryBounceAreaLights(hitAreaNum);
uint fastBudget = GetSecondaryBounceFastLightBudget(candidateCount);
uint accepted = 0u;
[loop]
for (uint i = 0; i < gLightCount; ++i)
{
if (accepted >= fastBudget)
break;
if (!SecondaryLightMatchesArea(gLights[i], hitAreaNum))
continue;
fastAllLights += EstimateFastBounceLight(hitPos, hitN, gLights[i]);
++accepted;
}
float fastLightScale = GetSecondaryBounceFastLightScale(accepted, candidateCount);
lighting += fastAllLights * fastLightScale;
// Visibility correction: replace a small stable subset of the unshadowed
// baseline with real shadowed direct lighting. Keying this to the hit
// position avoids frame-to-frame sparkle while keeping the all-lights bounce.
uint correctionBudget = GetSecondaryBounceCorrectionBudget(candidateCount);
if (correctionBudget > 0u)
{
float3 reservoirCorrection = 0.0;
[loop]
for (uint c = 0u; c < correctionBudget; ++c)
{
reservoirCorrection += EstimateReservoirShadowCorrection(
hitPixel,
hitPos,
hitN,
hitV,
hitAlbedo,
hitIsSkeletal,
hitAreaNum,
fastLightScale,
fastBudget,
c,
lightingRng);
}
lighting += reservoirCorrection / (float)correctionBudget;
}
//if (hitIsSkeletal)
// lighting *= 1.10;
// Outgoing diffuse radiance from the bounce surface. The primary surface's
// albedo is applied later in RayGen, so only the secondary hit albedo belongs
// here.
return clamp(hitAlbedo * max(lighting, 0.0), 0.0, 16.0);
}
)"
R"(
float3 EstimateReactiveScreenSpaceFinalGather(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, inout uint rng)
{
// Current-frame final gather / irradiance reuse. The gather samples nearby
// G-buffer surfaces, shades them as bounce emitters, and now traces short
// visibility rays so color bleed does not leak through walls.
static const int2 kGatherTaps[6] =
{
int2( 7, 3),
int2( -9, 6),
int2( 5, -13),
int2( 17, -8),
int2(-22, -15),
int2( 29, 18)
};
uint sampleBudget = 3u;
// Keep the pass cheap when many lights or high SPP are active. This path
// evaluates bounce lighting against the light list and traces short
// visibility rays, so adapt the sample count instead of raising ray count.
if (gLightCount > 4u)
sampleBudget = 2u;
if (gLightCount > 10u)
sampleBudget = 1u;
if (gSamplesPerPixel >= 4u)
sampleBudget = min(sampleBudget, 2u);
if (gSamplesPerPixel >= 6u)
sampleBudget = min(sampleBudget, 1u);
int2 maxPixel = int2((int)gScreenSize.x - 1, (int)gScreenSize.y - 1);
float pixelScale = max(1.0, round(min(gScreenSize.x, gScreenSize.y) / 720.0));
// Doom/idTech-like unit scale: large enough to catch wall/floor color bleed,
// small enough to avoid room-to-room light leaking from unrelated screen hits.
const float MAX_GATHER_DISTANCE = 224.0;
float2 jitter = float2(
Hash12((float2)pixel + float2(13.7, 91.1)),
Hash12((float2)pixel + float2(47.3, 19.9))) - 0.5;
float3 accum = 0.0;
float weightSum = 0.0;
[loop]
for (uint i = 0u; i < 6u; ++i)
{
if (i >= sampleBudget)
break;
float2 tap = float2(kGatherTaps[i].x, kGatherTaps[i].y) * pixelScale;
int2 sp = int2(pixel) + int2(
(int)round(tap.x + jitter.x * pixelScale * 2.0),
(int)round(tap.y + jitter.y * pixelScale * 2.0));
sp = clamp(sp, int2(0, 0), maxPixel);
float sampleDepth = gDepthTex.Load(int3(sp, 0));
if (sampleDepth <= 0.0 || sampleDepth >= 1.0)
continue;
float4 samplePos4 = gPositionTex.Load(int3(sp, 0));
float3 samplePos = samplePos4.xyz;
uint sampleGeoFlag = DecodeGeometryFlag(samplePos4.w);
float3 sampleAlbedo = saturate(gAlbedoTex.Load(int3(sp, 0)).rgb);
float3 sampleNormal = SafeNormalizeOr(gNormalTex.Load(int3(sp, 0)).xyz, N);
float3 delta = samplePos - worldPos;
float distSq = dot(delta, delta);
if (distSq <= 1e-4)
continue;
float dist = sqrt(distSq);
if (dist >= MAX_GATHER_DISTANCE)
continue;
float3 dirToSample = delta / dist;
float receiverFacing = saturate(dot(N, dirToSample));
if (receiverFacing <= 0.02)
continue;
bool sampleIsUnlit = (sampleGeoFlag & GEOMETRY_FLAG_UNLIT) != 0u;
float emitterFacing = sampleIsUnlit ? 1.0 : saturate(dot(sampleNormal, -dirToSample));
if (emitterFacing <= 0.02)
continue;
float distanceFade = saturate(1.0 - dist / MAX_GATHER_DISTANCE);
distanceFade *= distanceFade;
float distanceWeight = 1.0 / (1.0 + distSq * 0.00018);
// Favor concave/near-facing exchange and suppress unrelated background
// samples that happen to be close in screen-space but far in world-space.
float normalAffinity = saturate(dot(N, sampleNormal) * 0.35 + 0.65);
float formWeight = receiverFacing * emitterFacing * distanceFade * distanceWeight * normalAffinity;
if (formWeight <= 1e-5)
continue;
// Prevent screen-space final gather from leaking through walls. This is
// a short visibility ray, not another diffuse bounce, and it fixes the
// most obvious missing GI occlusion cases in doorways/corners.
float visibility = TraceVisibilityBiased(worldPos, N, dirToSample, dist);
if (visibility <= 0.0)
continue;
formWeight *= visibility;
float3 sampleView = SafeNormalizeOr(-dirToSample, V);
float3 outgoingRadiance = EstimateDirectLightingForBounceHit(
uint2(sp),
samplePos,
sampleNormal,
sampleView,
sampleAlbedo,
sampleGeoFlag,
-1,
rng);
accum += outgoingRadiance * formWeight;
weightSum += formWeight;
}
if (weightSum <= 1e-5)
return 0.0;
float3 gatheredRadiance = accum / weightSum;
// Coverage keeps one bright tap from flooding a pixel while still letting
// nearby high-confidence samples respond strongly to dynamic lights.
float coverage = saturate(weightSum * 2.15);
const float FINAL_GATHER_STRENGTH = 1.05;
return gatheredRadiance * coverage * FINAL_GATHER_STRENGTH;
}
)"
R"(
float2 GetStratifiedBounceXi(uint2 pixel, float3 worldPos, float3 pathNormal, uint sampleIndex)
{
uint spp = max(gSamplesPerPixel, 1u);
uint sequenceLength = max(spp * 64u, 64u);
uint rngFrameIndex = (gEnableDenoiser != 0u && gExternalDenoiser == 0u) ? gFrameIndex : 0u;
uint sequenceIndex = (sampleIndex + (rngFrameIndex & 63u) * spp) % sequenceLength;
float rand = Hash12(
(float2)pixel +
worldPos.xy * 0.013 +
float2(pathNormal.x * 19.19, pathNormal.y * 37.37));
return Hammersley2D(sequenceIndex, sequenceLength, rand);
}
float3 TraceOneIndirectBouncePath(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, uint sampleIndex, inout uint rng)
{
const float BOUNCE_TMAX = 1000000.0;
uint maxIndirectDepth = (gMaxBounces > 1u) ? min(gMaxBounces - 1u, 3u) : 0u;
if (gSamplesPerPixel <= 1u)
maxIndirectDepth = min(maxIndirectDepth, 2u);
float3 accum = 0.0;
float3 throughput = 1.0;
float3 pathPos = worldPos;
float3 pathNormal = N;
float3 pathView = V;
uint2 pathPixel = pixel;
[loop]
for (uint depth = 0; depth < maxIndirectDepth; ++depth)
{
float2 bounceXi = (depth == 0u)
? GetStratifiedBounceXi(pixel, pathPos, pathNormal, sampleIndex)
: Rand2(rng);
float3 bounceDir = SampleCosineWorldXi(pathNormal, bounceXi);
float NoD = saturate(dot(pathNormal, bounceDir));
float normalBias = lerp(gShadowBias * 3.0, gShadowBias * 0.75, NoD);
float3 bounceOrigin =
pathPos +
pathNormal * normalBias +
bounceDir * (gShadowBias * 0.5);
float hitT = 0.0;
uint materialFlags = 0;
uint meshIndex = 0;
float3 rayHitNormal = 0.0;
float2 rayHitTexCoord = 0.0;
bool hit = TraceBounce(bounceOrigin, bounceDir, BOUNCE_TMAX, hitT, materialFlags, meshIndex, rayHitNormal, rayHitTexCoord);
if (!hit)
{
// Environment miss. This is part of the path throughput and is now
// allowed at every diffuse depth, not only at the first miss.
float missScale = (depth == 0u) ? 0.28 : 0.42;
accum += throughput * GetSkyRadiance(bounceDir) * missScale;
break;
}
float3 rayHitPos = bounceOrigin + bounceDir * hitT;
uint2 hitPixel = pathPixel;
float3 hitPos = rayHitPos;
float3 hitNormal = SafeNormalizeOr(rayHitNormal, SafeNormalizeOr(-bounceDir, pathNormal));
MeshMetadata meshMeta = GetMeshMetadataSafe(meshIndex);
float3 meshAverageColor = GetMeshAverageColor(meshIndex);
float3 hitAlbedo = meshAverageColor;
uint hitGeoFlag = GEOMETRY_FLAG_NONE;
int hitAreaNum = meshMeta.areaNum;
float3 materialNormal = hitNormal;
bool hasGBufferMaterial = TryFetchGBufferAtRayHit(
rayHitPos,
hitPixel,
hitPos,
hitNormal,
hitAlbedo,
hitGeoFlag);
hitNormal = materialNormal;
hitGeoFlag |= MeshMetadataGeometryFlags(meshMeta, materialFlags);
// If the G-buffer normal points away from the incoming bounce ray, flip it
// so direct lighting at the secondary hit is evaluated on the side that
// the ray actually reached.
if (dot(hitNormal, -bounceDir) < 0.0)
hitNormal = -hitNormal;
float3 hitV = SafeNormalizeOr(-bounceDir, pathView);
float3 bouncedRadiance = 0.0;
if (hasGBufferMaterial)
{
bouncedRadiance = EstimateDirectLightingForBounceHit(
hitPixel,
hitPos,
hitNormal,
hitV,
hitAlbedo,
hitGeoFlag,
hitAreaNum,
rng);
bouncedRadiance *= 1.28;
}
else
{
// The ray hit real TLAS geometry, but material lookup through the
// camera G-buffer failed or landed on an unrelated visible pixel.
// Use mesh-derived color instead of neutral grey/screen material.
hitPos = rayHitPos;
hitAlbedo = meshAverageColor;
bouncedRadiance = EstimateUnresolvedBounceRadiance(hitPos, hitNormal, hitAlbedo, hitAreaNum);
}
bouncedRadiance += GetMeshEmissiveRadiance(meshMeta);
bouncedRadiance = CompressEmissiveRadiance(bouncedRadiance, depth == 0u ? 6.0 : 3.5);
bouncedRadiance = LimitStochasticRadiance(
bouncedRadiance,
depth == 0u ? 2.20 : 1.35,
depth == 0u ? 3.80 : 2.20);
accum += throughput * bouncedRadiance;
if ((hitGeoFlag & GEOMETRY_FLAG_UNLIT) != 0u)
break;
// Cosine-weighted diffuse sampling cancels the Lambertian cosine/pdf term;
// the remaining throughput is just the secondary surface albedo. Use a
// conservative energy scale because material data for off-screen hits is a
// G-buffer approximation.
throughput *= saturate(hitAlbedo) * 0.84;
float continuation = clamp(max(max(throughput.x, throughput.y), throughput.z), 0.12, 0.92);
if (depth >= 1u)
{
if (Rand(rng) > continuation)
break;
throughput /= continuation;
}
if (max(max(throughput.x, throughput.y), throughput.z) < 0.015)
break;
pathPos = hitPos;
pathNormal = hitNormal;
pathView = hitV;
pathPixel = hitPixel;
}
return accum;
}
float3 EstimatePathTracedIndirectBounce(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, uint sampleIndex, inout uint rng)
{
if (gMaxBounces <= 1u)
return 0.0;
// One stochastic diffuse path per lighting sample. The path can contain
// several diffuse depths, while the temporal pass below handles convergence.
float3 accum = TraceOneIndirectBouncePath(pixel, worldPos, N, V, baseAlbedo, sampleIndex, rng);
const float INDIRECT_STRENGTH = 0.95;
return LimitStochasticRadiance(accum * INDIRECT_STRENGTH, 2.50, 4.00);
}
float3 ApplyPrimaryDiffusePost(float3 lightingAccum, float ao, float microShadow, bool isSkeletal)
{
if (isSkeletal)
{
ao = lerp(ao, 1.0, 0.35);
microShadow = lerp(microShadow, 1.0, 0.65);
}
lightingAccum *= ao;
lightingAccum *= microShadow;
//if (isSkeletal)
// lightingAccum *= 1.2;
return max(lightingAccum, 0.0);
}
float3 ApplyPrimarySpecularPost(float3 specularAccum, float ao, bool isSkeletal)
{
// AO is a diffuse/ambient visibility term here. Multiplying specular by AO
// directly made highlights disappear on creases, props, and normal-mapped
// surfaces. Keep a mild occlusion tint, but let direct-light specular read.
float specAo = lerp(0.76, 1.0, saturate(ao));
if (isSkeletal)
{
specAo = lerp(specAo, 1.0, 0.50);
specularAccum *= 0.42;
}
specularAccum *= specAo;
//if (isSkeletal)
// specularAccum *= 1.15;
return max(specularAccum, 0.0);
}
float3 PathTraceDeterministicLighting(
uint2 pixel,
float3 worldPos,
float3 N,
float3 V,
float3 baseAlbedo,
float3 specularAlbedo,
float surfaceRoughness,
bool isSkeletal,
float cavity,
float ao,
float skyVis,
float ambientSkyVis,
out float3 specularAccum,
out float3 subsurfaceAccum)
{
specularAccum = 0.0;
subsurfaceAccum = 0.0;
float microShadow = lerp(0.75, 1.0, cavity);
// These environment and direct-light terms are deterministic for a given
// pixel. The old RayGen evaluated them once for every SPP, which multiplied
// the AO/sky/direct shadow ray budget without adding new samples.
float upness = saturate(N.z * 0.5 + 0.5);
float3 skyColorRGB = float3(0.98, 0.55, 0.35);
float3 skyColor = skyColorRGB * (0.35 + 0.65 * upness);
float3 lightingAccum = float3(0, 0, 0); // gAmbientColor.rgb * (gAmbientColor.a * 0.04);
lightingAccum += skyColor * (0.42 * skyVis);
lightingAccum += ambientSkyVis * (skyColorRGB * 0.12);
// Glow-map emissive is direct/bloom-only for now. It is added after lighting
// in RayGen and is intentionally not injected into lightingAccum.
//if (isSkeletal)
// lightingAccum += 0.1;
// The current direct-light evaluators are deterministic. Keep the inout RNG
// argument only because the functions share the same signature as stochastic
// helpers; it is not consumed by PathTraceDirect* in the current shader.
uint directRng = InitRng(pixel, 0u, 0xD17EC7u);
float subsurfaceMask = isSkeletal ? EstimateSkeletalSubsurfaceMask(baseAlbedo, specularAlbedo) : 0.0;
[loop]
for (uint i = 0; i < gLightCount; ++i)
{
Light Lgt = gLights[i];
float3 spec = 0.0;
float3 sss = 0.0;
float3 diffuse = 0.0;
if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_POINT)
{
diffuse = PathTraceDirectPointLight(pixel, worldPos, N, V, baseAlbedo, specularAlbedo, surfaceRoughness, isSkeletal, subsurfaceMask, Lgt, directRng, spec, sss);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT)
{
diffuse = PathTraceDirectSpotLight(worldPos, N, V, baseAlbedo, specularAlbedo, surfaceRoughness, isSkeletal, subsurfaceMask, Lgt, directRng, spec, sss);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_RECT)
{
diffuse = PathTraceDirectRectLight(pixel, worldPos, N, V, baseAlbedo, specularAlbedo, surfaceRoughness, isSkeletal, subsurfaceMask, Lgt, directRng, spec, sss);
}
lightingAccum += diffuse;
specularAccum += spec;
subsurfaceAccum += sss;
}
specularAccum = ApplyPrimarySpecularPost(specularAccum, ao, isSkeletal);
if (isSkeletal)
subsurfaceAccum *= lerp(0.72, 1.0, saturate(ao));
return ApplyPrimaryDiffusePost(lightingAccum, ao, microShadow, isSkeletal);
}
uint GetSpecularReflectionFastLightBudget()
{
if (gLightCount > 12u)
return 3u;
if (gLightCount > 6u)
return 2u;
return min(gLightCount, 2u);
}
float3 EstimateSpecularReflectionFastHitRadiance(float3 hitPos, float3 hitNormal, float3 incomingViewDir, float3 hitAlbedo, int hitAreaNum)
{
// Reflection rays can hit off-screen or camera-hidden TLAS geometry. In that
// case this pass has no per-triangle material/normal table, so use a neutral
// lit card instead of returning black. This keeps reflected objects visible
// without pretending every unknown hit is a perfect emissive surface.
incomingViewDir = SafeNormalizeOr(incomingViewDir, -hitNormal);
hitNormal = SafeNormalizeOr(hitNormal, incomingViewDir);
if (dot(hitNormal, incomingViewDir) < 0.0)
hitNormal = -hitNormal;
float upness = saturate(hitNormal.z * 0.5 + 0.5);
float3 lighting = gAmbientColor.rgb * (gAmbientColor.a * 0.035);
lighting += GetSkyRadiance(hitNormal) * (0.12 + 0.08 * upness);
lighting += GetSkyRadiance(SafeNormalizeOr(reflect(-incomingViewDir, hitNormal), hitNormal)) * 0.055;
uint lightBudget = GetSpecularReflectionFastLightBudget();
uint accepted = 0u;
[loop]
for (uint i = 0; i < gLightCount; ++i)
{
if (accepted >= lightBudget)
break;
Light Lgt = gLights[i];
if (!SecondaryLightMatchesArea(Lgt, hitAreaNum))
continue;
lighting += EstimateFastBounceLight(hitPos, hitNormal, Lgt) * 0.72;
++accepted;
}
return clamp(max(saturate(hitAlbedo), float3(0.08, 0.08, 0.08)) * max(lighting, 0.0), 0.0, 8.0);
}
float3 EstimateFallbackReflectionHitRadiance(float3 hitPos, float3 hitNormal, float3 incomingViewDir)
{
const float3 NEUTRAL_UNKNOWN_ALBEDO = float3(0.58, 0.58, 0.58);
return EstimateSpecularReflectionFastHitRadiance(hitPos, hitNormal, incomingViewDir, NEUTRAL_UNKNOWN_ALBEDO, -1);
}
void ComputeSpecularReflectionLocalInfluence(float3 worldPos, float3 N, out float energy, out float maxDistance)
{
energy = 0.0;
maxDistance = 0.0;
uint lightBudget = GetSpecularReflectionFastLightBudget();
[loop]
for (uint i = 0; i < lightBudget; ++i)
{
Light Lgt = gLights[i];
float atten = 0.0;
float range = 0.0;
if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_POINT)
{
atten = ComputePointLightAttenuation(worldPos, Lgt);
range = GetPointLightMaxRadius(Lgt);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT)
{
atten = ComputeSpotLightAttenuation(worldPos, Lgt);
range = max(Lgt.radius, 1.0);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_RECT)
{
float dist = length(Lgt.position - worldPos);
range = max(Lgt.radius, 1.0);
atten = saturate((range - dist) / range);
atten = atten * atten;
}
if (atten <= 0.0)
continue;
float3 toLight = SafeNormalizeOr(Lgt.position - worldPos, N);
float facing = saturate((dot(N, toLight) + 0.18) / 1.18);
float lightPeak = max(max(Lgt.color.r, Lgt.color.g), Lgt.color.b);
energy += atten * facing * Lgt.intensity * lightPeak;
maxDistance = max(maxDistance, range * lerp(0.18, 0.55, saturate(atten)));
}
// Keep reflections tied to nearby light contribution instead of becoming
// a global mirror pass.
energy = saturate(energy * 0.12);
maxDistance = clamp(maxDistance, 24.0, 768.0);
}
float ComputeSpecularReflectionDistanceFade(float hitT, float maxT)
{
float t = saturate(hitT / max(maxT, 1.0));
// Strong near reflection, smooth fade before the end of the local volume.
float fade = 1.0 - smoothstep(0.35, 1.0, t);
return fade * fade;
}
)"
R"(
float3 EstimateRayTracedSpecularReflection(
uint2 pixel,
float3 worldPos,
float3 N,
float3 V,
float3 baseAlbedo,
float3 specularAlbedo,
float surfaceRoughness,
float cavity,
bool isSkeletal,
inout uint rng)
{
if (gEnableSpecular == 0u || gMaxBounces <= 1u)
return 0.0;
// Character materials should get soft direct specular, not mirror-like local
// reflections. The G-buffer material model has no skeletal roughness map, so
// reflected TLAS hits make skin/armor read much wetter than intended.
if (isSkeletal)
return 0.0;
N = SafeNormalizeOr(N, float3(0.0, 0.0, 1.0));
V = SafeNormalizeOr(V, -N);
float normalVariance = EstimateSpecularNormalVariance(pixel, N);
// Prefer authored specular maps, but let missing maps use the synthesized
// dielectric fallback. The visibility threshold below keeps fallback rays
// rare unless the surface is grazing and locally lit.
float4 rawSpecular = gSpecularTex.Load(int3(pixel, 0));
bool hasSpecularMap = LooksLikeAuthoredSpecularSample(rawSpecular, baseAlbedo);
float specPeak = SpecularPeak3(specularAlbedo);
if (specPeak <= (hasSpecularMap ? 0.035 : 0.012))
return 0.0;
float NoV = saturate(dot(N, V));
float3 R = SafeNormalizeOr(reflect(-V, N), N);
float NoR = saturate(dot(N, R));
if (NoR <= 0.001)
return 0.0;
// Schlick fresnel keeps reflections strongest at grazing angles while still
// honoring the artist-authored Doom/idTech-style specular map color. Missing
// spec maps use a much lower F0 so the fallback is glossy, not mirror-like.
float3 F0 = hasSpecularMap
? saturate(specularAlbedo)
: saturate(lerp(float3(0.02, 0.02, 0.02), specularAlbedo, 0.38));
float3 fresnel = F0 + (1.0 - F0) * pow(1.0 - NoV, 5.0);
float roughness = EstimateSpecularRoughness(specularAlbedo, surfaceRoughness);
roughness = clamp(lerp(roughness, 0.78, smoothstep(0.12, 0.46, normalVariance)), 0.16, 0.84);
// Do not multiply by specPeak again here. specularAlbedo is already F0, and
// double-penalizing it made normal dielectric floors fall below the ray gate.
float reflectionVisibility = SpecularPeak3(fresnel) * saturate(1.0 - roughness * 0.72);
float visibilityThreshold = hasSpecularMap ? 0.035 : 0.030;
if (reflectionVisibility <= visibilityThreshold)
return 0.0;
float localReflectionEnergy = 0.0;
float reflectionMaxT = 0.0;
ComputeSpecularReflectionLocalInfluence(worldPos, N, localReflectionEnergy, reflectionMaxT);
if (localReflectionEnergy <= 0.001)
return 0.0;
float cavityFade = lerp(0.42, 1.0, saturate(cavity));
float reflectionStrength = hasSpecularMap
? saturate(specPeak * 1.35)
: saturate(specPeak * 4.0);
float reflectionScale = hasSpecularMap ? 0.72 : 0.34;
if (reflectionStrength <= 0.001)
return 0.0;
float rayVisibilityThreshold = hasSpecularMap ? 0.13 : 0.22;
if (reflectionVisibility < rayVisibilityThreshold)
{
float confidence = saturate(reflectionVisibility / max(rayVisibilityThreshold, 1.0e-4));
float3 stableGlossRadiance =
GetSkyRadiance(R) * 0.065 +
max(baseAlbedo, float3(0.05, 0.05, 0.05)) * (0.06 + localReflectionEnergy * 0.42);
return clamp(
stableGlossRadiance *
fresnel *
reflectionStrength *
cavityFade *
reflectionScale *
localReflectionEnergy *
lerp(0.35, 0.82, confidence),
0.0,
0.45);
}
float normalBias = lerp(gShadowBias * 3.0, gShadowBias * 0.75, NoR);
float3 reflectionOrigin = worldPos + N * normalBias + R * (gShadowBias * 0.5);
float hitT = 0.0;
uint materialFlags = 0u;
uint meshIndex = 0u;
float3 rayHitNormal = 0.0;
float2 rayHitTexCoord = 0.0;
bool hit = TraceSpecularReflection(reflectionOrigin, R, reflectionMaxT, hitT, materialFlags, meshIndex, rayHitNormal, rayHitTexCoord);
float3 reflectedRadiance = 0.0;
float distanceFade = 1.0;
float3 reflectedSurfaceAverageColor = 1.0;
if (!hit)
{
// Do not reflect sky forever. A missed finite reflection ray contributes
// only a tiny local glossy sheen.
reflectedRadiance = GetSkyRadiance(R) * 0.055;
distanceFade = 0.28;
}
else
{
distanceFade = ComputeSpecularReflectionDistanceFade(hitT, reflectionMaxT);
float3 rayHitPos = reflectionOrigin + R * hitT;
MeshMetadata reflectedMeshMeta = GetMeshMetadataSafe(meshIndex);
reflectedSurfaceAverageColor = GetMeshAverageColor(meshIndex);
int reflectedAreaNum = reflectedMeshMeta.areaNum;
float3 hitPos = rayHitPos;
float3 hitNormal = SafeNormalizeOr(rayHitNormal, SafeNormalizeOr(-R, N));
float3 hitAlbedo = reflectedSurfaceAverageColor;
if (dot(hitNormal, -R) < 0.0)
hitNormal = -hitNormal;
reflectedRadiance = EstimateSpecularReflectionFastHitRadiance(
hitPos,
hitNormal,
SafeNormalizeOr(-R, V),
hitAlbedo,
reflectedAreaNum);
reflectedRadiance += GetMeshEmissiveRadiance(reflectedMeshMeta);
}
reflectedRadiance *= reflectedSurfaceAverageColor;
reflectedRadiance = CompressEmissiveRadiance(reflectedRadiance, hasSpecularMap ? 2.4 : 1.6);
float3 reflectionResult =
reflectedRadiance *
fresnel *
reflectionStrength *
cavityFade *
reflectionScale *
localReflectionEnergy *
distanceFade;
return LimitStochasticRadiance(reflectionResult, hasSpecularMap ? 0.72 : 0.38, hasSpecularMap ? 1.15 : 0.62);
}
)"
R"(
float3 ApplyRealisticOutputCurve(float3 color)
{
color.r = ((color.r == color.r) && abs(color.r) < 65504.0) ? color.r : 0.0;
color.g = ((color.g == color.g) && abs(color.g) < 65504.0) ? color.g : 0.0;
color.b = ((color.b == color.b) && abs(color.b) < 65504.0) ? color.b : 0.0;
// Final photographic shoulder only: it does not change light radius or
// attenuation, but it prevents intense local lights/specular/bloom from
// clipping into a flat white patch. Values below 1.0 are left untouched.
color = max(color, 0.0);
float peak = max(max(color.r, color.g), color.b);
if (peak > 1.0)
{
float over = peak - 1.0;
float shoulderPeak = 1.0 + over / (1.0 + over * 0.38);
color *= shoulderPeak / max(peak, 1.0e-5);
}
return max(color, 0.0);
}
float3 PreparePathTraceOutputColor(float3 color)
{
color.r = ((color.r == color.r) && abs(color.r) < 65504.0) ? color.r : 0.0;
color.g = ((color.g == color.g) && abs(color.g) < 65504.0) ? color.g : 0.0;
color.b = ((color.b == color.b) && abs(color.b) < 65504.0) ? color.b : 0.0;
// External/hardware denoisers such as DLSS Ray Reconstruction need the noisy
// HDR lighting/GI signal. The display shoulder is applied after reconstruction.
if (gEnableDenoiser == 0u)
return max(color, 0.0);
return ApplyRealisticOutputCurve(color);
}
float PreparePathTraceHitDistance(float3 worldPos)
{
float d = length(gCameraPos.xyz - worldPos);
return ((d == d) && d >= 0.0 && d < 65504.0) ? d : 0.0;
}
[shader("raygeneration")]
void RayGen()
{
uint2 pixel = DispatchRaysIndex().xy;
if (pixel.x >= (uint)gScreenSize.x || pixel.y >= (uint)gScreenSize.y)
return;
float4 albedoSample = gAlbedoTex.Load(int3(pixel, 0));
float4 emissiveSample = gEmissiveTex.Load(int3(pixel, 0));
float3 emissiveSurface = CompressEmissiveRadiance(emissiveSample.rgb, 6.50);
float depthSample = gDepthTex.Load(int3(pixel, 0));
float3 emissiveBloom = EstimateEmissiveBloomAtPixel(pixel);
if (depthSample <= 0.0 || depthSample >= 1.0)
{
gOutputTex[pixel] = float4(PreparePathTraceOutputColor(albedoSample.rgb + emissiveSurface + emissiveBloom), 0.0);
return;
}
float3 baseAlbedo = albedoSample.rgb;
float3 specularAlbedo = LoadSceneSpecularAlbedo(pixel, baseAlbedo);
float4 positionSample = gPositionTex.Load(int3(pixel, 0));
float3 worldPos = positionSample.xyz;
float primaryHitDistance = PreparePathTraceHitDistance(worldPos);
uint geoFlag = DecodeGeometryFlag(positionSample.w);
bool isSkeletal = (geoFlag & GEOMETRY_FLAG_SKELETAL) != 0u;
bool isUnlit = (geoFlag & GEOMETRY_FLAG_UNLIT) != 0u;
float4 normalSample = LoadSceneNormal(pixel);
float surfaceRoughness = LoadSceneSurfaceRoughness(normalSample);
float3 N = EnhanceBumpNormal(pixel, worldPos, baseAlbedo, isSkeletal);
if (isSkeletal)
N = StabilizeSkeletalNormal(pixel, worldPos, N);
surfaceRoughness = StabilizeSpecularSurfaceRoughness(pixel, N, surfaceRoughness);
float3 V = normalize(gCameraPos.xyz - worldPos);
if (isUnlit)
{
gOutputTex[pixel] = float4(PreparePathTraceOutputColor(baseAlbedo + emissiveSurface + emissiveBloom), primaryHitDistance);
return;
}
uint spp = max(gSamplesPerPixel, 1u);
spp = min(spp, 8u);
// All four of these are deterministic for this pixel. Compute them once,
// then reuse them for every stochastic GI sample.
const bool externalCharacterPixel = (gExternalDenoiser != 0u) && isSkeletal;
float cavity = externalCharacterPixel ? 1.0 : ComputeCavity(pixel, worldPos, N);
if (isSkeletal)
cavity = lerp(cavity, 1.0, 0.65);
float ao = externalCharacterPixel ? 1.0 : ComputeAmbientOcclusion(worldPos, N, pixel, isSkeletal);
float skyVis = 0; //ComputeSkyVisibility(worldPos, N, pixel);
float ambientSkyVis = 0; //TraceStraightUpToSky(worldPos, N);
float microShadow = lerp(0.75, 1.0, cavity);
ApplyWorldGrime(worldPos, cavity, ao, isSkeletal, baseAlbedo, specularAlbedo);
float3 reactiveFinalGather = 0.0;
uint rngFrameIndex = (gEnableDenoiser != 0u && gExternalDenoiser == 0u) ? gFrameIndex : 0u;
if (gMaxBounces > 1u)
{
// Evaluate this deterministic screen-space irradiance reuse once per
// pixel, not once per SPP. That makes the GI more responsive without
// multiplying the light-list work inside the stochastic sample loop.
uint gatherRng = InitSurfaceStableRng(pixel, worldPos, N, 1337u, rngFrameIndex);
reactiveFinalGather = EstimateReactiveScreenSpaceFinalGather(
pixel,
worldPos,
N,
V,
baseAlbedo,
gatherRng);
}
float3 specularAccum = 0.0;
float3 subsurfaceAccum = 0.0;
float3 lightingAccum = PathTraceDeterministicLighting(
pixel,
worldPos,
N,
V,
baseAlbedo,
specularAlbedo,
surfaceRoughness,
isSkeletal,
cavity,
ao,
skyVis,
ambientSkyVis,
specularAccum,
subsurfaceAccum);
lightingAccum += EstimateScreenSpaceEmissiveParticleLighting(pixel, worldPos, N);
// Only the indirect bounce path uses per-SPP randomness now. Direct lights,
// AO, sky visibility, cavity, and final gather are deterministic and should
// not be re-traced spp times.
if (gMaxBounces > 1u && !externalCharacterPixel)
{
float3 indirectAccum = 0.0;
[loop]
for (uint s = 0; s < spp; ++s)
{
// Frame-vary only when the internal temporal accumulator is active.
// External denoisers get a surface-stable raw GI pattern so texture
// detail does not boil under reconstruction.
uint rng = InitSurfaceStableRng(pixel, worldPos, N, s, rngFrameIndex);
indirectAccum += EstimatePathTracedIndirectBounce(
pixel,
worldPos,
N,
V,
baseAlbedo,
s,
rng);
}
float3 indirectLighting = indirectAccum / (float)spp;
lightingAccum += ApplyPrimaryDiffusePost(indirectLighting, ao, microShadow, isSkeletal) * gGiAmount;
}
uint reflectionRng = InitRng(pixel, 0u, 0x5EECu);
float3 reflectedSpecular = EstimateRayTracedSpecularReflection(
pixel,
worldPos,
N,
V,
baseAlbedo,
specularAlbedo,
surfaceRoughness,
cavity,
isSkeletal,
reflectionRng);
float3 albedo = baseAlbedo * cavity;
float3 finalColor = (albedo * (lightingAccum)) + specularAccum + reflectedSpecular;
if (gMaxBounces > 1u)
{
// reactiveFinalGather is incoming indirect radiance. Apply the primary
// diffuse albedo here, matching the regular lighting path.
finalColor += baseAlbedo * reactiveFinalGather * gGiAmount;
}
finalColor += subsurfaceAccum;
// Volumetric light scattering is radiance in the camera ray, not surface
// reflectance, so add it after surface albedo/specular composition. Because
// it is written into the same path-trace target, the internal a-trous pass
// denoises the stochastic volume/GI signal together with the rest of the ray result.
uint volumeRng = InitRng(pixel, 0u, 0x51u);
finalColor += EstimatePathTracedVolumetricScattering(pixel, worldPos, volumeRng);
// Glow-map emission is direct radiance from the primary surface. It is added
// after lighting so it remains visible in darkness and under ray-traced shadows.
// emissiveBloom is the threshold-free halo, so emissive always blooms even if
// the eventual swap-chain/backbuffer is LDR.
finalColor += emissiveSurface + emissiveBloom;
gOutputTex[pixel] = float4(PreparePathTraceOutputColor(finalColor), primaryHitDistance);
}
)";
static const char* g_glRaytracingDenoiseHlsl = R"(
cbuffer LightingCB : register(b0)
{
float4x4 gInvViewProj;
float4x4 gInvViewMatrix;
float4x4 gViewProj;
float4 gCameraPos;
float4 gAmbientColor;
float4 gScreenSize;
float gNormalReconstructZ;
uint gLightCount;
uint gEnableSpecular;
uint gEnableHalfLambert;
float gShadowBias;
uint gFrameIndex;
uint gSamplesPerPixel;
uint gMaxBounces;
uint gEnableDenoiser;
uint gDenoisePassIndex;
float gDenoiseStepWidth;
float gDenoiseStrength;
float gDenoisePhiColor;
float gDenoisePhiNormal;
float gDenoisePhiPosition;
float gBumpStrength;
int gSecondaryLightBudget;
uint gSecondaryCorrectionBudget;
float gSecondaryLightCompensation;
uint gHistoryReset;
uint gExternalDenoiser;
float gGiAmount;
float gWorldGrimeAmount;
uint gPadding2;
};
Texture2D<float4> gAlbedoTex : register(t1);
Texture2D<float> gDepthTex : register(t2);
Texture2D<float4> gNormalTex : register(t3);
Texture2D<float4> gPositionTex : register(t4);
Texture2D<float4> gPathTraceTex : register(t6);
Texture2D<float4> gDenoiseATex : register(t7);
Texture2D<float4> gDenoiseBTex : register(t8);
Texture2D<float4> gTemporalTex : register(t10);
RWTexture2D<float4> gRayOutputTex : register(u0);
RWTexture2D<float4> gDenoiseAOutTex : register(u1);
RWTexture2D<float4> gDenoiseBOutTex : register(u2);
RWTexture2D<float4> gDenoisedOutputTex : register(u3);
static const float kKernel[5] = { 0.0625, 0.25, 0.375, 0.25, 0.0625 };
float3 SafeNormal(float3 n)
{
float lenSq = max(dot(n, n), 1e-8);
return n * rsqrt(lenSq);
}
float Luminance(float3 c)
{
return dot(c, float3(0.2126, 0.7152, 0.0722));
}
static const uint GEOMETRY_FLAG_SKELETAL = 1u;
static const uint GEOMETRY_FLAG_UNLIT = 2u;
static const uint GEOMETRY_FLAG_GLASS = 4u;
uint DecodeGeometryFlag(float geoFlag)
{
return (uint)floor(max(geoFlag, 0.0) + 0.5);
}
float3 SafeAlbedoDivisor(float3 albedo)
{
// Do not let black/dark textures explode when demodulating noisy lighting.
return max(abs(albedo), float3(0.06, 0.06, 0.06));
}
float3 DemodulateLighting(float3 radiance, float3 albedo)
{
return radiance / SafeAlbedoDivisor(albedo);
}
float3 RemodulateLighting(float3 lighting, float3 albedo)
{
return lighting * SafeAlbedoDivisor(albedo);
}
float4 LoadDenoiseSource(int2 p)
{
if (gDenoisePassIndex == 0u)
return gTemporalTex.Load(int3(p, 0));
if (gDenoisePassIndex == 1u)
return gDenoiseATex.Load(int3(p, 0));
return gDenoiseBTex.Load(int3(p, 0));
}
void StoreDenoiseOutput(uint2 p, float4 v)
{
if (gDenoisePassIndex == 0u)
gDenoiseAOutTex[p] = v;
else if (gDenoisePassIndex == 1u)
gDenoiseBOutTex[p] = v;
else
gDenoisedOutputTex[p] = v;
}
float GeometryAwareWeight(
float3 centerRadiance,
float3 sampleRadiance,
float3 centerAlbedo,
float3 sampleAlbedo,
float3 centerNormal,
float3 sampleNormal,
float3 centerPos,
float3 samplePos,
float centerDepth,
float sampleDepth,
uint centerGeoFlag,
uint sampleGeoFlag,
float kernelWeight)
{
if (sampleDepth <= 0.0 || sampleDepth >= 1.0)
return 0.0;
// Do not smear lighting across material-class boundaries. This is
// particularly important for skeletal pixels and glass, because both can sit
// in front of unrelated world lighting in the same screen neighborhood.
const uint classMask = GEOMETRY_FLAG_SKELETAL | GEOMETRY_FLAG_UNLIT | GEOMETRY_FLAG_GLASS;
if (((centerGeoFlag ^ sampleGeoFlag) & classMask) != 0u)
return 0.0;
float3 centerLighting = DemodulateLighting(centerRadiance, centerAlbedo);
float3 sampleLighting = DemodulateLighting(sampleRadiance, sampleAlbedo);
// Use albedo, not noisy lit radiance, as the main color edge guide. The
// previous filter used the shadowed/noisy signal itself as the guide, which
// rejected neighbors across shadow variation and left shadow noise intact.
float albedoDiff = length(centerAlbedo - sampleAlbedo);
float albedoWeight = exp(-albedoDiff * max(gDenoisePhiColor, 0.001));
// A deliberately soft lighting-domain gate keeps hard contact-shadow edges
// from being over-blurred, but still lets noisy penumbra/visibility samples
// converge across the same surface.
float centerLum = Luminance(centerLighting);
float sampleLum = Luminance(sampleLighting);
float illumDiff = abs(sampleLum - centerLum);
float relativeIllumDiff = illumDiff / max(max(abs(centerLum), abs(sampleLum)), 0.05);
// Keep this gate conservative. The noise fix is to stabilize and stratify the
// ray samples; over-loosening this filter smears direct lighting and makes the
// scene look noisier/blotchier.
float illuminationWeight = exp(-relativeIllumDiff * max(gDenoisePhiColor * 0.035, 0.10));
float normalWeight = pow(saturate(dot(centerNormal, sampleNormal)), max(gDenoisePhiNormal, 1.0));
float positionDiff = length(samplePos - centerPos);
float positionWeight = exp(-positionDiff * max(gDenoisePhiPosition, 0.001));
float depthDiff = abs(sampleDepth - centerDepth);
float depthWeight = exp(-depthDiff * 300.0);
return kernelWeight * albedoWeight * illuminationWeight * normalWeight * positionWeight * depthWeight;
}
[numthreads(8, 8, 1)]
void DenoiseCS(uint3 dispatchThreadId : SV_DispatchThreadID)
{
uint2 pixel = dispatchThreadId.xy;
if (pixel.x >= (uint)gScreenSize.x || pixel.y >= (uint)gScreenSize.y)
return;
float4 albedoSample = gAlbedoTex.Load(int3(pixel, 0));
float depthSample = gDepthTex.Load(int3(pixel, 0));
float4 centerSource = LoadDenoiseSource(int2(pixel));
if (depthSample <= 0.0 || depthSample >= 1.0)
{
// Preserve raygen's emissive bloom on background/no-depth pixels.
// Returning albedo here would erase the halo whenever the internal
// temporal/a-trous denoiser is active.
StoreDenoiseOutput(pixel, centerSource);
return;
}
if (gEnableDenoiser == 0u)
{
StoreDenoiseOutput(pixel, centerSource);
return;
}
float3 centerAlbedo = saturate(albedoSample.rgb);
float3 centerNormal = SafeNormal(gNormalTex.Load(int3(pixel, 0)).xyz);
float4 centerPos4 = gPositionTex.Load(int3(pixel, 0));
float3 centerPos = centerPos4.xyz;
uint centerGeoFlag = DecodeGeometryFlag(centerPos4.w);
int stepI = max((int)round(max(gDenoiseStepWidth, 1.0)), 1);
float3 accumLighting = 0.0;
float weightSum = 0.0;
// Three-pass a-trous wavelet filter. The CPU dispatches this with step
// widths 1, 2, and 4. It is geometry-aware; temporal GI accumulation happens
// before this pass.
[unroll]
for (int ky = 0; ky < 5; ++ky)
{
[unroll]
for (int kx = 0; kx < 5; ++kx)
{
int2 sp = int2(pixel) + int2(kx - 2, ky - 2) * stepI;
if (sp.x < 0 || sp.y < 0 || sp.x >= (int)gScreenSize.x || sp.y >= (int)gScreenSize.y)
continue;
float sampleDepth = gDepthTex.Load(int3(sp, 0));
float4 sampleColor4 = LoadDenoiseSource(sp);
float3 sampleAlbedo = saturate(gAlbedoTex.Load(int3(sp, 0)).rgb);
float3 sampleNormal = SafeNormal(gNormalTex.Load(int3(sp, 0)).xyz);
float4 samplePos4 = gPositionTex.Load(int3(sp, 0));
float3 samplePos = samplePos4.xyz;
uint sampleGeoFlag = DecodeGeometryFlag(samplePos4.w);
float kernelWeight = kKernel[kx] * kKernel[ky];
float w = GeometryAwareWeight(
centerSource.rgb,
sampleColor4.rgb,
centerAlbedo,
sampleAlbedo,
centerNormal,
sampleNormal,
centerPos,
samplePos,
depthSample,
sampleDepth,
centerGeoFlag,
sampleGeoFlag,
kernelWeight);
accumLighting += DemodulateLighting(sampleColor4.rgb, sampleAlbedo) * w;
weightSum += w;
}
}
float3 filteredLighting = (weightSum > 1e-6)
? (accumLighting / weightSum)
: DemodulateLighting(centerSource.rgb, centerAlbedo);
float3 filtered = RemodulateLighting(filteredLighting, centerAlbedo);
// Final-pass firefly clamp against the raw neighborhood.
if (gDenoisePassIndex >= 2u)
{
float3 minRaw = gPathTraceTex.Load(int3(pixel, 0)).rgb;
float3 maxRaw = minRaw;
[unroll]
for (int y = -1; y <= 1; ++y)
{
[unroll]
for (int x = -1; x <= 1; ++x)
{
int2 sp = int2(pixel) + int2(x, y);
if (sp.x < 0 || sp.y < 0 || sp.x >= (int)gScreenSize.x || sp.y >= (int)gScreenSize.y)
continue;
float sampleDepth = gDepthTex.Load(int3(sp, 0));
if (sampleDepth <= 0.0 || sampleDepth >= 1.0)
continue;
float4 samplePos4 = gPositionTex.Load(int3(sp, 0));
uint sampleGeoFlag = DecodeGeometryFlag(samplePos4.w);
const uint classMask = GEOMETRY_FLAG_SKELETAL | GEOMETRY_FLAG_UNLIT | GEOMETRY_FLAG_GLASS;
if (((centerGeoFlag ^ sampleGeoFlag) & classMask) != 0u)
continue;
float3 raw = gPathTraceTex.Load(int3(sp, 0)).rgb;
minRaw = min(minRaw, raw);
maxRaw = max(maxRaw, raw);
}
}
// Keep the clamp tight. A wide clamp lets bright stochastic GI/volume
// outliers survive and was the main reason the previous patch looked worse.
filtered = clamp(filtered, minRaw - 0.15, maxRaw + 0.15);
}
float3 outColor = lerp(centerSource.rgb, filtered, saturate(gDenoiseStrength));
StoreDenoiseOutput(pixel, float4(max(outColor, 0.0), centerSource.a));
}
)";
static const char* g_glRaytracingTemporalHlsl = R"(
cbuffer LightingCB : register(b0)
{
float4x4 gInvViewProj;
float4x4 gInvViewMatrix;
float4x4 gViewProj;
float4 gCameraPos;
float4 gAmbientColor;
float4 gScreenSize;
float gNormalReconstructZ;
uint gLightCount;
uint gEnableSpecular;
uint gEnableHalfLambert;
float gShadowBias;
uint gFrameIndex;
uint gSamplesPerPixel;
uint gMaxBounces;
uint gEnableDenoiser;
uint gDenoisePassIndex;
float gDenoiseStepWidth;
float gDenoiseStrength;
float gDenoisePhiColor;
float gDenoisePhiNormal;
float gDenoisePhiPosition;
float gBumpStrength;
int gSecondaryLightBudget;
uint gSecondaryCorrectionBudget;
float gSecondaryLightCompensation;
uint gHistoryReset;
uint gExternalDenoiser;
float gGiAmount;
float gWorldGrimeAmount;
uint gPadding2;
};
Texture2D<float4> gAlbedoTex : register(t1);
Texture2D<float> gDepthTex : register(t2);
Texture2D<float4> gNormalTex : register(t3);
Texture2D<float4> gPositionTex : register(t4);
Texture2D<float4> gPathTraceTex : register(t6);
Texture2D<float4> gHistoryTex : register(t9);
Texture2D<float4> gVelocityTex : register(t22);
RWTexture2D<float4> gTemporalOutTex : register(u4);
RWTexture2D<float4> gHistoryOutTex : register(u5);
float LuminanceTemporal(float3 c)
{
return dot(c, float3(0.2126, 0.7152, 0.0722));
}
float3 SafeNormalTemporal(float3 n)
{
float lenSq = max(dot(n, n), 1e-8);
return n * rsqrt(lenSq);
}
uint DecodeGeometryFlagTemporal(float geoFlag)
{
return (uint)floor(max(geoFlag, 0.0) + 0.5);
}
float3 ClampHistoryToCurrent(float3 history, float3 current)
{
// Clamp enough to kill rare GI/volume fireflies before they enter history,
// while still allowing real dynamic light changes to pull the accumulator.
float3 radius = 0.12 + abs(current) * 0.42;
return clamp(history, current - radius, current + radius);
}
float4 LoadHistoryClamped(int2 p)
{
int2 maxPixel = int2((int)gScreenSize.x - 1, (int)gScreenSize.y - 1);
p = clamp(p, int2(0, 0), maxPixel);
return gHistoryTex.Load(int3(p, 0));
}
float4 SampleHistoryBilinear(float2 uv)
{
float2 pixelF = uv * gScreenSize.xy - 0.5;
int2 p0 = int2(floor(pixelF));
float2 f = frac(pixelF);
float4 h00 = LoadHistoryClamped(p0 + int2(0, 0));
float4 h10 = LoadHistoryClamped(p0 + int2(1, 0));
float4 h01 = LoadHistoryClamped(p0 + int2(0, 1));
float4 h11 = LoadHistoryClamped(p0 + int2(1, 1));
float4 hx0 = lerp(h00, h10, f.x);
float4 hx1 = lerp(h01, h11, f.x);
return lerp(hx0, hx1, f.y);
}
[numthreads(8, 8, 1)]
void TemporalAccumCS(uint3 dispatchThreadId : SV_DispatchThreadID)
{
uint2 pixel = dispatchThreadId.xy;
if (pixel.x >= (uint)gScreenSize.x || pixel.y >= (uint)gScreenSize.y)
return;
float4 raw = gPathTraceTex.Load(int3(pixel, 0));
float depth = gDepthTex.Load(int3(pixel, 0));
float4 positionSample = gPositionTex.Load(int3(pixel, 0));
uint geoFlag = DecodeGeometryFlagTemporal(positionSample.w);
bool isSkeletal = (geoFlag & 1u) != 0u;
if (depth <= 0.0 || depth >= 1.0 || gMaxBounces <= 1u)
{
gTemporalOutTex[pixel] = raw;
gHistoryOutTex[pixel] = float4(raw.rgb, 0.0);
return;
}
float2 uv = ((float2)pixel + 0.5) / max(gScreenSize.xy, float2(1.0, 1.0));
float2 velocity = gVelocityTex.Load(int3(pixel, 0)).xy; // previousUV - currentUV
float2 prevUv = uv + velocity;
bool validHistoryUv =
prevUv.x >= 0.0 && prevUv.x <= 1.0 &&
prevUv.y >= 0.0 && prevUv.y <= 1.0 &&
all(abs(velocity) < 1.0);
float4 history = validHistoryUv ? SampleHistoryBilinear(prevUv) : float4(raw.rgb, 0.0);
float historyCount = (gFrameIndex == 0u || gHistoryReset != 0u || !validHistoryUv)
? 0.0
: clamp(history.a, 0.0, 63.0);
if (historyCount <= 0.0)
{
gTemporalOutTex[pixel] = raw;
gHistoryOutTex[pixel] = float4(raw.rgb, 1.0);
return;
}
float3 historyColor = ClampHistoryToCurrent(max(history.rgb, 0.0), max(raw.rgb, 0.0));
float rawLum = LuminanceTemporal(max(raw.rgb, 0.0));
float histLum = LuminanceTemporal(historyColor);
float relChange = abs(rawLum - histLum) / max(max(rawLum, histLum), 0.08);
float velocityPixels = length(velocity * gScreenSize.xy);
// Base accumulation approaches 64 frames, but large lighting changes raise
// current-frame weight so the accumulator does not leave obvious trails.
float currentWeight = max(1.0 / (historyCount + 1.0), 0.024);
currentWeight = max(currentWeight, saturate(relChange * 0.18));
currentWeight = max(currentWeight, saturate(velocityPixels * 0.016));
if (isSkeletal)
currentWeight = max(currentWeight, 0.18);
currentWeight = saturate(currentWeight);
float3 resolved = lerp(historyColor, max(raw.rgb, 0.0), currentWeight);
float nextCount = min(historyCount + 1.0, 63.0);
gTemporalOutTex[pixel] = float4(resolved, raw.a);
gHistoryOutTex[pixel] = float4(resolved, nextCount);
}
)";
static ComPtr<IDxcBlob> glRaytracingLightingCompileLibrary(const char* src)
{
ComPtr<IDxcUtils> utils;
ComPtr<IDxcCompiler3> compiler;
ComPtr<IDxcIncludeHandler> includeHandler;
HRESULT hr = DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&utils));
if (FAILED(hr))
{
glRaytracingFatal("DxcCreateInstance utils failed 0x%08X", (unsigned)hr);
return nullptr;
}
hr = DxcCreateInstance(CLSID_DxcCompiler, IID_PPV_ARGS(&compiler));
if (FAILED(hr))
{
glRaytracingFatal("DxcCreateInstance compiler failed 0x%08X", (unsigned)hr);
return nullptr;
}
hr = utils->CreateDefaultIncludeHandler(&includeHandler);
if (FAILED(hr))
{
glRaytracingFatal("CreateDefaultIncludeHandler failed 0x%08X", (unsigned)hr);
return nullptr;
}
DxcBuffer source = {};
source.Ptr = src;
source.Size = strlen(src);
source.Encoding = DXC_CP_UTF8;
const wchar_t* args[] =
{
L"-T", L"lib_6_3",
#if defined(_DEBUG)
L"-Zi",
L"-Qembed_debug",
#endif
// Keep the DXR library smaller to avoid long driver-side linking during
// CreateStateObject(). Compute/post shaders below still compile with O3.
L"-O1",
L"-all_resources_bound"
};
ComPtr<IDxcResult> result;
hr = compiler->Compile(&source, args, _countof(args), includeHandler.Get(), IID_PPV_ARGS(&result));
if (FAILED(hr))
{
glRaytracingFatal("DXC compile failed 0x%08X", (unsigned)hr);
return nullptr;
}
ComPtr<IDxcBlobUtf8> errors;
result->GetOutput(DXC_OUT_ERRORS, IID_PPV_ARGS(&errors), nullptr);
if (errors && errors->GetStringLength() > 0)
{
OutputDebugStringA(errors->GetStringPointer());
OutputDebugStringA("\n");
}
HRESULT status = S_OK;
result->GetStatus(&status);
if (FAILED(status))
{
glRaytracingFatal("DXIL compile status failed 0x%08X", (unsigned)status);
return nullptr;
}
ComPtr<IDxcBlob> dxil;
result->GetOutput(DXC_OUT_OBJECT, IID_PPV_ARGS(&dxil), nullptr);
return dxil;
}
static ComPtr<IDxcBlob> glRaytracingLightingCompileCompute(const char* src, const wchar_t* entryPoint)
{
ComPtr<IDxcUtils> utils;
ComPtr<IDxcCompiler3> compiler;
ComPtr<IDxcIncludeHandler> includeHandler;
HRESULT hr = DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&utils));
if (FAILED(hr))
{
glRaytracingFatal("DxcCreateInstance utils failed 0x%08X", (unsigned)hr);
return nullptr;
}
hr = DxcCreateInstance(CLSID_DxcCompiler, IID_PPV_ARGS(&compiler));
if (FAILED(hr))
{
glRaytracingFatal("DxcCreateInstance compiler failed 0x%08X", (unsigned)hr);
return nullptr;
}
hr = utils->CreateDefaultIncludeHandler(&includeHandler);
if (FAILED(hr))
{
glRaytracingFatal("CreateDefaultIncludeHandler failed 0x%08X", (unsigned)hr);
return nullptr;
}
DxcBuffer source = {};
source.Ptr = src;
source.Size = strlen(src);
source.Encoding = DXC_CP_UTF8;
const wchar_t* args[] =
{
L"-E", entryPoint,
L"-T", L"cs_6_0",
#if defined(_DEBUG)
L"-Zi",
L"-Qembed_debug",
#endif
L"-O3",
L"-all_resources_bound"
};
ComPtr<IDxcResult> result;
hr = compiler->Compile(&source, args, _countof(args), includeHandler.Get(), IID_PPV_ARGS(&result));
if (FAILED(hr))
{
glRaytracingFatal("DXC compute compile failed 0x%08X", (unsigned)hr);
return nullptr;
}
ComPtr<IDxcBlobUtf8> errors;
result->GetOutput(DXC_OUT_ERRORS, IID_PPV_ARGS(&errors), nullptr);
if (errors && errors->GetStringLength() > 0)
{
OutputDebugStringA(errors->GetStringPointer());
OutputDebugStringA("\n");
}
HRESULT status = S_OK;
result->GetStatus(&status);
if (FAILED(status))
{
glRaytracingFatal("DXIL compute compile status failed 0x%08X", (unsigned)status);
return nullptr;
}
ComPtr<IDxcBlob> dxil;
result->GetOutput(DXC_OUT_OBJECT, IID_PPV_ARGS(&dxil), nullptr);
return dxil;
}
static int glRaytracingLightingCreateDescriptorHeap(void)
{
D3D12_DESCRIPTOR_HEAP_DESC hd = {};
hd.NumDescriptors = GLR_DESC_COUNT;
hd.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
hd.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
for (UINT frame = 0; frame < GL_RAYTRACING_CMD_RING_SIZE; ++frame)
{
GLR_CHECK(g_glRaytracingCmd.device->CreateDescriptorHeap(
&hd,
IID_PPV_ARGS(&g_glRaytracingLighting.descriptorHeapRing[frame])));
}
g_glRaytracingLighting.descriptorHeap = g_glRaytracingLighting.descriptorHeapRing[0];
g_glRaytracingLighting.descriptorStride =
g_glRaytracingCmd.device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
return 1;
}
static int glRaytracingLightingCreateRootSignatures(void)
{
{
D3D12_DESCRIPTOR_RANGE ranges[2] = {};
ranges[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
ranges[0].NumDescriptors = GLR_DESC_SRV_COUNT;
ranges[0].BaseShaderRegister = 0;
ranges[0].RegisterSpace = 0;
ranges[0].OffsetInDescriptorsFromTableStart = 0;
ranges[1].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_UAV;
ranges[1].NumDescriptors = GLR_DESC_UAV_COUNT;
ranges[1].BaseShaderRegister = 0;
ranges[1].RegisterSpace = 0;
ranges[1].OffsetInDescriptorsFromTableStart = 0;
D3D12_ROOT_PARAMETER params[3] = {};
params[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
params[0].DescriptorTable.NumDescriptorRanges = 1;
params[0].DescriptorTable.pDescriptorRanges = &ranges[0];
params[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
params[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
params[1].DescriptorTable.NumDescriptorRanges = 1;
params[1].DescriptorTable.pDescriptorRanges = &ranges[1];
params[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
params[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;
params[2].Descriptor.ShaderRegister = 0;
params[2].Descriptor.RegisterSpace = 0;
params[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
D3D12_ROOT_SIGNATURE_DESC rsd = {};
rsd.NumParameters = _countof(params);
rsd.pParameters = params;
rsd.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE;
ComPtr<ID3DBlob> sig;
ComPtr<ID3DBlob> err;
GLR_CHECK(D3D12SerializeRootSignature(&rsd, D3D_ROOT_SIGNATURE_VERSION_1, &sig, &err));
GLR_CHECK(g_glRaytracingCmd.device->CreateRootSignature(
0, sig->GetBufferPointer(), sig->GetBufferSize(),
IID_PPV_ARGS(&g_glRaytracingLighting.globalRootSig)));
}
{
D3D12_ROOT_SIGNATURE_DESC rsd = {};
rsd.Flags = D3D12_ROOT_SIGNATURE_FLAG_LOCAL_ROOT_SIGNATURE;
ComPtr<ID3DBlob> sig;
ComPtr<ID3DBlob> err;
GLR_CHECK(D3D12SerializeRootSignature(&rsd, D3D_ROOT_SIGNATURE_VERSION_1, &sig, &err));
GLR_CHECK(g_glRaytracingCmd.device->CreateRootSignature(
0, sig->GetBufferPointer(), sig->GetBufferSize(),
IID_PPV_ARGS(&g_glRaytracingLighting.localRootSig)));
}
return 1;
}
static int glRaytracingMapUploadBufferPersistent(const glRaytracingBuffer_t& buffer, void** mapped)
{
if (!buffer.resource || !mapped)
return 0;
*mapped = nullptr;
D3D12_RANGE readRange = {};
HRESULT hr = buffer.resource->Map(0, &readRange, mapped);
if (FAILED(hr) || !*mapped)
{
glRaytracingFatal("Persistent upload Map failed 0x%08X", (unsigned)hr);
return 0;
}
return 1;
}
static void glRaytracingLightingSelectFrameResources(UINT frameSlot)
{
frameSlot %= GL_RAYTRACING_CMD_RING_SIZE;
g_glRaytracingLighting.descriptorHeap = g_glRaytracingLighting.descriptorHeapRing[frameSlot];
g_glRaytracingLighting.constantBuffer = g_glRaytracingLighting.constantBufferRing[frameSlot];
g_glRaytracingLighting.lightBuffer = g_glRaytracingLighting.lightBufferRing[frameSlot];
g_glRaytracingLighting.meshMetadataBuffer = g_glRaytracingLighting.meshMetadataBufferRing[frameSlot];
g_glRaytracingLighting.materialVertexBuffer = g_glRaytracingLighting.materialVertexBufferRing[frameSlot];
g_glRaytracingLighting.materialIndexBuffer = g_glRaytracingLighting.materialIndexBufferRing[frameSlot];
g_glRaytracingLighting.constantBufferMapped = g_glRaytracingLighting.constantBufferMappedRing[frameSlot];
g_glRaytracingLighting.lightBufferMapped = g_glRaytracingLighting.lightBufferMappedRing[frameSlot];
g_glRaytracingLighting.meshMetadataBufferMapped = g_glRaytracingLighting.meshMetadataBufferMappedRing[frameSlot];
for (int i = 0; i < 3; ++i)
{
g_glRaytracingLighting.denoiseConstantBuffer[i] = g_glRaytracingLighting.denoiseConstantBufferRing[frameSlot][i];
g_glRaytracingLighting.denoiseConstantBufferMapped[i] = g_glRaytracingLighting.denoiseConstantBufferMappedRing[frameSlot][i];
}
}
static int glRaytracingLightingCreateBuffers(void)
{
const UINT64 constantsBytes = glRaytracingAlignUp(sizeof(glRaytracingLightingConstants_t), 256);
for (UINT frame = 0; frame < GL_RAYTRACING_CMD_RING_SIZE; ++frame)
{
g_glRaytracingLighting.constantBufferRing[frame] = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
constantsBytes,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
D3D12_RESOURCE_FLAG_NONE);
g_glRaytracingLighting.lightBufferRing[frame] = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
sizeof(glRaytracingLight_t) * GL_RAYTRACING_MAX_LIGHTS,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
D3D12_RESOURCE_FLAG_NONE);
g_glRaytracingLighting.meshMetadataBufferRing[frame] = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
sizeof(glRaytracingMeshMetadata_t) * 65536ull,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
D3D12_RESOURCE_FLAG_NONE);
if (!g_glRaytracingLighting.constantBufferRing[frame].resource ||
!g_glRaytracingLighting.lightBufferRing[frame].resource ||
!g_glRaytracingLighting.meshMetadataBufferRing[frame].resource)
{
return 0;
}
for (int i = 0; i < 3; ++i)
{
g_glRaytracingLighting.denoiseConstantBufferRing[frame][i] = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
constantsBytes,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
D3D12_RESOURCE_FLAG_NONE);
if (!g_glRaytracingLighting.denoiseConstantBufferRing[frame][i].resource)
return 0;
}
// These are small UPLOAD-heap buffers updated every pass. Keeping every
// per-frame copy persistently mapped avoids Map/Unmap overhead and makes
// the async command-ring safe: the CPU never overwrites constants that an
// older in-flight command list still reads.
if (!glRaytracingMapUploadBufferPersistent(
g_glRaytracingLighting.constantBufferRing[frame],
&g_glRaytracingLighting.constantBufferMappedRing[frame]))
{
return 0;
}
if (!glRaytracingMapUploadBufferPersistent(
g_glRaytracingLighting.lightBufferRing[frame],
&g_glRaytracingLighting.lightBufferMappedRing[frame]))
{
return 0;
}
if (!glRaytracingMapUploadBufferPersistent(
g_glRaytracingLighting.meshMetadataBufferRing[frame],
&g_glRaytracingLighting.meshMetadataBufferMappedRing[frame]))
{
return 0;
}
for (int i = 0; i < 3; ++i)
{
if (!glRaytracingMapUploadBufferPersistent(
g_glRaytracingLighting.denoiseConstantBufferRing[frame][i],
&g_glRaytracingLighting.denoiseConstantBufferMappedRing[frame][i]))
{
return 0;
}
}
}
glRaytracingLightingSelectFrameResources(0);
return 1;
}
static void glRaytracingLightingUploadConstantsTo(
const glRaytracingBuffer_t& dst,
const glRaytracingLightingConstants_t& constants)
{
if (!dst.resource)
return;
void* mapped = nullptr;
if (dst.resource.Get() == g_glRaytracingLighting.constantBuffer.resource.Get())
{
mapped = g_glRaytracingLighting.constantBufferMapped;
}
else
{
for (int i = 0; i < 3; ++i)
{
if (dst.resource.Get() == g_glRaytracingLighting.denoiseConstantBuffer[i].resource.Get())
{
mapped = g_glRaytracingLighting.denoiseConstantBufferMapped[i];
break;
}
}
}
if (mapped)
{
memcpy(mapped, &constants, sizeof(constants));
return;
}
glRaytracingMapCopy(dst.resource.Get(), &constants, sizeof(constants));
}
static void glRaytracingLightingUpdateConstants(void)
{
if (!g_glRaytracingLighting.uploadToCurrentFrameResource)
return;
glRaytracingLightingUploadConstantsTo(
g_glRaytracingLighting.constantBuffer,
g_glRaytracingLighting.constants);
}
static float glRaytracingLightingEstimateLightImportance(const glRaytracingLight_t& light)
{
float colorPeak = light.color.x;
if (light.color.y > colorPeak) colorPeak = light.color.y;
if (light.color.z > colorPeak) colorPeak = light.color.z;
float radius = light.radius;
if (light.type == GL_RAYTRACING_LIGHT_TYPE_POINT)
{
radius = light.pointRadius.x;
if (light.pointRadius.y > radius) radius = light.pointRadius.y;
if (light.pointRadius.z > radius) radius = light.pointRadius.z;
}
else if (light.type == GL_RAYTRACING_LIGHT_TYPE_RECT)
{
radius = (light.halfWidth > light.halfHeight) ? light.halfWidth : light.halfHeight;
radius *= 6.0f;
}
if (radius < 1.0f)
radius = 1.0f;
float importance = colorPeak * light.intensity * sqrtf(radius);
if (light.samples != 0u)
importance *= 1.15f;
if (light.volumetricScattering > 0.0f)
importance *= 1.10f;
if (light.iesStrength > 0.0f)
importance *= 1.05f;
return importance;
}
static void glRaytracingLightingUpdateLights(void)
{
if (!g_glRaytracingLighting.uploadToCurrentFrameResource)
return;
if (!g_glRaytracingLighting.lightBuffer.resource)
return;
if (g_glRaytracingLighting.cpuLights.empty())
return;
std::vector<glRaytracingLight_t>& uploadLights = g_glRaytracingLighting.uploadLights;
uploadLights = g_glRaytracingLighting.cpuLights;
std::sort(uploadLights.begin(), uploadLights.end(),
[](const glRaytracingLight_t& a, const glRaytracingLight_t& b)
{
return glRaytracingLightingEstimateLightImportance(a) > glRaytracingLightingEstimateLightImportance(b);
});
for (size_t i = 0; i < uploadLights.size(); ++i)
{
glRaytracingLight_t& light = uploadLights[i];
const uint32_t textureId = light.iesTextureId;
if (textureId == 0 || light.iesStrength <= 0.0f)
{
light.iesTextureId = 0;
light.iesStrength = 0.0f;
continue;
}
uint32_t slot = GL_RAYTRACING_MAX_IES_TEXTURES;
for (uint32_t j = 0; j < GL_RAYTRACING_MAX_IES_TEXTURES; ++j)
{
if (g_glRaytracingLighting.iesTextureIds[j] == textureId)
{
slot = j;
break;
}
if (g_glRaytracingLighting.iesTextureIds[j] == 0 && slot == GL_RAYTRACING_MAX_IES_TEXTURES)
{
slot = j;
}
}
if (slot == GL_RAYTRACING_MAX_IES_TEXTURES)
{
light.iesTextureId = 0;
light.iesStrength = 0.0f;
continue;
}
if (g_glRaytracingLighting.iesTextureIds[slot] == 0)
{
DXGI_FORMAT format = DXGI_FORMAT_R8G8B8A8_UNORM;
UINT width = 0;
UINT height = 0;
ID3D12Resource* resource = QD3D12_GetTextureResource((GLuint)textureId, &format, &width, &height, g_glRaytracingCmd.cmdList.Get());
if (!resource || width == 0 || height == 0)
{
light.iesTextureId = 0;
light.iesStrength = 0.0f;
continue;
}
g_glRaytracingLighting.iesTextureIds[slot] = textureId;
g_glRaytracingLighting.iesTextures[slot] = resource;
g_glRaytracingLighting.iesFormats[slot] = format;
}
light.iesTextureId = slot + 1;
}
const size_t bytes = uploadLights.size() * sizeof(glRaytracingLight_t);
if (g_glRaytracingLighting.lightBufferMapped)
{
memcpy(g_glRaytracingLighting.lightBufferMapped, uploadLights.data(), bytes);
return;
}
glRaytracingMapCopy(
g_glRaytracingLighting.lightBuffer.resource.Get(),
uploadLights.data(),
bytes);
}
static bool glRaytracingLightingEnsureMaterialGeometryBuffers(uint32_t vertexCount, uint32_t indexCount)
{
const UINT frameSlot = g_glRaytracingCmd.cmdCurrentSlot % GL_RAYTRACING_CMD_RING_SIZE;
const UINT64 vertexBytes = glRaytracingAlignUp((UINT64)std::max<uint32_t>(vertexCount, 1u) * sizeof(glRaytracingVertex_t), 256);
const UINT64 indexBytes = glRaytracingAlignUp((UINT64)std::max<uint32_t>(indexCount, 1u) * sizeof(uint32_t), 256);
if (!g_glRaytracingLighting.materialVertexBufferRing[frameSlot].resource ||
g_glRaytracingLighting.materialVertexCapacityRing[frameSlot] < vertexCount)
{
g_glRaytracingLighting.materialVertexBufferRing[frameSlot] = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
vertexBytes,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
D3D12_RESOURCE_FLAG_NONE);
if (!g_glRaytracingLighting.materialVertexBufferRing[frameSlot].resource)
return false;
g_glRaytracingLighting.materialVertexCapacityRing[frameSlot] = vertexCount;
}
if (!g_glRaytracingLighting.materialIndexBufferRing[frameSlot].resource ||
g_glRaytracingLighting.materialIndexCapacityRing[frameSlot] < indexCount)
{
g_glRaytracingLighting.materialIndexBufferRing[frameSlot] = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
indexBytes,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
D3D12_RESOURCE_FLAG_NONE);
if (!g_glRaytracingLighting.materialIndexBufferRing[frameSlot].resource)
return false;
g_glRaytracingLighting.materialIndexCapacityRing[frameSlot] = indexCount;
}
g_glRaytracingLighting.materialVertexBuffer = g_glRaytracingLighting.materialVertexBufferRing[frameSlot];
g_glRaytracingLighting.materialIndexBuffer = g_glRaytracingLighting.materialIndexBufferRing[frameSlot];
return true;
}
static bool glRaytracingLightingUpdateMaterialGeometryBuffers(void)
{
if (!g_glRaytracingLighting.uploadToCurrentFrameResource)
return true;
uint64_t totalVertices = 0;
uint64_t totalIndices = 0;
for (size_t i = 0; i < g_glRaytracingScene.meshes.size(); ++i)
{
const glRaytracingMeshRecord_t& mesh = g_glRaytracingScene.meshes[i];
if (!mesh.alive)
continue;
totalVertices += mesh.verticesCpu.size();
totalIndices += mesh.indicesCpu.size();
}
if (totalVertices > 0xFFFFFFFFull || totalIndices > 0xFFFFFFFFull)
return false;
const uint32_t vertexCount = (uint32_t)totalVertices;
const uint32_t indexCount = (uint32_t)totalIndices;
if (!glRaytracingLightingEnsureMaterialGeometryBuffers(vertexCount, indexCount))
return false;
std::vector<glRaytracingVertex_t> packedVertices;
std::vector<uint32_t> packedIndices;
packedVertices.reserve(vertexCount);
packedIndices.reserve(indexCount);
for (size_t i = 0; i < g_glRaytracingScene.meshes.size(); ++i)
{
glRaytracingMeshRecord_t& mesh = g_glRaytracingScene.meshes[i];
if (!mesh.alive)
continue;
const uint32_t vertexOffset = (uint32_t)packedVertices.size();
const uint32_t indexOffset = (uint32_t)packedIndices.size();
mesh.materialVertexOffset = vertexOffset;
mesh.materialIndexOffset = indexOffset;
mesh.materialVertexCount = (uint32_t)mesh.verticesCpu.size();
mesh.materialIndexCount = (uint32_t)mesh.indicesCpu.size();
packedVertices.insert(packedVertices.end(), mesh.verticesCpu.begin(), mesh.verticesCpu.end());
for (size_t index = 0; index < mesh.indicesCpu.size(); ++index)
packedIndices.push_back(vertexOffset + mesh.indicesCpu[index]);
}
g_glRaytracingLighting.materialVertexCount = vertexCount;
g_glRaytracingLighting.materialIndexCount = indexCount;
if (vertexCount > 0)
{
glRaytracingMapCopy(
g_glRaytracingLighting.materialVertexBuffer.resource.Get(),
packedVertices.data(),
(size_t)vertexCount * sizeof(glRaytracingVertex_t));
}
if (indexCount > 0)
{
glRaytracingMapCopy(
g_glRaytracingLighting.materialIndexBuffer.resource.Get(),
packedIndices.data(),
(size_t)indexCount * sizeof(uint32_t));
}
return true;
}
static void glRaytracingLightingUpdateMeshMetadata(void)
{
if (!g_glRaytracingLighting.uploadToCurrentFrameResource)
return;
if (!g_glRaytracingLighting.meshMetadataBuffer.resource)
return;
glRaytracingMeshMetadata_t fallback = {};
fallback.averageColor[0] = fallback.averageColor[1] = fallback.averageColor[2] = fallback.averageColor[3] = 1.0f;
fallback.areaNum = -1;
void* mapped = g_glRaytracingLighting.meshMetadataBufferMapped;
if (!mapped)
{
glRaytracingMapCopy(
g_glRaytracingLighting.meshMetadataBuffer.resource.Get(),
&fallback,
sizeof(fallback));
return;
}
glRaytracingMeshMetadata_t* metadata = (glRaytracingMeshMetadata_t*)mapped;
metadata[0] = fallback;
for (size_t i = 0; i < g_glRaytracingScene.meshes.size(); ++i)
{
const glRaytracingMeshRecord_t& mesh = g_glRaytracingScene.meshes[i];
if (!mesh.alive)
continue;
const uint32_t metadataIndex = mesh.handle & GL_RAYTRACING_INSTANCE_USER_ID_MASK;
memcpy(metadata[metadataIndex].averageColor, mesh.averageColor, sizeof(metadata[metadataIndex].averageColor));
memcpy(metadata[metadataIndex].emissiveColor, mesh.emissiveColor, sizeof(metadata[metadataIndex].emissiveColor));
metadata[metadataIndex].procFlags = mesh.procFlags;
metadata[metadataIndex].areaNum = mesh.areaNum;
metadata[metadataIndex].vertexOffset = mesh.materialVertexOffset;
metadata[metadataIndex].indexOffset = mesh.materialIndexOffset;
metadata[metadataIndex].vertexCount = mesh.materialVertexCount;
metadata[metadataIndex].indexCount = mesh.materialIndexCount;
}
}
static void glRaytracingLightingUnmapUploadBuffers(void)
{
for (UINT frame = 0; frame < GL_RAYTRACING_CMD_RING_SIZE; ++frame)
{
if (g_glRaytracingLighting.constantBufferRing[frame].resource &&
g_glRaytracingLighting.constantBufferMappedRing[frame])
{
g_glRaytracingLighting.constantBufferRing[frame].resource->Unmap(0, nullptr);
g_glRaytracingLighting.constantBufferMappedRing[frame] = nullptr;
}
if (g_glRaytracingLighting.lightBufferRing[frame].resource &&
g_glRaytracingLighting.lightBufferMappedRing[frame])
{
g_glRaytracingLighting.lightBufferRing[frame].resource->Unmap(0, nullptr);
g_glRaytracingLighting.lightBufferMappedRing[frame] = nullptr;
}
if (g_glRaytracingLighting.meshMetadataBufferRing[frame].resource &&
g_glRaytracingLighting.meshMetadataBufferMappedRing[frame])
{
g_glRaytracingLighting.meshMetadataBufferRing[frame].resource->Unmap(0, nullptr);
g_glRaytracingLighting.meshMetadataBufferMappedRing[frame] = nullptr;
}
for (int i = 0; i < 3; ++i)
{
if (g_glRaytracingLighting.denoiseConstantBufferRing[frame][i].resource &&
g_glRaytracingLighting.denoiseConstantBufferMappedRing[frame][i])
{
g_glRaytracingLighting.denoiseConstantBufferRing[frame][i].resource->Unmap(0, nullptr);
g_glRaytracingLighting.denoiseConstantBufferMappedRing[frame][i] = nullptr;
}
}
}
g_glRaytracingLighting.constantBufferMapped = nullptr;
g_glRaytracingLighting.lightBufferMapped = nullptr;
g_glRaytracingLighting.meshMetadataBufferMapped = nullptr;
for (int i = 0; i < 3; ++i)
g_glRaytracingLighting.denoiseConstantBufferMapped[i] = nullptr;
}
static void glRaytracingLightingCreatePersistentLightSRV(void)
{
for (UINT frame = 0; frame < GL_RAYTRACING_CMD_RING_SIZE; ++frame)
{
if (!g_glRaytracingLighting.descriptorHeapRing[frame] ||
!g_glRaytracingLighting.lightBufferRing[frame].resource ||
!g_glRaytracingLighting.meshMetadataBufferRing[frame].resource)
{
continue;
}
D3D12_CPU_DESCRIPTOR_HANDLE base =
g_glRaytracingLighting.descriptorHeapRing[frame]->GetCPUDescriptorHandleForHeapStart();
D3D12_SHADER_RESOURCE_VIEW_DESC srv = {};
srv.ViewDimension = D3D12_SRV_DIMENSION_BUFFER;
srv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srv.Format = DXGI_FORMAT_UNKNOWN;
srv.Buffer.FirstElement = 0;
srv.Buffer.NumElements = GL_RAYTRACING_MAX_LIGHTS;
srv.Buffer.StructureByteStride = sizeof(glRaytracingLight_t);
srv.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_NONE;
g_glRaytracingCmd.device->CreateShaderResourceView(
g_glRaytracingLighting.lightBufferRing[frame].resource.Get(),
&srv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_LIGHTS_SRV));
srv.Buffer.NumElements = 65536;
srv.Buffer.StructureByteStride = sizeof(glRaytracingMeshMetadata_t);
g_glRaytracingCmd.device->CreateShaderResourceView(
g_glRaytracingLighting.meshMetadataBufferRing[frame].resource.Get(),
&srv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_MESH_METADATA_SRV));
}
}
static int glRaytracingLightingCreateStateObject(void)
{
ComPtr<IDxcBlob> dxil = glRaytracingLightingCompileLibrary(g_glRaytracingLightingHlsl);
if (!dxil)
return 0;
D3D12_EXPORT_DESC exports[8] = {};
exports[0].Name = L"RayGen";
exports[1].Name = L"ShadowMiss";
exports[2].Name = L"ShadowAnyHit";
exports[3].Name = L"ShadowClosestHit";
exports[4].Name = L"BounceMiss";
exports[5].Name = L"BounceAnyHit";
exports[6].Name = L"BounceClosestHit";
exports[7].Name = L"ReflectionClosestHit";
D3D12_DXIL_LIBRARY_DESC libDesc = {};
D3D12_SHADER_BYTECODE libBytecode = {};
libBytecode.pShaderBytecode = dxil->GetBufferPointer();
libBytecode.BytecodeLength = dxil->GetBufferSize();
libDesc.DXILLibrary = libBytecode;
libDesc.NumExports = _countof(exports);
libDesc.pExports = exports;
D3D12_HIT_GROUP_DESC hitGroups[3] = {};
hitGroups[0].HitGroupExport = L"ShadowHitGroup";
hitGroups[0].AnyHitShaderImport = L"ShadowAnyHit";
hitGroups[0].ClosestHitShaderImport = L"ShadowClosestHit";
hitGroups[0].Type = D3D12_HIT_GROUP_TYPE_TRIANGLES;
hitGroups[1].HitGroupExport = L"BounceHitGroup";
hitGroups[1].AnyHitShaderImport = L"BounceAnyHit";
hitGroups[1].ClosestHitShaderImport = L"BounceClosestHit";
hitGroups[1].Type = D3D12_HIT_GROUP_TYPE_TRIANGLES;
hitGroups[2].HitGroupExport = L"ReflectionHitGroup";
hitGroups[2].ClosestHitShaderImport = L"ReflectionClosestHit";
hitGroups[2].Type = D3D12_HIT_GROUP_TYPE_TRIANGLES;
D3D12_RAYTRACING_SHADER_CONFIG shaderConfig = {};
shaderConfig.MaxPayloadSizeInBytes = 48; // BouncePayload now carries hit normal/UV for secondary material shading.
shaderConfig.MaxAttributeSizeInBytes = 8;
D3D12_GLOBAL_ROOT_SIGNATURE globalRS = {};
globalRS.pGlobalRootSignature = g_glRaytracingLighting.globalRootSig.Get();
D3D12_LOCAL_ROOT_SIGNATURE localRS = {};
localRS.pLocalRootSignature = g_glRaytracingLighting.localRootSig.Get();
D3D12_STATE_SUBOBJECT subobjects[9] = {};
UINT sub = 0;
subobjects[sub].Type = D3D12_STATE_SUBOBJECT_TYPE_DXIL_LIBRARY;
subobjects[sub].pDesc = &libDesc;
++sub;
subobjects[sub].Type = D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP;
subobjects[sub].pDesc = &hitGroups[0];
++sub;
subobjects[sub].Type = D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP;
subobjects[sub].pDesc = &hitGroups[1];
++sub;
subobjects[sub].Type = D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP;
subobjects[sub].pDesc = &hitGroups[2];
++sub;
subobjects[sub].Type = D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_SHADER_CONFIG;
subobjects[sub].pDesc = &shaderConfig;
++sub;
subobjects[sub].Type = D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE;
subobjects[sub].pDesc = &globalRS;
++sub;
subobjects[sub].Type = D3D12_STATE_SUBOBJECT_TYPE_LOCAL_ROOT_SIGNATURE;
subobjects[sub].pDesc = &localRS;
++sub;
LPCWSTR localExports[] =
{
L"RayGen",
L"ShadowMiss",
L"ShadowHitGroup",
L"BounceMiss",
L"BounceHitGroup",
L"ReflectionHitGroup"
};
D3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION assoc = {};
assoc.pSubobjectToAssociate = &subobjects[6];
assoc.NumExports = _countof(localExports);
assoc.pExports = localExports;
subobjects[sub].Type = D3D12_STATE_SUBOBJECT_TYPE_SUBOBJECT_TO_EXPORTS_ASSOCIATION;
subobjects[sub].pDesc = &assoc;
++sub;
D3D12_RAYTRACING_PIPELINE_CONFIG pipeConfig = {};
pipeConfig.MaxTraceRecursionDepth = 1;
subobjects[sub].Type = D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_PIPELINE_CONFIG;
subobjects[sub].pDesc = &pipeConfig;
++sub;
D3D12_STATE_OBJECT_DESC soDesc = {};
soDesc.Type = D3D12_STATE_OBJECT_TYPE_RAYTRACING_PIPELINE;
soDesc.NumSubobjects = sub;
soDesc.pSubobjects = subobjects;
GLR_CHECK(g_glRaytracingCmd.device->CreateStateObject(&soDesc, IID_PPV_ARGS(&g_glRaytracingLighting.rtStateObject)));
GLR_CHECK(g_glRaytracingLighting.rtStateObject.As(&g_glRaytracingLighting.rtStateProps));
return 1;
}
static int glRaytracingLightingCreateShaderTables(void)
{
void* raygenId = g_glRaytracingLighting.rtStateProps->GetShaderIdentifier(L"RayGen");
void* shadowMissId = g_glRaytracingLighting.rtStateProps->GetShaderIdentifier(L"ShadowMiss");
void* bounceMissId = g_glRaytracingLighting.rtStateProps->GetShaderIdentifier(L"BounceMiss");
void* shadowHitId = g_glRaytracingLighting.rtStateProps->GetShaderIdentifier(L"ShadowHitGroup");
void* bounceHitId = g_glRaytracingLighting.rtStateProps->GetShaderIdentifier(L"BounceHitGroup");
void* reflectionHitId = g_glRaytracingLighting.rtStateProps->GetShaderIdentifier(L"ReflectionHitGroup");
if (!raygenId || !shadowMissId || !bounceMissId || !shadowHitId || !bounceHitId || !reflectionHitId)
{
glRaytracingFatal("Failed to fetch shader identifiers");
return 0;
}
const UINT shaderIdSize = D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES;
const UINT recordSize = (UINT)glRaytracingAlignUp(shaderIdSize, D3D12_RAYTRACING_SHADER_RECORD_BYTE_ALIGNMENT);
const UINT missTableSize = recordSize * 2u;
const UINT hitTableSize = recordSize * 3u;
g_glRaytracingLighting.raygenTable = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
recordSize,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
D3D12_RESOURCE_FLAG_NONE);
g_glRaytracingLighting.missTable = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
missTableSize,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
D3D12_RESOURCE_FLAG_NONE);
g_glRaytracingLighting.hitTable = glRaytracingCreateBuffer(
g_glRaytracingCmd.device.Get(),
hitTableSize,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
D3D12_RESOURCE_FLAG_NONE);
if (!g_glRaytracingLighting.raygenTable.resource ||
!g_glRaytracingLighting.missTable.resource ||
!g_glRaytracingLighting.hitTable.resource)
{
return 0;
}
std::vector<uint8_t> temp;
temp.resize((size_t)max(recordSize, max(missTableSize, hitTableSize)), 0);
memset(temp.data(), 0, temp.size());
memcpy(temp.data(), raygenId, shaderIdSize);
glRaytracingMapCopy(g_glRaytracingLighting.raygenTable.resource.Get(), temp.data(), recordSize);
memset(temp.data(), 0, temp.size());
memcpy(temp.data(), shadowMissId, shaderIdSize);
memcpy(temp.data() + recordSize, bounceMissId, shaderIdSize);
glRaytracingMapCopy(g_glRaytracingLighting.missTable.resource.Get(), temp.data(), missTableSize);
memset(temp.data(), 0, temp.size());
memcpy(temp.data(), shadowHitId, shaderIdSize);
memcpy(temp.data() + recordSize, bounceHitId, shaderIdSize);
memcpy(temp.data() + recordSize * 2u, reflectionHitId, shaderIdSize);
glRaytracingMapCopy(g_glRaytracingLighting.hitTable.resource.Get(), temp.data(), hitTableSize);
return 1;
}
static int glRaytracingLightingCreateDenoisePipeline(void)
{
ComPtr<IDxcBlob> dxil = glRaytracingLightingCompileCompute(g_glRaytracingDenoiseHlsl, L"DenoiseCS");
if (!dxil)
return 0;
D3D12_COMPUTE_PIPELINE_STATE_DESC pso = {};
pso.pRootSignature = g_glRaytracingLighting.globalRootSig.Get();
pso.CS.pShaderBytecode = dxil->GetBufferPointer();
pso.CS.BytecodeLength = dxil->GetBufferSize();
GLR_CHECK(g_glRaytracingCmd.device->CreateComputePipelineState(
&pso,
IID_PPV_ARGS(&g_glRaytracingLighting.denoisePSO)));
return 1;
}
static int glRaytracingLightingCreateTemporalPipeline(void)
{
ComPtr<IDxcBlob> dxil = glRaytracingLightingCompileCompute(g_glRaytracingTemporalHlsl, L"TemporalAccumCS");
if (!dxil)
return 0;
D3D12_COMPUTE_PIPELINE_STATE_DESC pso = {};
pso.pRootSignature = g_glRaytracingLighting.globalRootSig.Get();
pso.CS.pShaderBytecode = dxil->GetBufferPointer();
pso.CS.BytecodeLength = dxil->GetBufferSize();
GLR_CHECK(g_glRaytracingCmd.device->CreateComputePipelineState(
&pso,
IID_PPV_ARGS(&g_glRaytracingLighting.temporalPSO)));
return 1;
}
static int glRaytracingLightingEnsureDenoiseResources(UINT width, UINT height)
{
if (width == 0 || height == 0)
return 0;
if (g_glRaytracingLighting.pathTraceTexture[0].resource &&
g_glRaytracingLighting.pathTraceTexture[1].resource &&
g_glRaytracingLighting.temporalTexture.resource &&
g_glRaytracingLighting.historyTexture[0].resource &&
g_glRaytracingLighting.historyTexture[1].resource &&
g_glRaytracingLighting.denoiseTemp[0].resource &&
g_glRaytracingLighting.denoiseTemp[1].resource &&
g_glRaytracingLighting.denoiseWidth == width &&
g_glRaytracingLighting.denoiseHeight == height &&
g_glRaytracingLighting.denoiseFormat == GL_RAYTRACING_DENOISE_FORMAT)
{
return 1;
}
if (g_glRaytracingLighting.pathTraceTexture[0].resource ||
g_glRaytracingLighting.pathTraceTexture[1].resource ||
g_glRaytracingLighting.temporalTexture.resource ||
g_glRaytracingLighting.historyTexture[0].resource ||
g_glRaytracingLighting.historyTexture[1].resource ||
g_glRaytracingLighting.denoiseTemp[0].resource ||
g_glRaytracingLighting.denoiseTemp[1].resource)
{
glRaytracingWaitIdle();
}
g_glRaytracingLighting.pathTraceTexture[0] = glRaytracingTexture_t();
g_glRaytracingLighting.pathTraceTexture[1] = glRaytracingTexture_t();
g_glRaytracingLighting.temporalTexture = glRaytracingTexture_t();
g_glRaytracingLighting.historyTexture[0] = glRaytracingTexture_t();
g_glRaytracingLighting.historyTexture[1] = glRaytracingTexture_t();
g_glRaytracingLighting.denoiseTemp[0] = glRaytracingTexture_t();
g_glRaytracingLighting.denoiseTemp[1] = glRaytracingTexture_t();
for (int i = 0; i < 2; ++i)
{
g_glRaytracingLighting.pathTraceTexture[i] = glRaytracingCreateTexture2D(
g_glRaytracingCmd.device.Get(),
width,
height,
GL_RAYTRACING_DENOISE_FORMAT,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
glRaytracingSetObjectName(g_glRaytracingLighting.pathTraceTexture[i].resource.Get(),
i == 0 ? L"GLR Raw Path Trace HDR 0" : L"GLR Raw Path Trace HDR 1");
}
g_glRaytracingLighting.temporalTexture = glRaytracingCreateTexture2D(
g_glRaytracingCmd.device.Get(),
width,
height,
GL_RAYTRACING_DENOISE_FORMAT,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
glRaytracingSetObjectName(g_glRaytracingLighting.temporalTexture.resource.Get(), L"GLR Temporal GI Accumulation");
for (int i = 0; i < 2; ++i)
{
g_glRaytracingLighting.historyTexture[i] = glRaytracingCreateTexture2D(
g_glRaytracingCmd.device.Get(),
width,
height,
GL_RAYTRACING_DENOISE_FORMAT,
D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
glRaytracingSetObjectName(g_glRaytracingLighting.historyTexture[i].resource.Get(),
i == 0 ? L"GLR GI History 0" : L"GLR GI History 1");
}
for (int i = 0; i < 2; ++i)
{
g_glRaytracingLighting.denoiseTemp[i] = glRaytracingCreateTexture2D(
g_glRaytracingCmd.device.Get(),
width,
height,
GL_RAYTRACING_DENOISE_FORMAT,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
glRaytracingSetObjectName(g_glRaytracingLighting.denoiseTemp[i].resource.Get(),
i == 0 ? L"GLR A-Trous Temp A" : L"GLR A-Trous Temp B");
}
if (!g_glRaytracingLighting.pathTraceTexture[0].resource ||
!g_glRaytracingLighting.pathTraceTexture[1].resource ||
!g_glRaytracingLighting.temporalTexture.resource ||
!g_glRaytracingLighting.historyTexture[0].resource ||
!g_glRaytracingLighting.historyTexture[1].resource ||
!g_glRaytracingLighting.denoiseTemp[0].resource ||
!g_glRaytracingLighting.denoiseTemp[1].resource)
{
return 0;
}
g_glRaytracingLighting.currentHistoryIndex = 0;
g_glRaytracingLighting.currentPathTraceIndex = 0;
g_glRaytracingLighting.denoiseWidth = width;
g_glRaytracingLighting.denoiseHeight = height;
g_glRaytracingLighting.denoiseFormat = GL_RAYTRACING_DENOISE_FORMAT;
glRaytracingLightingResetDenoiseHistory();
return 1;
}
static void glRaytracingLightingCreatePerPassDescriptors(
const glRaytracingLightingPassDesc_t* pass,
ID3D12Resource* topLevelAS,
ID3D12Resource* rayOutputTexture,
ID3D12Resource* pathTraceTexture,
ID3D12Resource* denoiseATexture,
ID3D12Resource* denoiseBTexture,
ID3D12Resource* historyReadTexture,
ID3D12Resource* historyWriteTexture,
ID3D12Resource* temporalTexture)
{
D3D12_CPU_DESCRIPTOR_HANDLE base = g_glRaytracingLighting.descriptorHeap->GetCPUDescriptorHandleForHeapStart();
D3D12_SHADER_RESOURCE_VIEW_DESC albedoSrv = {};
albedoSrv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
albedoSrv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
albedoSrv.Format = pass->albedoFormat;
albedoSrv.Texture2D.MipLevels = 1;
g_glRaytracingCmd.device->CreateShaderResourceView(pass->albedoTexture, &albedoSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_ALBEDO_SRV));
D3D12_SHADER_RESOURCE_VIEW_DESC depthSrv = {};
depthSrv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
depthSrv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
depthSrv.Format = glRaytracingGetSrvFormatForDepth(pass->depthFormat);
depthSrv.Texture2D.MipLevels = 1;
g_glRaytracingCmd.device->CreateShaderResourceView(pass->depthTexture, &depthSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_DEPTH_SRV));
D3D12_SHADER_RESOURCE_VIEW_DESC normalSrv = {};
normalSrv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
normalSrv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
normalSrv.Format = pass->normalFormat;
normalSrv.Texture2D.MipLevels = 1;
g_glRaytracingCmd.device->CreateShaderResourceView(pass->normalTexture, &normalSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_NORMAL_SRV));
D3D12_SHADER_RESOURCE_VIEW_DESC positionSrv = {};
positionSrv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
positionSrv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
positionSrv.Format = pass->positionFormat;
positionSrv.Texture2D.MipLevels = 1;
g_glRaytracingCmd.device->CreateShaderResourceView(pass->positionTexture, &positionSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_POSITION_SRV));
D3D12_SHADER_RESOURCE_VIEW_DESC tlasSrv = {};
tlasSrv.ViewDimension = D3D12_SRV_DIMENSION_RAYTRACING_ACCELERATION_STRUCTURE;
tlasSrv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
tlasSrv.RaytracingAccelerationStructure.Location = topLevelAS->GetGPUVirtualAddress();
g_glRaytracingCmd.device->CreateShaderResourceView(nullptr, &tlasSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_TLAS_SRV));
D3D12_SHADER_RESOURCE_VIEW_DESC denoiseSrv = {};
denoiseSrv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
denoiseSrv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
denoiseSrv.Format = GL_RAYTRACING_DENOISE_FORMAT;
denoiseSrv.Texture2D.MipLevels = 1;
g_glRaytracingCmd.device->CreateShaderResourceView(pathTraceTexture, &denoiseSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_PATHTRACE_SRV));
g_glRaytracingCmd.device->CreateShaderResourceView(denoiseATexture, &denoiseSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_DENOISE_A_SRV));
g_glRaytracingCmd.device->CreateShaderResourceView(denoiseBTexture, &denoiseSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_DENOISE_B_SRV));
g_glRaytracingCmd.device->CreateShaderResourceView(historyReadTexture, &denoiseSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_HISTORY_SRV));
g_glRaytracingCmd.device->CreateShaderResourceView(temporalTexture, &denoiseSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_TEMPORAL_SRV));
D3D12_SHADER_RESOURCE_VIEW_DESC emissiveSrv = {};
emissiveSrv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
emissiveSrv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
emissiveSrv.Format = g_glRaytracingLighting.emissiveFormat;
emissiveSrv.Texture2D.MipLevels = 1;
g_glRaytracingCmd.device->CreateShaderResourceView(g_glRaytracingLighting.emissiveTexture, &emissiveSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_EMISSIVE_SRV));
D3D12_SHADER_RESOURCE_VIEW_DESC specularSrv = {};
specularSrv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
specularSrv.Texture2D.MipLevels = 1;
ID3D12Resource* specularResource = g_glRaytracingLighting.specularTexture;
if (specularResource)
{
specularSrv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
specularSrv.Format = g_glRaytracingLighting.specularFormat;
}
else
{
// Bind a harmless fallback descriptor when the shim has not provided a
// specular G-buffer. The shader sees alpha zero and uses the legacy fallback.
specularResource = pass->albedoTexture;
specularSrv.Shader4ComponentMapping = D3D12_ENCODE_SHADER_4_COMPONENT_MAPPING(
D3D12_SHADER_COMPONENT_MAPPING_FROM_MEMORY_COMPONENT_0,
D3D12_SHADER_COMPONENT_MAPPING_FROM_MEMORY_COMPONENT_1,
D3D12_SHADER_COMPONENT_MAPPING_FROM_MEMORY_COMPONENT_2,
D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_0);
specularSrv.Format = pass->albedoFormat;
}
g_glRaytracingCmd.device->CreateShaderResourceView(specularResource, &specularSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_SPECULAR_SRV));
for (uint32_t i = 0; i < GL_RAYTRACING_MAX_IES_TEXTURES; ++i)
{
D3D12_SHADER_RESOURCE_VIEW_DESC iesSrv = {};
iesSrv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
iesSrv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
iesSrv.Format = g_glRaytracingLighting.iesTextures[i]
? g_glRaytracingLighting.iesFormats[i]
: DXGI_FORMAT_R8G8B8A8_UNORM;
iesSrv.Texture2D.MipLevels = 1;
g_glRaytracingCmd.device->CreateShaderResourceView(
g_glRaytracingLighting.iesTextures[i],
&iesSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_IES_TEXTURES_SRV + i));
}
D3D12_SHADER_RESOURCE_VIEW_DESC velocitySrv = {};
velocitySrv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
velocitySrv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
velocitySrv.Format = pass->velocityTexture ? pass->velocityFormat : pass->positionFormat;
velocitySrv.Texture2D.MipLevels = 1;
g_glRaytracingCmd.device->CreateShaderResourceView(
pass->velocityTexture ? pass->velocityTexture : pass->positionTexture,
&velocitySrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_VELOCITY_SRV));
D3D12_SHADER_RESOURCE_VIEW_DESC materialVertexSrv = {};
materialVertexSrv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
materialVertexSrv.ViewDimension = D3D12_SRV_DIMENSION_BUFFER;
materialVertexSrv.Format = DXGI_FORMAT_UNKNOWN;
materialVertexSrv.Buffer.FirstElement = 0;
materialVertexSrv.Buffer.NumElements = std::max<UINT>(g_glRaytracingLighting.materialVertexCount, 1u);
materialVertexSrv.Buffer.StructureByteStride = sizeof(glRaytracingVertex_t);
materialVertexSrv.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_NONE;
g_glRaytracingCmd.device->CreateShaderResourceView(
g_glRaytracingLighting.materialVertexBuffer.resource.Get(),
&materialVertexSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_MATERIAL_VERTICES_SRV));
D3D12_SHADER_RESOURCE_VIEW_DESC materialIndexSrv = {};
materialIndexSrv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
materialIndexSrv.ViewDimension = D3D12_SRV_DIMENSION_BUFFER;
materialIndexSrv.Format = DXGI_FORMAT_UNKNOWN;
materialIndexSrv.Buffer.FirstElement = 0;
materialIndexSrv.Buffer.NumElements = std::max<UINT>(g_glRaytracingLighting.materialIndexCount, 1u);
materialIndexSrv.Buffer.StructureByteStride = sizeof(uint32_t);
materialIndexSrv.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_NONE;
g_glRaytracingCmd.device->CreateShaderResourceView(
g_glRaytracingLighting.materialIndexBuffer.resource.Get(),
&materialIndexSrv,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_MATERIAL_INDICES_SRV));
D3D12_UNORDERED_ACCESS_VIEW_DESC rayOutputUav = {};
rayOutputUav.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D;
rayOutputUav.Format = GL_RAYTRACING_DENOISE_FORMAT;
g_glRaytracingCmd.device->CreateUnorderedAccessView(rayOutputTexture, nullptr, &rayOutputUav,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_PATHTRACE_UAV));
D3D12_UNORDERED_ACCESS_VIEW_DESC denoiseUav = {};
denoiseUav.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D;
denoiseUav.Format = GL_RAYTRACING_DENOISE_FORMAT;
g_glRaytracingCmd.device->CreateUnorderedAccessView(denoiseATexture, nullptr, &denoiseUav,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_DENOISE_A_UAV));
g_glRaytracingCmd.device->CreateUnorderedAccessView(denoiseBTexture, nullptr, &denoiseUav,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_DENOISE_B_UAV));
D3D12_UNORDERED_ACCESS_VIEW_DESC outputUav = {};
outputUav.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D;
outputUav.Format = pass->outputFormat;
g_glRaytracingCmd.device->CreateUnorderedAccessView(pass->outputTexture, nullptr, &outputUav,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_OUTPUT_UAV));
D3D12_UNORDERED_ACCESS_VIEW_DESC temporalUav = {};
temporalUav.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D;
temporalUav.Format = GL_RAYTRACING_DENOISE_FORMAT;
g_glRaytracingCmd.device->CreateUnorderedAccessView(temporalTexture, nullptr, &temporalUav,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_TEMPORAL_UAV));
g_glRaytracingCmd.device->CreateUnorderedAccessView(historyWriteTexture, nullptr, &temporalUav,
glRaytracingOffsetCpu(base, g_glRaytracingLighting.descriptorStride, GLR_DESC_HISTORY_UAV));
}
// ============================================================
// Lighting public API
// ============================================================
static bool glRaytracingLightingExecuteInternal(
const glRaytracingLightingPassDesc_t* pass,
ID3D12Resource* topLevelAS)
{
if (!g_glRaytracingLighting.initialized || !pass || !topLevelAS)
return false;
if (!pass->albedoTexture || !pass->depthTexture || !pass->normalTexture || !pass->positionTexture || !pass->outputTexture)
return false;
if (pass->width == 0 || pass->height == 0)
return false;
if (!glRaytracingLightingEnsureDenoiseResources(pass->width, pass->height))
return false;
const bool useInternalDenoiser =
(g_glRaytracingLighting.constants.enableDenoiser != 0u) &&
!g_glRaytracingLighting.externalDenoiser;
ID3D12Resource* rayOutputTexture = useInternalDenoiser
? g_glRaytracingLighting.pathTraceTexture[g_glRaytracingLighting.currentPathTraceIndex & 1u].resource.Get()
: pass->outputTexture;
if (!glRaytracingBeginCmd())
return false;
glRaytracingLightingSelectFrameResources(g_glRaytracingCmd.cmdCurrentSlot);
g_glRaytracingLighting.constants.screenSize[0] = (float)pass->width;
g_glRaytracingLighting.constants.screenSize[1] = (float)pass->height;
g_glRaytracingLighting.constants.screenSize[2] = 1.0f / (float)pass->width;
g_glRaytracingLighting.constants.screenSize[3] = 1.0f / (float)pass->height;
g_glRaytracingLighting.constants.frameIndex = g_glRaytracingLighting.frameCounter;
g_glRaytracingLighting.constants.externalDenoiser = g_glRaytracingLighting.externalDenoiser ? 1u : 0u;
g_glRaytracingLighting.constants.lightCount =
(uint32_t)glRaytracingClamp<size_t>(g_glRaytracingLighting.cpuLights.size(), 0, GL_RAYTRACING_MAX_LIGHTS);
g_glRaytracingLighting.uploadToCurrentFrameResource = true;
glRaytracingLightingUpdateLights();
if (!glRaytracingLightingUpdateMaterialGeometryBuffers())
{
g_glRaytracingLighting.uploadToCurrentFrameResource = false;
if (g_glRaytracingCmd.cmdList)
g_glRaytracingCmd.cmdList->Close();
return false;
}
glRaytracingLightingUpdateMeshMetadata();
glRaytracingLightingUpdateConstants();
for (uint32_t passIndex = 0; passIndex < 3u; ++passIndex)
{
glRaytracingLightingConstants_t denoiseConstants = g_glRaytracingLighting.constants;
denoiseConstants.denoisePassIndex = passIndex;
denoiseConstants.denoiseStepWidth = (float)(1u << passIndex);
glRaytracingLightingUploadConstantsTo(g_glRaytracingLighting.denoiseConstantBuffer[passIndex], denoiseConstants);
}
g_glRaytracingLighting.uploadToCurrentFrameResource = false;
glRaytracingBeginPixEvent(g_glRaytracingCmd.cmdList.Get(), useInternalDenoiser
? L"GLR Path Traced Lighting With Internal Denoiser"
: L"GLR Path Traced Lighting Raw For External Denoiser");
const uint32_t historyReadIndex = g_glRaytracingLighting.currentHistoryIndex & 1u;
const uint32_t historyWriteIndex = (g_glRaytracingLighting.currentHistoryIndex ^ 1u) & 1u;
const uint32_t pathTraceIndex = g_glRaytracingLighting.currentPathTraceIndex & 1u;
glRaytracingTexture_t& pathTraceTexture = g_glRaytracingLighting.pathTraceTexture[pathTraceIndex];
{
GLR_PIX_EVENT(g_glRaytracingCmd.cmdList.Get(), L"GLR Create Per-Pass Descriptors");
glRaytracingLightingCreatePerPassDescriptors(
pass,
topLevelAS,
rayOutputTexture,
pathTraceTexture.resource.Get(),
g_glRaytracingLighting.denoiseTemp[0].resource.Get(),
g_glRaytracingLighting.denoiseTemp[1].resource.Get(),
g_glRaytracingLighting.historyTexture[historyReadIndex].resource.Get(),
g_glRaytracingLighting.historyTexture[historyWriteIndex].resource.Get(),
g_glRaytracingLighting.temporalTexture.resource.Get());
}
if (useInternalDenoiser)
{
GLR_PIX_MARKER(g_glRaytracingCmd.cmdList.Get(), L"GLR transition raw path-trace target to UAV");
glRaytracingTransition(g_glRaytracingCmd.cmdList.Get(),
pathTraceTexture.resource.Get(),
pathTraceTexture.state,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
pathTraceTexture.state = D3D12_RESOURCE_STATE_UNORDERED_ACCESS;
}
else
{
GLR_PIX_MARKER(g_glRaytracingCmd.cmdList.Get(), L"GLR transition external denoiser output to UAV");
glRaytracingTransition(g_glRaytracingCmd.cmdList.Get(),
pass->outputTexture,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
}
ID3D12DescriptorHeap* heaps[] = { g_glRaytracingLighting.descriptorHeap.Get() };
g_glRaytracingCmd.cmdList->SetDescriptorHeaps(_countof(heaps), heaps);
g_glRaytracingCmd.cmdList->SetComputeRootSignature(g_glRaytracingLighting.globalRootSig.Get());
D3D12_GPU_DESCRIPTOR_HANDLE gpuBase = g_glRaytracingLighting.descriptorHeap->GetGPUDescriptorHandleForHeapStart();
g_glRaytracingCmd.cmdList->SetComputeRootDescriptorTable(0,
glRaytracingOffsetGpu(gpuBase, g_glRaytracingLighting.descriptorStride, GLR_DESC_LIGHTS_SRV));
g_glRaytracingCmd.cmdList->SetComputeRootDescriptorTable(1,
glRaytracingOffsetGpu(gpuBase, g_glRaytracingLighting.descriptorStride, GLR_DESC_PATHTRACE_UAV));
g_glRaytracingCmd.cmdList->SetComputeRootConstantBufferView(2, g_glRaytracingLighting.constantBuffer.gpuVA);
g_glRaytracingCmd.cmdList->SetPipelineState1(g_glRaytracingLighting.rtStateObject.Get());
const UINT timestampBase = g_glRaytracingCmd.cmdCurrentSlot * 2u;
const bool writeTimestamp =
g_glRaytracingCmd.pathTracingTimestampHeap.Get() != nullptr &&
g_glRaytracingCmd.pathTracingTimestampReadback.resource.Get() != nullptr;
if (writeTimestamp)
{
g_glRaytracingCmd.cmdList->EndQuery(
g_glRaytracingCmd.pathTracingTimestampHeap.Get(),
D3D12_QUERY_TYPE_TIMESTAMP,
timestampBase + 0u);
}
const UINT shaderRecordSize = (UINT)glRaytracingAlignUp(
D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES,
D3D12_RAYTRACING_SHADER_RECORD_BYTE_ALIGNMENT);
D3D12_DISPATCH_RAYS_DESC rays = {};
rays.RayGenerationShaderRecord.StartAddress = g_glRaytracingLighting.raygenTable.gpuVA;
rays.RayGenerationShaderRecord.SizeInBytes = shaderRecordSize;
rays.MissShaderTable.StartAddress = g_glRaytracingLighting.missTable.gpuVA;
rays.MissShaderTable.SizeInBytes = shaderRecordSize * 2u;
rays.MissShaderTable.StrideInBytes = shaderRecordSize;
rays.HitGroupTable.StartAddress = g_glRaytracingLighting.hitTable.gpuVA;
rays.HitGroupTable.SizeInBytes = shaderRecordSize * 3u;
rays.HitGroupTable.StrideInBytes = shaderRecordSize;
rays.Width = pass->width;
rays.Height = pass->height;
rays.Depth = 1;
{
GLR_PIX_EVENT(g_glRaytracingCmd.cmdList.Get(), L"GLR DispatchRays RayGen GI");
g_glRaytracingCmd.cmdList->DispatchRays(&rays);
}
D3D12_RESOURCE_BARRIER rawUav = {};
rawUav.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
rawUav.UAV.pResource = rayOutputTexture;
GLR_PIX_MARKER(g_glRaytracingCmd.cmdList.Get(), L"GLR raw ray output UAV barrier");
g_glRaytracingCmd.cmdList->ResourceBarrier(1, &rawUav);
if (useInternalDenoiser)
{
GLR_PIX_EVENT(g_glRaytracingCmd.cmdList.Get(), L"GLR Internal Temporal + A-Trous Denoise");
const UINT groupsX = (pass->width + 7u) / 8u;
const UINT groupsY = (pass->height + 7u) / 8u;
glRaytracingTransition(g_glRaytracingCmd.cmdList.Get(),
pathTraceTexture.resource.Get(),
pathTraceTexture.state,
D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
pathTraceTexture.state = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
glRaytracingTransition(g_glRaytracingCmd.cmdList.Get(),
g_glRaytracingLighting.temporalTexture.resource.Get(),
g_glRaytracingLighting.temporalTexture.state,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
g_glRaytracingLighting.temporalTexture.state = D3D12_RESOURCE_STATE_UNORDERED_ACCESS;
glRaytracingTransition(g_glRaytracingCmd.cmdList.Get(),
g_glRaytracingLighting.historyTexture[historyReadIndex].resource.Get(),
g_glRaytracingLighting.historyTexture[historyReadIndex].state,
D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
g_glRaytracingLighting.historyTexture[historyReadIndex].state = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
glRaytracingTransition(g_glRaytracingCmd.cmdList.Get(),
g_glRaytracingLighting.historyTexture[historyWriteIndex].resource.Get(),
g_glRaytracingLighting.historyTexture[historyWriteIndex].state,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
g_glRaytracingLighting.historyTexture[historyWriteIndex].state = D3D12_RESOURCE_STATE_UNORDERED_ACCESS;
g_glRaytracingCmd.cmdList->SetComputeRootSignature(g_glRaytracingLighting.globalRootSig.Get());
g_glRaytracingCmd.cmdList->SetComputeRootDescriptorTable(0,
glRaytracingOffsetGpu(gpuBase, g_glRaytracingLighting.descriptorStride, GLR_DESC_LIGHTS_SRV));
g_glRaytracingCmd.cmdList->SetComputeRootDescriptorTable(1,
glRaytracingOffsetGpu(gpuBase, g_glRaytracingLighting.descriptorStride, GLR_DESC_PATHTRACE_UAV));
g_glRaytracingCmd.cmdList->SetComputeRootConstantBufferView(2, g_glRaytracingLighting.constantBuffer.gpuVA);
g_glRaytracingCmd.cmdList->SetPipelineState(g_glRaytracingLighting.temporalPSO.Get());
{
GLR_PIX_EVENT(g_glRaytracingCmd.cmdList.Get(), L"GLR Temporal Accumulation CS");
g_glRaytracingCmd.cmdList->Dispatch(groupsX, groupsY, 1);
}
D3D12_RESOURCE_BARRIER temporalUavs[2] = {};
temporalUavs[0].Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
temporalUavs[0].UAV.pResource = g_glRaytracingLighting.temporalTexture.resource.Get();
temporalUavs[1].Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
temporalUavs[1].UAV.pResource = g_glRaytracingLighting.historyTexture[historyWriteIndex].resource.Get();
g_glRaytracingCmd.cmdList->ResourceBarrier(2, temporalUavs);
glRaytracingTransition(g_glRaytracingCmd.cmdList.Get(),
g_glRaytracingLighting.temporalTexture.resource.Get(),
g_glRaytracingLighting.temporalTexture.state,
D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
g_glRaytracingLighting.temporalTexture.state = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
glRaytracingTransition(g_glRaytracingCmd.cmdList.Get(),
g_glRaytracingLighting.historyTexture[historyWriteIndex].resource.Get(),
g_glRaytracingLighting.historyTexture[historyWriteIndex].state,
D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
g_glRaytracingLighting.historyTexture[historyWriteIndex].state = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
for (int i = 0; i < 2; ++i)
{
glRaytracingTransition(g_glRaytracingCmd.cmdList.Get(),
g_glRaytracingLighting.denoiseTemp[i].resource.Get(),
g_glRaytracingLighting.denoiseTemp[i].state,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
g_glRaytracingLighting.denoiseTemp[i].state = D3D12_RESOURCE_STATE_UNORDERED_ACCESS;
}
glRaytracingTransition(g_glRaytracingCmd.cmdList.Get(),
pass->outputTexture,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
g_glRaytracingCmd.cmdList->SetComputeRootSignature(g_glRaytracingLighting.globalRootSig.Get());
g_glRaytracingCmd.cmdList->SetComputeRootDescriptorTable(0,
glRaytracingOffsetGpu(gpuBase, g_glRaytracingLighting.descriptorStride, GLR_DESC_LIGHTS_SRV));
g_glRaytracingCmd.cmdList->SetComputeRootDescriptorTable(1,
glRaytracingOffsetGpu(gpuBase, g_glRaytracingLighting.descriptorStride, GLR_DESC_PATHTRACE_UAV));
g_glRaytracingCmd.cmdList->SetPipelineState(g_glRaytracingLighting.denoisePSO.Get());
// Pass 0: temporally accumulated path trace -> temp A.
g_glRaytracingCmd.cmdList->SetComputeRootConstantBufferView(2, g_glRaytracingLighting.denoiseConstantBuffer[0].gpuVA);
{
GLR_PIX_EVENT(g_glRaytracingCmd.cmdList.Get(), L"GLR A-Trous Denoise Pass 0");
g_glRaytracingCmd.cmdList->Dispatch(groupsX, groupsY, 1);
}
D3D12_RESOURCE_BARRIER uavA = {};
uavA.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
uavA.UAV.pResource = g_glRaytracingLighting.denoiseTemp[0].resource.Get();
g_glRaytracingCmd.cmdList->ResourceBarrier(1, &uavA);
glRaytracingTransition(g_glRaytracingCmd.cmdList.Get(),
g_glRaytracingLighting.denoiseTemp[0].resource.Get(),
g_glRaytracingLighting.denoiseTemp[0].state,
D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
g_glRaytracingLighting.denoiseTemp[0].state = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
// Pass 1: temp A -> temp B.
g_glRaytracingCmd.cmdList->SetComputeRootConstantBufferView(2, g_glRaytracingLighting.denoiseConstantBuffer[1].gpuVA);
{
GLR_PIX_EVENT(g_glRaytracingCmd.cmdList.Get(), L"GLR A-Trous Denoise Pass 1");
g_glRaytracingCmd.cmdList->Dispatch(groupsX, groupsY, 1);
}
D3D12_RESOURCE_BARRIER uavB = {};
uavB.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
uavB.UAV.pResource = g_glRaytracingLighting.denoiseTemp[1].resource.Get();
g_glRaytracingCmd.cmdList->ResourceBarrier(1, &uavB);
glRaytracingTransition(g_glRaytracingCmd.cmdList.Get(),
g_glRaytracingLighting.denoiseTemp[1].resource.Get(),
g_glRaytracingLighting.denoiseTemp[1].state,
D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
g_glRaytracingLighting.denoiseTemp[1].state = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
// Pass 2: temp B -> final output.
g_glRaytracingCmd.cmdList->SetComputeRootConstantBufferView(2, g_glRaytracingLighting.denoiseConstantBuffer[2].gpuVA);
{
GLR_PIX_EVENT(g_glRaytracingCmd.cmdList.Get(), L"GLR A-Trous Denoise Pass 2");
g_glRaytracingCmd.cmdList->Dispatch(groupsX, groupsY, 1);
}
D3D12_RESOURCE_BARRIER outputUav = {};
outputUav.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
outputUav.UAV.pResource = pass->outputTexture;
g_glRaytracingCmd.cmdList->ResourceBarrier(1, &outputUav);
}
glRaytracingTransition(g_glRaytracingCmd.cmdList.Get(),
pass->outputTexture,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
if (writeTimestamp)
{
GLR_PIX_MARKER(g_glRaytracingCmd.cmdList.Get(), L"GLR resolve path tracing timestamp");
g_glRaytracingCmd.cmdList->EndQuery(
g_glRaytracingCmd.pathTracingTimestampHeap.Get(),
D3D12_QUERY_TYPE_TIMESTAMP,
timestampBase + 1u);
g_glRaytracingCmd.cmdList->ResolveQueryData(
g_glRaytracingCmd.pathTracingTimestampHeap.Get(),
D3D12_QUERY_TYPE_TIMESTAMP,
timestampBase,
2,
g_glRaytracingCmd.pathTracingTimestampReadback.resource.Get(),
sizeof(UINT64) * timestampBase);
}
glRaytracingEndPixEvent(g_glRaytracingCmd.cmdList.Get());
if (!glRaytracingEndCmd())
return false;
if (writeTimestamp)
g_glRaytracingCmd.pathTracingTimestampFenceRing[g_glRaytracingCmd.cmdCurrentSlot] = g_glRaytracingCmd.cmdLastFenceValue;
if (useInternalDenoiser)
{
g_glRaytracingLighting.currentHistoryIndex = historyWriteIndex;
g_glRaytracingLighting.currentPathTraceIndex = (pathTraceIndex ^ 1u);
}
++g_glRaytracingLighting.frameCounter;
return true;
}
static ID3D12Resource* glRaytracingResolveTLASForWorld(glRaytracingSceneHandle_t worldHandle)
{
glRaytracingRenderWorld_t* world = glRaytracingFindWorld(worldHandle);
if (!world)
return nullptr;
if (!glRaytracingBuildDirtyMeshesInternal())
return nullptr;
if (!glRaytracingBuildSceneInternal(world))
return nullptr;
if (!world->tlasBuilt)
return nullptr;
return glRaytracingGetCurrentTLASBuffer(world)->resource.Get();
}
bool glRaytracingLightingInit(void)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (g_glRaytracingLighting.initialized)
return true;
if (!glRaytracingInitCmdContext())
return false;
if (!glRaytracingLightingCreateDescriptorHeap())
return false;
if (!glRaytracingLightingCreateRootSignatures())
return false;
if (!glRaytracingLightingCreateBuffers())
return false;
glRaytracingLightingCreatePersistentLightSRV();
if (!glRaytracingLightingCreateStateObject())
return false;
if (!glRaytracingLightingCreateShaderTables())
return false;
if (!glRaytracingLightingCreateDenoisePipeline())
return false;
if (!glRaytracingLightingCreateTemporalPipeline())
return false;
memset(&g_glRaytracingLighting.constants, 0, sizeof(g_glRaytracingLighting.constants));
g_glRaytracingLighting.cpuLights.reserve(GL_RAYTRACING_MAX_LIGHTS);
glRaytracingSetIdentity4x4(g_glRaytracingLighting.constants.invViewProj);
glRaytracingSetIdentity4x4(g_glRaytracingLighting.constants.invViewMatrix);
glRaytracingSetIdentity4x4(g_glRaytracingLighting.constants.viewProj);
g_glRaytracingLighting.constants.ambientColor[0] = 0.08f;
g_glRaytracingLighting.constants.ambientColor[1] = 0.08f;
g_glRaytracingLighting.constants.ambientColor[2] = 0.09f;
g_glRaytracingLighting.constants.ambientColor[3] = 1.0f;
g_glRaytracingLighting.constants.enableSpecular = 1;
g_glRaytracingLighting.constants.enableHalfLambert = 1;
g_glRaytracingLighting.constants.normalReconstructZ = 1.0f;
g_glRaytracingLighting.constants.shadowBias = 1.5f;
g_glRaytracingLighting.constants.frameIndex = 0;
g_glRaytracingLighting.constants.samplesPerPixel = 1;
g_glRaytracingLighting.constants.maxBounces = 2;
g_glRaytracingLighting.constants.enableDenoiser = 1;
g_glRaytracingLighting.constants.denoisePassIndex = 0;
g_glRaytracingLighting.constants.denoiseStepWidth = 1.0f;
g_glRaytracingLighting.constants.denoiseStrength = 1.0f;
g_glRaytracingLighting.constants.denoisePhiColor = 8.0f;
g_glRaytracingLighting.constants.denoisePhiNormal = 64.0f;
g_glRaytracingLighting.constants.denoisePhiPosition = 0.045f;
g_glRaytracingLighting.constants.bumpStrength = 2.35f;
g_glRaytracingLighting.constants.secondaryLightBudget = 0;
g_glRaytracingLighting.constants.secondaryCorrectionBudget = 0;
g_glRaytracingLighting.constants.secondaryLightCompensation = 0.62f;
g_glRaytracingLighting.constants.historyReset = 1u;
g_glRaytracingLighting.constants.externalDenoiser = 0u;
g_glRaytracingLighting.constants.giAmount = 1.0f;
g_glRaytracingLighting.constants.worldGrimeAmount = 0.35f;
glRaytracingLightingResetDenoiseHistory();
glRaytracingLightingUpdateConstants();
g_glRaytracingLighting.initialized = true;
glRaytracingLog("glRaytracingLightingInit ok");
return true;
}
void glRaytracingLightingShutdown(void)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingLighting.initialized)
return;
glRaytracingWaitIdle();
glRaytracingLightingUnmapUploadBuffers();
g_glRaytracingLighting = glRaytracingLightingState_t();
}
bool glRaytracingLightingIsInitialized(void)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
return g_glRaytracingLighting.initialized;
}
void glRaytracingLightingClearLights(bool clearPersistant)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (clearPersistant)
{
g_glRaytracingLighting.cpuLights.clear();
}
else
{
size_t writeIndex = 0;
for (size_t i = 0; i < g_glRaytracingLighting.cpuLights.size(); ++i)
{
if (g_glRaytracingLighting.cpuLights[i].persistant)
{
if (writeIndex != i)
{
g_glRaytracingLighting.cpuLights[writeIndex] = g_glRaytracingLighting.cpuLights[i];
}
++writeIndex;
}
}
g_glRaytracingLighting.cpuLights.resize(writeIndex);
}
g_glRaytracingLighting.constants.lightCount =
(uint32_t)g_glRaytracingLighting.cpuLights.size();
glRaytracingLightingResetDenoiseHistory();
glRaytracingLightingUpdateConstants();
}
bool glRaytracingLightingAddLight(const glRaytracingLight_t* light)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingLighting.initialized || !light)
return false;
if (g_glRaytracingLighting.cpuLights.size() >= GL_RAYTRACING_MAX_LIGHTS)
return false;
g_glRaytracingLighting.cpuLights.push_back(*light);
g_glRaytracingLighting.constants.lightCount = (uint32_t)g_glRaytracingLighting.cpuLights.size();
glRaytracingLightingResetDenoiseHistory();
glRaytracingLightingUpdateLights();
glRaytracingLightingUpdateConstants();
return true;
}
void glRaytracingLightingSetAmbient(float r, float g, float b, float intensity)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (g_glRaytracingLighting.constants.ambientColor[0] != r ||
g_glRaytracingLighting.constants.ambientColor[1] != g ||
g_glRaytracingLighting.constants.ambientColor[2] != b ||
g_glRaytracingLighting.constants.ambientColor[3] != intensity)
{
glRaytracingLightingResetDenoiseHistory();
}
g_glRaytracingLighting.constants.ambientColor[0] = r;
g_glRaytracingLighting.constants.ambientColor[1] = g;
g_glRaytracingLighting.constants.ambientColor[2] = b;
g_glRaytracingLighting.constants.ambientColor[3] = intensity;
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingSetCameraPosition(float x, float y, float z)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
g_glRaytracingLighting.constants.cameraPos[0] = x;
g_glRaytracingLighting.constants.cameraPos[1] = y;
g_glRaytracingLighting.constants.cameraPos[2] = z;
g_glRaytracingLighting.constants.cameraPos[3] = 1.0f;
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingSetInvViewProjMatrix(const float* m16)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!m16)
return;
memcpy(g_glRaytracingLighting.constants.invViewProj, m16, sizeof(float) * 16);
if (!glRaytracingInvertMatrix4x4(m16, g_glRaytracingLighting.constants.viewProj))
glRaytracingSetIdentity4x4(g_glRaytracingLighting.constants.viewProj);
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingSetInvViewMatrix(const float* m16)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!m16)
return;
memcpy(g_glRaytracingLighting.constants.invViewMatrix, m16, sizeof(float) * 16);
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingSetHistoryReset(int reset)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
g_glRaytracingLighting.constants.historyReset = reset ? 1u : 0u;
if (reset)
glRaytracingLightingResetDenoiseHistory();
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingSetNormalReconstructSign(float signValue)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (g_glRaytracingLighting.constants.normalReconstructZ != signValue)
glRaytracingLightingResetDenoiseHistory();
g_glRaytracingLighting.constants.normalReconstructZ = signValue;
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingSetBumpStrength(float strength)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
strength = glRaytracingClamp<float>(strength, 0.0f, 8.0f);
if (g_glRaytracingLighting.constants.bumpStrength != strength)
glRaytracingLightingResetDenoiseHistory();
g_glRaytracingLighting.constants.bumpStrength = strength;
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingEnableSpecular(int enable)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
uint32_t value = enable ? 1u : 0u;
if (g_glRaytracingLighting.constants.enableSpecular != value)
glRaytracingLightingResetDenoiseHistory();
g_glRaytracingLighting.constants.enableSpecular = value;
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingEnableHalfLambert(int enable)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
uint32_t value = enable ? 1u : 0u;
if (g_glRaytracingLighting.constants.enableHalfLambert != value)
glRaytracingLightingResetDenoiseHistory();
g_glRaytracingLighting.constants.enableHalfLambert = value;
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingSetShadowBias(float bias)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (g_glRaytracingLighting.constants.shadowBias != bias)
glRaytracingLightingResetDenoiseHistory();
g_glRaytracingLighting.constants.shadowBias = bias;
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingSetPathTracingOptions(uint32_t samplesPerPixel, uint32_t maxBounces, int enableDenoiser, float denoiseStrength)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
samplesPerPixel = glRaytracingClamp<uint32_t>(samplesPerPixel ? samplesPerPixel : 1u, 1u, 8u);
maxBounces = glRaytracingClamp<uint32_t>(maxBounces ? maxBounces : 1u, 1u, 4u);
uint32_t denoiser = enableDenoiser ? 1u : 0u;
denoiseStrength = glRaytracingClamp<float>(denoiseStrength, 0.0f, 1.0f);
if (g_glRaytracingLighting.constants.samplesPerPixel != samplesPerPixel ||
g_glRaytracingLighting.constants.maxBounces != maxBounces ||
g_glRaytracingLighting.constants.enableDenoiser != denoiser ||
g_glRaytracingLighting.constants.denoiseStrength != denoiseStrength)
{
glRaytracingLightingResetDenoiseHistory();
}
g_glRaytracingLighting.constants.samplesPerPixel = samplesPerPixel;
g_glRaytracingLighting.constants.maxBounces = maxBounces;
g_glRaytracingLighting.constants.enableDenoiser = denoiser;
g_glRaytracingLighting.constants.denoiseStrength = denoiseStrength;
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingSetGIAmount(float amount)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
amount = glRaytracingClamp<float>(amount, 0.0f, 8.0f);
if (g_glRaytracingLighting.constants.giAmount != amount)
glRaytracingLightingResetDenoiseHistory();
g_glRaytracingLighting.constants.giAmount = amount;
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingSetWorldGrime(float amount)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
amount = glRaytracingClamp<float>(amount, 0.0f, 4.0f);
if (g_glRaytracingLighting.constants.worldGrimeAmount != amount)
glRaytracingLightingResetDenoiseHistory();
g_glRaytracingLighting.constants.worldGrimeAmount = amount;
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingSetSecondaryLightBudget(int budget, float compensation)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (budget < -1)
budget = -1;
if (budget > (int)GL_RAYTRACING_MAX_LIGHTS)
budget = (int)GL_RAYTRACING_MAX_LIGHTS;
compensation = glRaytracingClamp<float>(compensation, 0.0f, 1.0f);
if (g_glRaytracingLighting.constants.secondaryLightBudget != budget ||
g_glRaytracingLighting.constants.secondaryLightCompensation != compensation)
{
glRaytracingLightingResetDenoiseHistory();
}
g_glRaytracingLighting.constants.secondaryLightBudget = budget;
g_glRaytracingLighting.constants.secondaryCorrectionBudget = 0;
g_glRaytracingLighting.constants.secondaryLightCompensation = compensation;
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingSetDenoiseTuning(float phiColor, float phiNormal, float phiPosition)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
phiColor = glRaytracingClamp<float>(phiColor, 0.001f, 64.0f);
phiNormal = glRaytracingClamp<float>(phiNormal, 1.0f, 128.0f);
phiPosition = glRaytracingClamp<float>(phiPosition, 0.001f, 4.0f);
if (g_glRaytracingLighting.constants.denoisePhiColor != phiColor ||
g_glRaytracingLighting.constants.denoisePhiNormal != phiNormal ||
g_glRaytracingLighting.constants.denoisePhiPosition != phiPosition)
{
glRaytracingLightingResetDenoiseHistory();
}
g_glRaytracingLighting.constants.denoisePhiColor = phiColor;
g_glRaytracingLighting.constants.denoisePhiNormal = phiNormal;
g_glRaytracingLighting.constants.denoisePhiPosition = phiPosition;
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingSetVolumetricScattering(glRaytracingLight_t* light, float strength)
{
if (!light)
return;
// This uses glRaytracingLight_t::pad1, which is renamed to
// Light::volumetricScattering in HLSL. Keeping the existing pad slot avoids
// changing the StructuredBuffer stride for already-integrated callers.
light->pad1 = glRaytracingClamp<float>(strength, 0.0f, 16.0f);
}
void glRaytracingLightingSetExternalDenoiser(int enabled)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
const bool newValue = enabled ? true : false;
if (g_glRaytracingLighting.externalDenoiser != newValue)
glRaytracingLightingResetDenoiseHistory();
g_glRaytracingLighting.externalDenoiser = newValue;
g_glRaytracingLighting.constants.externalDenoiser = newValue ? 1u : 0u;
glRaytracingLightingUpdateConstants();
}
void glRaytracingLightingUseExternalDenoiser(int enabled)
{
glRaytracingLightingSetExternalDenoiser(enabled);
}
void glRaytracingLightingSetEmissiveInput(ID3D12Resource* texture, DXGI_FORMAT format)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
g_glRaytracingLighting.emissiveTexture = texture;
g_glRaytracingLighting.emissiveFormat = (format == DXGI_FORMAT_UNKNOWN)
? DXGI_FORMAT_R16G16B16A16_FLOAT
: format;
}
void glRaytracingLightingSetSpecularInput(ID3D12Resource* texture, DXGI_FORMAT format)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
DXGI_FORMAT newFormat = (format == DXGI_FORMAT_UNKNOWN)
? DXGI_FORMAT_R8G8B8A8_UNORM
: format;
if (g_glRaytracingLighting.specularTexture != texture ||
g_glRaytracingLighting.specularFormat != newFormat)
{
glRaytracingLightingResetDenoiseHistory();
}
g_glRaytracingLighting.specularTexture = texture;
g_glRaytracingLighting.specularFormat = newFormat;
}
bool glRaytracingLightingExecuteForScene(const glRaytracingLightingPassDesc_t* pass, glRaytracingSceneHandle_t worldHandle)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
ID3D12Resource* topLevelAS = glRaytracingResolveTLASForWorld(worldHandle);
if (!topLevelAS)
return false;
const glRaytracingRenderWorld_t* world = glRaytracingFindWorldConst(worldHandle);
g_glRaytracingLighting.lastProfileSceneHandle = worldHandle;
g_glRaytracingLighting.lastProfileInstanceCount = glRaytracingCountAliveInstances(world);
g_glRaytracingLighting.lastProfileWidth = pass ? pass->width : 0;
g_glRaytracingLighting.lastProfileHeight = pass ? pass->height : 0;
return glRaytracingLightingExecuteInternal(pass, topLevelAS);
}
glRaytracingLight_t glRaytracingLightingMakePointLight(
float px, float py, float pz,
float radiusX, float radiusY, float radiusZ,
float r, float g, float b,
float intensity)
{
glRaytracingLight_t l = {};
float ax = (radiusX < 0.0f) ? -radiusX : radiusX;
float ay = (radiusY < 0.0f) ? -radiusY : radiusY;
float az = (radiusZ < 0.0f) ? -radiusZ : radiusZ;
float maxRadius = ax;
if (ay > maxRadius) maxRadius = ay;
if (az > maxRadius) maxRadius = az;
if (maxRadius <= 0.0f) maxRadius = 1e-4f;
if (ax <= 0.0f) ax = maxRadius;
if (ay <= 0.0f) ay = maxRadius;
if (az <= 0.0f) az = maxRadius;
l.position.x = px;
l.position.y = py;
l.position.z = pz;
// Keep radius populated as a scalar fallback/max range, but point lights now
// attenuate using pointRadius.x/y/z in the ray generation shader.
l.radius = maxRadius;
l.pointRadius.x = ax;
l.pointRadius.y = ay;
l.pointRadius.z = az;
l.pointRadiusPad = 0.0f;
l.color.x = r;
l.color.y = g;
l.color.z = b;
l.intensity = intensity;
l.normal.x = 0.0f;
l.normal.y = 0.0f;
l.normal.z = 1.0f;
l.type = GL_RAYTRACING_LIGHT_TYPE_POINT;
l.axisU.x = 1.0f;
l.axisU.y = 0.0f;
l.axisU.z = 0.0f;
l.halfWidth = 0.0f;
l.axisV.x = 0.0f;
l.axisV.y = 1.0f;
l.axisV.z = 0.0f;
l.halfHeight = 0.0f;
l.samples = 1;
l.twoSided = 0;
l.persistant = 0.0f;
l.pad1 = 0.0f; // volumetric scattering disabled by default.
l.areaNum = -1;
return l;
}
glRaytracingLight_t glRaytracingLightingMakeSpotLight(
float px, float py, float pz,
float dx, float dy, float dz,
float ux, float uy, float uz,
float vx, float vy, float vz,
float nearPlane,
float farPlane,
float tanHalfWidth,
float tanHalfHeight,
float r, float g, float b,
float intensity,
uint32_t samples)
{
glRaytracingLight_t l = {};
glRaytracingNormalize3(dx, dy, dz);
// Make U perpendicular to D.
{
const float du = dx * ux + dy * uy + dz * uz;
ux -= dx * du;
uy -= dy * du;
uz -= dz * du;
const float uLenSq = ux * ux + uy * uy + uz * uz;
if (uLenSq <= 1e-20f)
{
const float absDz = (dz < 0.0f) ? -dz : dz;
if (absDz < 0.999f)
{
glRaytracingCross3(0.0f, 0.0f, 1.0f, dx, dy, dz, ux, uy, uz);
}
else
{
glRaytracingCross3(0.0f, 1.0f, 0.0f, dx, dy, dz, ux, uy, uz);
}
}
glRaytracingNormalize3(ux, uy, uz);
}
// Rebuild V from D x U so the basis is orthonormal, while preserving the
// sign of the caller-provided V whenever possible.
{
float builtVx, builtVy, builtVz;
glRaytracingCross3(dx, dy, dz, ux, uy, uz, builtVx, builtVy, builtVz);
glRaytracingNormalize3(builtVx, builtVy, builtVz);
const float sign = builtVx * vx + builtVy * vy + builtVz * vz;
if (sign < 0.0f)
{
builtVx = -builtVx;
builtVy = -builtVy;
builtVz = -builtVz;
}
vx = builtVx;
vy = builtVy;
vz = builtVz;
}
if (nearPlane < 0.0f)
nearPlane = 0.0f;
if (farPlane <= nearPlane)
farPlane = nearPlane + 1e-3f;
if (tanHalfWidth < 0.0f) tanHalfWidth = -tanHalfWidth;
if (tanHalfHeight < 0.0f) tanHalfHeight = -tanHalfHeight;
if (tanHalfWidth <= 1e-4f)
tanHalfWidth = 1e-4f;
if (tanHalfHeight <= 1e-4f)
tanHalfHeight = 1e-4f;
l.position.x = px;
l.position.y = py;
l.position.z = pz;
// For spot lights, radius stores the far clip distance while pointRadius.x
// stores the near clip distance.
l.radius = farPlane;
l.pointRadius.x = nearPlane;
l.pointRadius.y = 0.0f;
l.pointRadius.z = 0.0f;
l.pointRadiusPad = 0.0f;
l.color.x = r;
l.color.y = g;
l.color.z = b;
l.intensity = intensity;
l.normal.x = dx;
l.normal.y = dy;
l.normal.z = dz;
l.type = GL_RAYTRACING_LIGHT_TYPE_SPOT;
l.axisU.x = ux;
l.axisU.y = uy;
l.axisU.z = uz;
l.halfWidth = tanHalfWidth;
l.axisV.x = vx;
l.axisV.y = vy;
l.axisV.z = vz;
l.halfHeight = tanHalfHeight;
l.samples = samples ? samples : 1u;
l.twoSided = 0;
l.persistant = 0.0f;
l.pad1 = 0.0f; // volumetric scattering disabled by default.
l.areaNum = -1;
return l;
}
glRaytracingLight_t glRaytracingLightingMakeRectLight(
float px, float py, float pz,
float nx, float ny, float nz,
float ux, float uy, float uz,
float vx, float vy, float vz,
float halfWidth, float halfHeight,
float r, float g, float b,
float intensity,
uint32_t samples,
uint32_t twoSided)
{
glRaytracingLight_t l = {};
glRaytracingNormalize3(nx, ny, nz);
glRaytracingNormalize3(ux, uy, uz);
glRaytracingNormalize3(vx, vy, vz);
if ((nx == 0.0f && ny == 0.0f && nz == 0.0f) &&
!((ux == 0.0f && uy == 0.0f && uz == 0.0f) ||
(vx == 0.0f && vy == 0.0f && vz == 0.0f)))
{
glRaytracingCross3(ux, uy, uz, vx, vy, vz, nx, ny, nz);
glRaytracingNormalize3(nx, ny, nz);
}
l.position.x = px;
l.position.y = py;
l.position.z = pz;
// Reuse radius as influence/falloff range for the rect light.
l.radius = (halfWidth > halfHeight ? halfWidth : halfHeight) * 6.0f;
l.pointRadius.x = l.radius;
l.pointRadius.y = l.radius;
l.pointRadius.z = l.radius;
l.pointRadiusPad = 0.0f;
l.color.x = r;
l.color.y = g;
l.color.z = b;
l.intensity = intensity;
l.normal.x = nx;
l.normal.y = ny;
l.normal.z = nz;
l.type = GL_RAYTRACING_LIGHT_TYPE_RECT;
l.axisU.x = ux;
l.axisU.y = uy;
l.axisU.z = uz;
l.halfWidth = halfWidth;
l.axisV.x = vx;
l.axisV.y = vy;
l.axisV.z = vz;
l.halfHeight = halfHeight;
l.samples = samples ? samples : 4u;
l.twoSided = twoSided ? 1u : 0u;
l.persistant = 0.0f;
l.pad1 = 0.0f; // volumetric scattering disabled by default.
l.areaNum = -1;
return l;
}
uint32_t glRaytracingLightingGetLightCount(void)
{
return (uint32_t)g_glRaytracingLighting.cpuLights.size();
}
bool glRaytracingLightingGetProfileCounters(glRaytracingProfileCounters_t* counters)
{
if (!counters)
return false;
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
memset(counters, 0, sizeof(*counters));
const glRaytracingLightingConstants_t& constants = g_glRaytracingLighting.constants;
const uint32_t lightCount = (uint32_t)g_glRaytracingLighting.cpuLights.size();
const uint32_t samplesPerPixel = constants.samplesPerPixel ? constants.samplesPerPixel : 1u;
const uint32_t maxBounces = constants.maxBounces ? constants.maxBounces : 1u;
uint32_t directLightSamples = 0;
uint32_t directShadowRays = 0;
uint32_t volumetricShadowRays = 0;
for (size_t i = 0; i < g_glRaytracingLighting.cpuLights.size(); ++i)
{
const glRaytracingLight_t& light = g_glRaytracingLighting.cpuLights[i];
const uint32_t sampleCount = glRaytracingClamp<uint32_t>(light.samples ? light.samples : 1u, 1u, 4u);
if (light.type == GL_RAYTRACING_LIGHT_TYPE_POINT)
++counters->pointLightCount;
else if (light.type == GL_RAYTRACING_LIGHT_TYPE_SPOT)
++counters->spotLightCount;
else if (light.type == GL_RAYTRACING_LIGHT_TYPE_RECT)
++counters->rectLightCount;
directLightSamples += sampleCount;
if (light.samples != 0u)
{
++counters->shadowCastingLightCount;
directShadowRays += sampleCount;
}
if (light.volumetricScattering > 0.0f &&
(light.type == GL_RAYTRACING_LIGHT_TYPE_POINT || light.type == GL_RAYTRACING_LIGHT_TYPE_SPOT))
{
++counters->volumetricLightCount;
volumetricShadowRays += glRaytracingClamp<uint32_t>(light.samples ? light.samples : 3u, 3u, 6u);
}
}
uint32_t meshCount = 0;
for (size_t i = 0; i < g_glRaytracingScene.meshes.size(); ++i)
{
if (g_glRaytracingScene.meshes[i].alive)
++meshCount;
}
uint32_t indirectDepth = 0;
if (maxBounces > 1u)
{
indirectDepth = glRaytracingClamp<uint32_t>(maxBounces - 1u, 0u, 3u);
if (samplesPerPixel <= 1u)
indirectDepth = glRaytracingClamp<uint32_t>(indirectDepth, 0u, 2u);
}
uint32_t correctionBudget = 0;
if (lightCount > 0u)
{
if (constants.secondaryCorrectionBudget > 0u)
{
correctionBudget = glRaytracingClamp<uint32_t>(constants.secondaryCorrectionBudget, 0u, lightCount);
}
else
{
correctionBudget = (lightCount <= 2u) ? 1u : 0u;
if (samplesPerPixel >= 4u)
correctionBudget = glRaytracingClamp<uint32_t>(std::max<uint32_t>(correctionBudget, 1u), 0u, lightCount);
if (samplesPerPixel >= 6u && lightCount <= 4u)
correctionBudget = glRaytracingClamp<uint32_t>(correctionBudget + 1u, 0u, lightCount);
}
}
uint32_t fastBounceLightBudget = lightCount;
if (constants.secondaryLightBudget < 0)
{
fastBounceLightBudget = lightCount;
}
else if (constants.secondaryLightBudget > 0)
{
fastBounceLightBudget = glRaytracingClamp<uint32_t>((uint32_t)constants.secondaryLightBudget, 0u, lightCount);
}
else
{
if (lightCount > 16u)
fastBounceLightBudget = 8u;
else if (lightCount > 10u)
fastBounceLightBudget = 7u;
else if (lightCount > 6u)
fastBounceLightBudget = 6u;
}
uint32_t gatherSamples = 0;
if (maxBounces > 1u)
{
gatherSamples = 3u;
if (lightCount > 4u)
gatherSamples = 2u;
if (lightCount > 10u)
gatherSamples = 1u;
if (samplesPerPixel >= 4u)
gatherSamples = glRaytracingClamp<uint32_t>(gatherSamples, 0u, 2u);
if (samplesPerPixel >= 6u)
gatherSamples = glRaytracingClamp<uint32_t>(gatherSamples, 0u, 1u);
}
counters->frameCounter = g_glRaytracingLighting.frameCounter;
counters->width = g_glRaytracingLighting.lastProfileWidth;
counters->height = g_glRaytracingLighting.lastProfileHeight;
counters->lightCount = lightCount;
counters->samplesPerPixel = samplesPerPixel;
counters->maxBounces = maxBounces;
counters->enableSpecular = constants.enableSpecular;
counters->enableDenoiser = constants.enableDenoiser;
counters->meshCount = meshCount;
counters->instanceCount = g_glRaytracingLighting.lastProfileInstanceCount;
counters->directLightSamplesPerPixel = directLightSamples;
counters->directShadowRaysPerPixel = directShadowRays;
counters->indirectBounceRaysPerPixel = samplesPerPixel * indirectDepth;
counters->indirectLightEvalsPerPixel = samplesPerPixel * indirectDepth * (fastBounceLightBudget + correctionBudget);
counters->finalGatherRaysPerPixel = gatherSamples;
counters->reflectionRaysPerPixel = (constants.enableSpecular != 0u && maxBounces > 1u) ? 1u : 0u;
counters->volumetricShadowRaysPerPixel = volumetricShadowRays;
counters->gpuMsec = com_pathTracingGpuMsec;
return true;
}
int glRaytracingSetInstanceVisibilityUnlocked(
glRaytracingRenderWorld_t* world,
glRaytracingInstanceHandle_t instanceHandle,
int visible)
{
if (!world || !world->alive)
return 0;
glRaytracingInstanceRecord_t* inst =
glRaytracingFindInstance(world, instanceHandle);
if (!inst || !inst->alive)
return 0;
const uint32_t newMask = visible ? 0xFFu : 0u;
if ((((uint32_t)inst->descCpu.mask) & 0xFFu) == newMask)
return 1;
inst->descCpu.mask = newMask;
inst->dirty = 1;
glRaytracingMarkWorldNeedsUpdate(world);
return 1;
}
static void glRaytracingSetAllInstancesVisibleUnlocked(
glRaytracingRenderWorld_t* world,
int visible)
{
if (!world || !world->alive)
return;
const uint32_t newMask = visible ? 0xFFu : 0u;
int changed = 0;
for (size_t i = 0; i < world->instances.size(); ++i)
{
glRaytracingInstanceRecord_t& inst = world->instances[i];
if (!inst.alive)
continue;
const int wasVisible = inst.descCpu.mask != 0;
if (wasVisible == (visible != 0))
continue;
inst.descCpu.mask = newMask;
inst.dirty = 1;
changed = 1;
}
if (changed)
glRaytracingMarkWorldNeedsUpdate(world);
}
int glRaytracingSetInstanceVisibilityInScene(
glRaytracingSceneHandle_t sceneHandle,
glRaytracingInstanceHandle_t instanceHandle,
int visible)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized)
return 0;
glRaytracingRenderWorld_t* world = glRaytracingFindWorld(sceneHandle);
return glRaytracingSetInstanceVisibilityUnlocked(
world,
instanceHandle,
visible ? 1 : 0);
}
int glRaytracingGetInstanceVisibilityInScene(
glRaytracingSceneHandle_t sceneHandle,
glRaytracingInstanceHandle_t instanceHandle)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized)
return 0;
glRaytracingRenderWorld_t* world = glRaytracingFindWorld(sceneHandle);
if (!world)
return 0;
glRaytracingInstanceRecord_t* inst =
glRaytracingFindInstance(world, instanceHandle);
if (!inst || !inst->alive)
return 0;
return inst->descCpu.mask != 0 ? 1 : 0;
}
void glRaytracingHideAllInstancesInScene(glRaytracingSceneHandle_t sceneHandle)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized)
return;
glRaytracingRenderWorld_t* world = glRaytracingFindWorld(sceneHandle);
glRaytracingSetAllInstancesVisibleUnlocked(world, 0);
}
void glRaytracingShowAllInstancesInScene(glRaytracingSceneHandle_t sceneHandle)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized)
return;
glRaytracingRenderWorld_t* world = glRaytracingFindWorld(sceneHandle);
glRaytracingSetAllInstancesVisibleUnlocked(world, 1);
}
void glRaytracingHideAllInstances(void)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized)
return;
for (int i = 0; i < GL_RAYTRACING_MAX_RENDER_WORLDS; ++i)
glRaytracingSetAllInstancesVisibleUnlocked(&g_glRaytracingScene.worlds[i], 0);
}
void glRaytracingShowAllInstances(void)
{
std::lock_guard<std::mutex> lock(g_glRaytracingMutex);
if (!g_glRaytracingScene.initialized)
return;
for (int i = 0; i < GL_RAYTRACING_MAX_RENDER_WORLDS; ++i)
glRaytracingSetAllInstancesVisibleUnlocked(&g_glRaytracingScene.worlds[i], 1);
}