金属片段着色器 A-Buffer 产生闪烁的毛刺

Metal fragment shader A-Buffer produces shimmering glitch

我正在为 Mac 在 Metal 中实现一个 A-Buffer,它几乎可以正常工作——除了我在三角形重叠的地方看到闪烁的故障。似乎涉及的缓冲区可能没有在正确的时间更新。但我不知道是什么原因造成的。这是一张图片 -- 'corrupted' 区域每一帧都在变化,但始终是两种颜色重叠的地方。

我不会解释整个 A-Buffer 操作,但它涉及将三个缓冲区绑定到着色器:一个非常大(172MB,尽管本示例只写入了一小部分)。还有一个 "texture" 整数和一个整数原子计数器。

渲染分两步完成——第一步为每个可见的渲染像素位置创建一个像素片段链表:

// the uint return goes into the start index buffer, our 'image'.  The FragLinkBuffer stores the data

fragment uint stroke_abuffer_fragment(VertexIn interpolated [[stage_in]],
                                                const device uint&  color [[ buffer(0) ]],
                                                device FragLink*  LinkBuffer [[ buffer(1) ]],
                                                device atomic_uint &counter[[buffer(2)]],
                                                texture2d<uint> StartTexture     [[ texture(0) ]]) {
    constexpr sampler Sampler(coord::pixel,filter::nearest);

    // get old start position for this pixel from from start buffer
    uint value = atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);

    // store pointer to this position in the start buffer
    int oldStart = StartTexture.sample(Sampler, interpolated.position.xy).x;

    // store fragment information in link buffer
    FragLink F;
    F.color = color;
    F.depth = interpolated.position.z;
    F.next = oldStart;
    LinkBuffer[value] = F;

    // return pointer to new start for this fragment, which will be stored back to the StartTexture
    return value;  
}

第二遍在每个像素处对片段进行排序和混合。

#define MAX_PIXELS 16

fragment float4 stroke_abuffer_fragment_composite(CompositeVertexOut interpolated [[stage_in]],
                                                  device FragLink*  LinkBuffer [[ buffer(0) ]],
                                                  texture2d<uint> StartTexture     [[ texture(0) ]]) {
    pixel SortedPixels[MAX_PIXELS];
    int numPixels = 0;
    constexpr sampler Sampler(coord::pixel,filter::nearest);
    FragLink F;
    pixel P;

    uint index = StartTexture.sample(Sampler, interpolated.position.xy).x;
    if (index == 0)
        discard_fragment();

    float4 finalColor = float4(0.0);

    // grab all the linked fragments for this pixel
    while (index != 0) {
        F = LinkBuffer[index];
        P.color = F.color;
        P.depth = F.depth;
        SortedPixels[numPixels++] = P;
        index = (numPixels >= MAX_PIXELS) ? 0 : F.next;
    }

    // now sort them by depth
    for (int j = 1; j < numPixels; ++j) {
        pixel key = SortedPixels[j];
        int i = j - 1;
        while (i >= 0 && SortedPixels[i].depth <= key.depth)
        {
            SortedPixels[i+1] = SortedPixels[i];
            --i;
        }
        SortedPixels[i+1] = key;
    }

    // blend them in order
    for (int k = 0; k < numPixels; k++) {
        uint color = SortedPixels[k].color;
        float red = ((color>>24)&255)/255.0;
        float green = ((color>>16)&255)/255.0;
        float blue = ((color>>8)&255)/255.0;
        float alpha = ((color)&255)/255.0;
        //red = 1.0; green = 0.0; blue = 0.0; alpha = 0.25;
        finalColor.xyz = mix(finalColor.xyz, float3(red,green,blue), alpha);
        finalColor.w = alpha;
    }


    return finalColor;

}

我只是想知道这种行为的原因可能是什么。如果我在每一帧检查缓冲区的值,通过将它们的内容 blit 回 CPU 内存和打印值,它们在每一帧都在改变,而它们应该是相同的。

无论我是否在每帧调用 commandBuffer.commit() 之后调用 commandBuffer.waitUntilCompleted(),结果都是一样的。通过调用 waitUntilCompleted,我是否应该消除与一帧使用缓冲区相关的任何问题,而下一帧也在尝试访问它? (因为我想也许我需要将 172MB 的缓冲区三重缓冲,这太可怕了。)

我正在做整个渲染——重置计数器的初始 blit、第一个渲染通道,然后是第二个渲染通道,全部作为一个 commandBuffer 调用。那会有问题吗?换句话说,我是否需要实际提交第一个渲染通道,等待它完成,然后启动第二个渲染通道? (编辑:我试过了,它没有改变任何东西)

