光线生成着色器中同时访问同一像素是否安全及相关问题
背景代码
光线生成着色器:
cbuffer GlobalParams : register(b0) { float tau; uint width; uint height; uint seed; }; RWTexture2D<float3> output : register(u0); [shader("raygeneration")] void foo() { for (;;) { float2 const raster_point = float2(rand(seed) * width, rand(seed) * height); float3 radiance; // 追踪从raster_point出发的路径并将结果存入radiance tau += /* 随机计算得到的值 */; output[uint2(raster_point)] += radiance; if (/* 满足退出条件 */) break; } }
归一化用计算着色器:
cbuffer GlobalParams : register(b0) { float tau; uint width; uint height; }; RWTexture2D<float3> output : register(u0); [shader("compute")] void bar() { for (uint i = 0; i < height; ++i) { for (uint j = 0; j < width; ++j) output[uint(i, j)] /= tau; } }
疑问解答
1. 如何在调用foo前将output清零?
两种可行方案:
方案一:使用
ClearUnorderedAccessViewFloat
确保UAV创建时设置D3D11_UAV_FLAG_ALLOW_UNORDERED_ACCESS,且资源状态处于D3D11_RESOURCE_STATE_UNORDERED_ACCESS。调用参数示例:float clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f}; deviceContext->ClearUnorderedAccessViewFloat(uavOutput, clearColor);若出现问题,大概率是资源状态不匹配或UAV创建参数错误,可先切换资源状态到
UNORDERED_ACCESS再执行清除。方案二:计算着色器清零
兼容性更强的方案,适合特殊格式纹理。编写简单计算着色器:cbuffer GlobalParams : register(b0) { uint width; uint height; }; RWTexture2D<float3> output : register(u0); [shader("compute")] [numthreads(8, 8, 1)] void ClearOutput() { uint2 pixel = DispatchThreadID.xy; if (pixel.x < width && pixel.y < height) { output[pixel] = float3(0.0f, 0.0f, 0.0f); } }CPU端调用时计算线程组数:
UINT groupX = (width + 7) / 8; UINT groupY = (height + 7) / 8; deviceContext->Dispatch(groupX, groupY, 1);
2. 多个foo线程修改同一像素是否安全?
不安全,普通的output[pixel] += radiance是非原子操作,多线程同时写入会导致数据竞争,结果不可预测。两种解决方式:
方案一:原子操作
对float3的每个分量单独使用InterlockedAdd(HLSL仅支持单分量原子加法):uint2 pixel = uint2(raster_point); InterlockedAdd(output[pixel].x, radiance.x); InterlockedAdd(output[pixel].y, radiance.y); InterlockedAdd(output[pixel].z, radiance.z);适合线程数不多、像素竞争不频繁的场景,缺点是原子操作有性能开销。
方案二:纹理数组分散写入
声明RWTexture2D<float3> outputs[N]数组,每个光线生成线程仅写入outputs[DispatchRaysIndex().x](DispatchRays的width设为N),避免写入竞争。后续需通过计算着色器将N个纹理的对应像素求和,再做归一化。该方案无原子操作开销,但要注意显存占用(总内存为N * width * height * sizeof(float3))。
3. 是否必须用计算着色器完成归一化/求和?
不是必须,但计算着色器是最高效直接的实现方式:
- 单
output纹理场景,计算着色器归一化可避免CPU读取纹理的巨大开销,是最优选择。 - 多纹理数组场景,求和+归一化也用计算着色器实现:
cbuffer GlobalParams : register(b0) { float tau; uint width; uint height; uint numTextures; // 对应N }; RWTexture2D<float3> finalOutput : register(u0); RWTexture2D<float3> outputs[16] : register(u1); // 根据实际N调整数量 [shader("compute")] [numthreads(8, 8, 1)] void SumAndNormalize() { uint2 pixel = DispatchThreadID.xy; if (pixel.x >= width || pixel.y >= height) return; float3 total = float3(0.0f, 0.0f, 0.0f); for (uint i = 0; i < numTextures; ++i) { total += outputs[i][pixel]; } finalOutput[pixel] = total / tau; }
内容的提问来源于stack exchange,提问作者0xbadf00d

