.metallib文件冲突报错与解决方案
在将Metal接入主工程时,发现编译时就会报错:
Multiple commands produce 'xxx/default.metallib':
- Target 'xxx' (project 'xxx'): MetalLink /xxx/default.metallib
- That command depends on command in Target 'xxx' (project 'xxx'): script phase “[CP] Copy Pods Resources”
原因:
工程中存在多个default.metallib文件,因为使用了其他生成metallib的库,因此有冲突
解决:
- 自行生成.metallib文件,运行时进行静态编译:
在compile_source去掉.metal文件,根据苹果的指引和官方文档来操作,先把数个.metal文件合成多个.air文件,然后再将所有.air文件合并编译成.metallib文件。
根据上面的代码改了一下.sh代码:
xcrun -sdk iphoneos metal MyLibrary.metal -o MyLibrary.air
xcrun -sdk iphoneos metallib MyLibrary.air -o MyLibrary.metallib
发现iPhone 6S机型有以下问题:
pipelinestate error: Error Domain=AGXMetalA8 Code=3 "Function fragmentShader has a deployment target which is incompatible with this OS." UserInfo={NSLocalizedDescription=Function fragmentShader has a deployment target which is incompatible with this OS.}
根据论坛的介绍,发现是没指定-mios-version-min
,导致编译时会以deployment target的版本进行编译。
另外,不同版本的iOS系统支持的Metal版本也不一样,因为我们要以最低适配来对metallib文件进行编译:
得到.sh文件:
xcrun -sdk iphoneos metal -c -target air64-apple-ios9.0 alpha_video_renderer.metal -o alpha_video_renderer.air
xcrun -sdk iphoneos metal -std=ios-metal1.1 -mios-version-min=9.0 alpha_video_renderer.air -o alpha_video_renderer.metallib
- 在工程中读取metallib文件
将读取代码改为,即可进行读取:
NSString *libPath = [METAL_BUNDLE pathForResource:@"alpha_video_renderer" ofType:@"metallib"];
id <MTLLibrary> defaultLibrary = [_device newLibraryWithFile:libPath error:&error];
- ShaderString保底
如果.metallib文件还是读取失败,那可以考虑将.metal文件写进String里面,用newLibraryWithSource:options:error:
来读取,不过这种是动态编译,感觉不太好。
感觉还是有坑..如果一个工程能用多个.metal文件统一生成default.metallib就好了,但是无奈是引用了别的带Metal的文件,只能这样曲线救国了..
更新于7.19
发现不少提供Metal渲染的第三方库都是直接用default.metallib,这样的话接入两个库就会有符号冲突问题,这段时间恰好自研了一个Metal链式结构库,集成在Pod里面了,为了让接入的项目不会像我之前那样被坑,所以需要自己生成一个自定义的Metallib文件。
自定义metallib文件,有三个问题:
-
一个项目中往往会有多个.metal文件,如何一次性把所有的.metal文件编译成一个.metallib?
-
生成自定义metallib文件,意味着要把所有的.metal文件从工程的Compile Source里面移除掉,如何集成到Pod里面,让.metal文件成为一个只读而不编译的文件?
-
最重要的一个问题,我们读取.metallib文件是要手动运行脚本生成的,那能不能编写一个脚本,让每次编译时都自动运行生成.metallib的脚本,而不是每次编译前都自己手动跑一次?
通过自己不断查阅资料,找到了解决方案:
1. 如何一次性把所有的.metal文件编译成一个.metallib?
我们可以回顾一下生成.metallib的流程图:
可以看到,编译脚本其实是支持多个输入的,因此多个.metal文件生成一个.metallib文件是可行的,比如,对于mix.metal和no_green.metal文件,我们可以这样来写:
xcrun -sdk iphoneos metal -c -target air64-apple-ios9.0 mix.metal -o mix.air
xcrun -sdk iphoneos metal -c -target air64-apple-ios9.0 no_green.metal -o no_green.air
xcrun -sdk iphoneos metal -std=ios-metal1.1 -mios-version-min=9.0 mix.air no_green.air -o alpha_video_renderer.metallib
rm mix.air no_green.air
可以看到,我们先根据两个.metal文件生成了对应的.air文件,再将多个.air文件合并成了一个.metallib文件,那么,对于若干个.metal文件,脚本也就迎刃而解了:
#! /bin/sh
path=$(cd "$(dirname "$0")";pwd)
cd $path
files=$(ls $path)
metal_array=()
air_array=()
index=0
for filename in $files
do
if [ ${filename##*.} == 'metal' ]
then
echo ${filename}
prefix=${filename%%.*}
air_array[index]=$prefix".air"
metal_array[index]=$prefix".metal"
let index++
air_command="xcrun -sdk iphoneos metal -c -target air64-apple-ios9.0 "
air_command+=$prefix".metal"
air_command+=" -o "
air_command+=$prefix".air"
$air_command
fi
done
bulid_command="xcrun -sdk iphoneos metal -std=ios-metal1.1 -mios-version-min=9.0 "
remove_command="rm "
for (( i = 0; i < index; i++ )); do
bulid_command+=${air_array[i]}
bulid_command+=" "
remove_command+=${air_array[i]}
remove_command+=" "
done
bulid_command+="-o alpha_video_renderer.metallib"
$bulid_command
$remove_command
# xcrun -sdk iphoneos metal -c -target air64-apple-ios9.0 mix.metal -o mix.air
# xcrun -sdk iphoneos metal -c -target air64-apple-ios9.0 no_green.metal -o no_green.air
# xcrun -sdk iphoneos metal -std=ios-metal1.1 -mios-version-min=9.0 mix.air no_green.air -o alpha_video_renderer.metallib
# rm mix.air no_green.air
上面的脚本主要流程,就是先进入存放.metal文件的文件夹,并遍历所有.metal文件,生成好所有的.air文件后,再根据字符串把所有.air文件合并成.metallib文件。
2. 如何集成到Pod里面,让.metal文件成为一个只读而不编译的文件?
这个问题比较简单了,先打开Pods => Build Phases => Compile Sources,把所有.metal文件都移除掉。
然后在.podspec声明他是Resource就可以了,然后记得将.metallib放进Bundle里面,我们就可以用Bundle来读取.metallib了。
spec.resource_bundles = {
'Metal' => ['CCAlphaVideoPlayer/MetalKit/**/*.metallib']
}
spec.subspec "MetalKit" do |sp|
sp.source_files = "CCAlphaVideoPlayer/MetalKit/**/*.{h,m,mm,c}"
sp.resources = "CCAlphaVideoPlayer/MetalKit/**/*.{metal,sh}"
end
3. 如何在编译时自动运行编译.metallib脚本?
首先我们要在工程根目录放置一个脚本,用于找到对应目录的运行脚本:
#! /bin/sh
path=$(cd "$(dirname "$0")";pwd)
cd $path/CCAlphaVideoPlayer/MetalKit/Shaders
if [[ -f "compile_metal.sh" ]]; then
sh compile_metal.sh
fi
然后打开Pods => Build Phases,新建一个New Run Script Phase,需要注意的是,这个脚本要先于Copy Bundle Resources项,因为我们要先运行Build Metal的脚本,再取到.metallib文件:
Build Metal脚本就是找到根目录的脚本运行啦:
if [[ -f "../../compile_metal.sh" ]]; then
sh ../../compile_metal.sh
fi
最后我们就得到一个自定义的.metallib生成库了~效果和default.metallib是一样的,还没有符号冲突(吐槽一下苹果,使用default.metallib导致符号冲突这个真的是让人抓狂,这就意味着开发者不能集成一个以上的Metal库了吗= =)
比如我们片段着色器返回了一个float3的值,那么我编译的时候,编译器就会自动帮我指正出来了:
fragment float4 passthroughFragment(SingleInputVertexIO fragmentInput [[stage_in]],
texture2d<float> inputTexture [[texture(0)]])
{
constexpr sampler quadSampler;
float4 color = inputTexture.sample(quadSampler, fragmentInput.textureCoordinate);
return float3(0,0,0);
}
还可以协助报警告:
fragment float4 passthroughFragment(SingleInputVertexIO fragmentInput [[stage_in]],
texture2d<float> inputTexture [[texture(0)]])
{
constexpr sampler quadSampler;
float4 color = inputTexture.sample(quadSampler, fragmentInput.textureCoordinate);
float useless;
return color;
}
虽然把.metal文件从Compile Source里面移除掉后,没有了定位到具体行的功能,但总好过符号冲突编译不过吧,不知道这个问题苹果有没有办法解决了= =
更新于8.24
发现自己手动在Build Phases手动更改脚本会有问题,每次Pod Update完之后脚本都会自动消失掉,因此需要把脚本放到.podspec中:
build_metal_script = <<-CMD
#Pods目录
podsPath=$(pwd)
if [[ -f "../../compile_metal.sh" ]]; then
sh ../../compile_metal.sh
fi
CMD
spec.script_phase = { :name => 'Build Metal', :script => build_metal_script, :shell_path =>'/bin/sh', :execution_position => :before_compile}
注意该脚本需要指定参数before_compile
,代表编译前执行,还需要放到source_files前面,否则编译失败后,因为已经执行了移除.metallib脚本,会导致.metallib文件找不到从而无法继续编译,因此需要先执行脚本再寻找.metallib。
最后Pod update一下,即可在Build Phases看到自动编译脚本啦
但这样还是有点问题,原因是指定了before_compile
参数之后,会发现Bundle文件的Build会优先于Pod工程的Build,由于.metallib文件是Pod工程的脚本编译前生成的,而Bundle生成时会直接取旧的.metallib文件,因此修改.metal代码并不会直接生效,而是要再Build一次。。
在搜索资料无果之后,只能去CocoaPod的GitHub提了个issue,但作者也说目前暂不支持在Bundle文件构建之前运行自定义脚本,他会在1.11版本新增两个参数的支持,也只能期待那个时候新加的参数能解决这个问题了吧~