通过更改 uv 坐标在金属中出现意外性能
Unexpected performance hit in metal by changing uv coordinates
下面是 metal
中的一个简单顶点和片段着色器组合,可渲染 64 个相同的 2D 四边形。
vertex VertexOut vertexMain(uint k [[ vertex_id ]],
uint ii [[instance_id]],
device float2* tex [[buffer(2)]],
device float2* position [[buffer(1)]],
device float* state [[buffer(0)]]){
VertexOut output;
int i = 4*ii+1;
float2 pos = position[k];
pos *= float2(state[i+2],state[i+3]);
pos += float2(state[i],state[i+1]);
pos.x *= state[0];
output.position = float4(pos,0,1);
output.tex = tex[k]*float2(du,dv);
return output;
};
fragment float4 fragmentMain(VertexOut input [[stage_in]],
texture2d<float> texture [[texture(0)]],
sampler sam [[sampler(0)]] ){
return texture.sample(sam, input.tex);
};
采样器使用归一化坐标,因此 du
和 dv
的范围可以从 0 到 1,并控制从左下角开始采样纹理剪辑的大小。
看来我对金属采样的工作原理有误解。我希望无论 du
和 dv
保持什么值,计算成本都将保持不变。但是,当我将 du
和 dv
增加到 1 时,帧速率会下降。我没有使用任何 mipmapping,也没有更改屏幕上光栅化的四边形的大小。这种影响在线性过滤时更为显着,但在最近过滤时也会发生。在我看来,由于绘制到屏幕上的像素数量相同,因此 GPU 上的负载不应取决于 du
和 dv
。我错过了什么?
编辑:这是我的采样器和颜色附件:
let samplerDescriptor = MTLSamplerDescriptor()
samplerDescriptor.normalizedCoordinates = true
samplerDescriptor.minFilter = .linear
samplerDescriptor.magFilter = .linear
let sampler = device.makeSamplerState(descriptor: samplerDescriptor)
let attachment = pipelineStateDescriptor.colorAttachments[0]
attachment?.isBlendingEnabled = true
attachment?.sourceRGBBlendFactor = .one
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
随着 du
和 dv
的增加,您的四边形会显示更多纹理。 GPU 往往具有用于纹理数据的小型缓存,并且当您显示更多纹理时,您将更多地丢弃和重新填充该缓存。
抖动纹理缓存将使用更多的内存带宽,这是非常有限的资源,纹理内存带宽通常不是瓶颈,但由于您的片段着色器除了获取纹理外几乎什么都不做,所以这不足为奇这是你的瓶颈。因此,改变 UV 会影响性能也就不足为奇了。
令人惊讶的是,当您所做的只是渲染 64 个四边形时,帧率在这些非常强大的设备上下降到 60 以下(iPad Pro 尤其是一个非常强大的设备)。也就是说,如果所有 64 个四边形都覆盖了大部分屏幕,帧率下降是可以理解的。
要提高性能,您需要减少 GPU 需要处理的纹理数据量。从 32 位纹理格式 (8888) 更改为 16 位 (565/4444) 或 4 位(PVRTC 压缩纹理)会产生很大影响。
真正的大胜利可能是启用 mipmapping。假设使用较高的 du
和 dv
值,您最终会最小化纹理,然后使用 mipmapping 将得到 huge 性能优势,作为额外的奖励,您的纹理也会看起来更好(它将修复锯齿)。不错 return 增加了 33% 的纹理内存。
下面是 metal
中的一个简单顶点和片段着色器组合,可渲染 64 个相同的 2D 四边形。
vertex VertexOut vertexMain(uint k [[ vertex_id ]],
uint ii [[instance_id]],
device float2* tex [[buffer(2)]],
device float2* position [[buffer(1)]],
device float* state [[buffer(0)]]){
VertexOut output;
int i = 4*ii+1;
float2 pos = position[k];
pos *= float2(state[i+2],state[i+3]);
pos += float2(state[i],state[i+1]);
pos.x *= state[0];
output.position = float4(pos,0,1);
output.tex = tex[k]*float2(du,dv);
return output;
};
fragment float4 fragmentMain(VertexOut input [[stage_in]],
texture2d<float> texture [[texture(0)]],
sampler sam [[sampler(0)]] ){
return texture.sample(sam, input.tex);
};
采样器使用归一化坐标,因此 du
和 dv
的范围可以从 0 到 1,并控制从左下角开始采样纹理剪辑的大小。
看来我对金属采样的工作原理有误解。我希望无论 du
和 dv
保持什么值,计算成本都将保持不变。但是,当我将 du
和 dv
增加到 1 时,帧速率会下降。我没有使用任何 mipmapping,也没有更改屏幕上光栅化的四边形的大小。这种影响在线性过滤时更为显着,但在最近过滤时也会发生。在我看来,由于绘制到屏幕上的像素数量相同,因此 GPU 上的负载不应取决于 du
和 dv
。我错过了什么?
编辑:这是我的采样器和颜色附件:
let samplerDescriptor = MTLSamplerDescriptor()
samplerDescriptor.normalizedCoordinates = true
samplerDescriptor.minFilter = .linear
samplerDescriptor.magFilter = .linear
let sampler = device.makeSamplerState(descriptor: samplerDescriptor)
let attachment = pipelineStateDescriptor.colorAttachments[0]
attachment?.isBlendingEnabled = true
attachment?.sourceRGBBlendFactor = .one
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
随着 du
和 dv
的增加,您的四边形会显示更多纹理。 GPU 往往具有用于纹理数据的小型缓存,并且当您显示更多纹理时,您将更多地丢弃和重新填充该缓存。
抖动纹理缓存将使用更多的内存带宽,这是非常有限的资源,纹理内存带宽通常不是瓶颈,但由于您的片段着色器除了获取纹理外几乎什么都不做,所以这不足为奇这是你的瓶颈。因此,改变 UV 会影响性能也就不足为奇了。
令人惊讶的是,当您所做的只是渲染 64 个四边形时,帧率在这些非常强大的设备上下降到 60 以下(iPad Pro 尤其是一个非常强大的设备)。也就是说,如果所有 64 个四边形都覆盖了大部分屏幕,帧率下降是可以理解的。
要提高性能,您需要减少 GPU 需要处理的纹理数据量。从 32 位纹理格式 (8888) 更改为 16 位 (565/4444) 或 4 位(PVRTC 压缩纹理)会产生很大影响。
真正的大胜利可能是启用 mipmapping。假设使用较高的 du
和 dv
值,您最终会最小化纹理,然后使用 mipmapping 将得到 huge 性能优势,作为额外的奖励,您的纹理也会看起来更好(它将修复锯齿)。不错 return 增加了 33% 的纹理内存。