我正在移植的原始技术 (https://www.slideshare.net/hgruen/oit-and-indirect-illumination-using-dx11-linked-lists) 在第二阶段不使用 OpenGL 混合——它们将背景绑定为纹理缓冲区并将其与像素片段一起手动混合,然后 return 完整的结果。我只是决定跳过这个并使用正常 'over' 混合将我最终组合的片段颜色与背景混合。但我不明白为什么这会导致我遇到的问题。我会按照他们的方式尝试以防万一...

我非常感谢任何关于导致这种情况的想法! 谢谢

。 . .

更新:根据评论中的对话,我更新了着色器以使用原子缓冲区而不是纹理,但现在得到 "Execution of the command buffer was aborted due to an error during execution. Internal Error (IOAF code 1)":

fragment void stroke_abuffer_fragment(VertexIn interpolated [[stage_in]],
                                      const device uint&  color [[ buffer(0) ]],
                                      constant Uniforms&  uniforms    [[ buffer(1) ]],
                                      device FragLink*  LinkBuffer [[ buffer(3) ]],
                                      device atomic_uint &counter[[buffer(2)]],
                                      device atomic_uint *StartBuffer[[buffer(4)]]
                                      ) {

    uint pos = int(interpolated.position.x)+int(interpolated.position.y)*uniforms.displaySize[0];

    // get counter value -- the index to next spot in link buffer
    uint value = atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);
    value += 1;

    // store fragment information in link buffer
    FragLink F;
    F.color = color;
    F.depth = interpolated.position.z;
    F.next = atomic_exchange_explicit(&StartBuffer[pos], value, memory_order_relaxed);

    LinkBuffer[value] = F;
}

对于 stroke_abuffer_fragment 通道,您是否对渲染目标和 StartTexture 参数使用相同的纹理?我不认为那是犹太洁食。我希望验证层会抱怨这一点,但也许不会。

可能 StartTexture 应该使用 access::read_write 并且函数应该将结果写入它和 return void。在这种情况下,渲染命令编码器应该没有渲染目标。

您还需要使用 raster_order_group(0) 限定符对其进行声明,以确保一次只为该像素调用一次片段函数 运行。

写入后可能需要调用StartTexture.fence()。我不确定这一点,因为下一次读取同一个纹素将在片段函数的后续调用中进行(感谢 raster_order_group())。换句话说,raster_order_group() 本身似乎暗示着栅栏。

您还需要在该通道的绘制调用之后在命令编码器上调用 textureBarrier。这对于确保下一遍看到第一遍写入的结果是必要的。不过,除此之外,在一个命令缓冲区中完成所有这些应该没问题。


更新:

如果您不能使用 raster_order_group() 因为您的目标是 High Sierra 之前的 OS 版本,还有一个替代方案。事实上,即使可以,它也可能更优越,因为它不需要 raster_order_group().

隐含的同步

基本思想是使用原子交换来操作 linked 列表。

因此,必须将 StartTexture 更改为缓冲区而不是纹理(正如您在第一条评论中提到的尝试)。是的,您需要将宽度作为 "uniform" 传递并按照您指示的方式计算元素索引 (x + y * width)。您不会尝试继续使用 read()。缓冲区没有那样的成员函数。它们只是引用,或者在本例中是指针。您只需像 StartTexture[index].

一样对其进行索引

不过,您需要将元素类型设为 atomic_uint 而不是 uint。您将使用原子交换而不是正常的读取或写入 StartTexture 将新节点集成到 link 列表中:

F.next = atomic_exchange_explicit(&StartTexture[index], value, memory_order_relaxed);

这保持了 linked 列表的完整性,即使 stroke_abuffer_fragment() 的两次调用同时针对给定位置 运行ning。


另一件事:您要将 counter 缓冲区初始化为什么? StartBuffer 清除了什么?似乎您正在使用 0 值作为列表结尾的标记,所以我猜您将两者都重置为全零。这是有道理的,但请记住 atomic_fetch_add_explicit() return 是计数器的值,因为它是 递增之前。所以第一次调用stroke_abuffer_fragment()会得到0,如果你想要递增后的值,当然要加1。如果你不想浪费LinkBuffer中的一个元素,你可以在对其进行索引时减去 1。或者您可以选择不同的哨兵值并适当地清除事物。不管怎样,您需要修复不匹配问题。

哦对了,stroke_abuffer_fragment()color参数大概应该声明在constant地址space,而不是device地址space.