回転ブラーのGPU対応の模索
回転ブラーの問題
ティムさんの製作されたスクリプトの中に、「回転ブラー」というものがあります。僕はあまり使わないスクリプトなのですが、かなり前から「回転ブラーは重い」といったことを耳にします。これをGPUでいいかんじに高速化できないかなと思って試してみたら、そこそこなかんじで動いたので共有です。ちゃんとdllで作ったりアルゴリズムを見直せばティムさんの回転ブラーよりも高速に動作しそうです。
luajit実装
回転ブラーというものは、一定の距離を回転させたときにできる軌跡上のピクセルの平均をとったものです。1ピクセルごとに回転行列を計算すると、とてつもない演算量になってしまいます。以下はluajit実装の回転ブラーです。200×200の画象でも重いです。フルHDなんて到底できません。
code:lua
--track0:size,0,100,10,0.01
local work,data,w,h=obj.getpixeldata"work",obj.getpixeldata()
local ffi=require("ffi")
pcall(ffi.cdef,typedef struct Pixel_ {uint8_t b,g,r,a;} Pixel;)
local cdata=ffi.cast("Pixel*",data)
local cwork=ffi.cast("Pixel*",work)
local function mcast(d)
return math.max(math.min(math.floor(d+0.5),255),0)
end
for y=0,h-1 do
for x=0,w-1 do
local nx,ny,d=0,0,{0,0,0,0,0}
for i=0,1024-1 do
local th=math.pi*i*obj.track0/(1024*180)
nx=math.cos(th)*(x-w/2)-math.sin(th)*(y-h/2)
ny=math.sin(th)*(x-w/2)+math.cos(th)*(y-h/2)
nx,ny=math.floor(nx+w/2),math.floor(ny+h/2)
if(nx>=0 and nx<w and ny>=0 and ny<h)then
end
end
end
end
end
obj.putpixeldata(work)
当然ですが、重いのはfor i=0,1024-1 doなんてことを1ピクセルごとにやっているからです。しかも、そのループの中には三角関数があります。回転ブラーというものは、その概念自体がとても重い処理なのです。
GPUに対応させる
GPUは6次元方向に並列に処理を実行することができます。横方向ピクセルと縦方向ピクセルで2次元、更に軌跡上のピクセルを拾うためにnumthreadsが32×32=1024で2次元あれば一気に並列処理できそうです。
問題があるとすれば、回転ブラーの精度となるnumthreadsの上限が1024を超えれないということです。これはhlslの制約によるものです。では、1024という精度だと駄目なのかというと、そうでも無さそうです。
code:lua
obj.load("figure","円",0xff,10)
for i=0,1023 do
local th=2*math.pi*i/1024
obj.draw(math.cos(th)*1000,math.sin(th)*1000)
end
気にならない人は気にならないレベルです。僕は気にならない派ですね。
一応、1024でも駄目な場合は1つのスレッドで2つ分のスレッドの処理をするなどの方法で1024の倍数で精度を増やして行けそうなので安心です。
GPU実装の方向性
GPU実装をするには様々な選択肢があります。バイナリを直接叩いたり、hlslを使ったり、hlslのラッパーであるWebGPUを使ったりといろいろです。その中でも今回はM_ComputeShader_Module.dllを使用することにしました。
M_ComputeShader_Module.dllはhlsl ComputeShaderの単純なラッパーで、luajitから簡単に使用できます。デバッグが大変というデメリットがありますが、自分で作ったdllなのでちゃんと使ってあげようとかそんなかんじです。
GPU対応プログラム
M_ComputeShader_Module.dllと、hlslに対応したGPUが必要です。
code:回転ブラー_M.lua
--[[
回転ブラー_M.anm
]]
--track0:rot,-1000,1000,0,0.01
--track1:精度,1,32,10,1
local work,data,w,h=obj.getpixeldata"work",obj.getpixeldata()
local code=[[
//struct
struct BUFFDATA {uint x;};
//CSsetting
struct CSInput {
uint3 groupThread : SV_GroupThreadID;//numthreads
uint3 group : SV_GroupID;//Dispatch
uint groupIndex : SV_GroupIndex;//numthreads
uint3 dispatch : SV_DispatchThreadID;//Dispatch,numthreads
};
//buff(SRV)
StructuredBuffer<BUFFDATA> INbuff : register(t0);
//buff(UAV)
RWStructuredBuffer<BUFFDATA> buff1 : register(u0);
RWStructuredBuffer<BUFFDATA> buff2 : register(u1);
//groupshared
groupshared uint all_sum5; //mainFunc
void main(const CSInput input) {
if(input.groupIndex == 0) {
}
GroupMemoryBarrierWithGroupSync();
uint x = input.group.x;
uint y = input.group.y;
uint index = x + ]]..w..[[ * y;
float hx = float(x) - float(]]..(w/2)..[[);
float hy = float(y) - float(]]..(h/2)..[[);
float th = float(input.groupIndex) * ]]..(math.pi*obj.track0/180).. / float(..32*obj.track1..[[);
float s = sin(th);
float c = cos(th);
int nx = int(c * hx - s * hy + float(]]..(w/2+0.5)..[[));
int ny = int(s * hx + c * hy + float(]]..(h/2+0.5)..[[));
if(nx >= 0 && nx < ]]..w.. && ny >= 0 && ny < ..h..[[) {
uint ix = nx + ]]..w..[[ * ny;
InterlockedAdd(all_sum0, ((buff1ix.x << 0) >> 24)); InterlockedAdd(all_sum1, ((buff1ix.x << 8) >> 24)); InterlockedAdd(all_sum2, ((buff1ix.x << 16) >> 24)); InterlockedAdd(all_sum3, ((buff1ix.x << 24) >> 24)); InterlockedAdd(all_sum4, 1); }
GroupMemoryBarrierWithGroupSync();
if(input.groupIndex == 0) {
buff2index.x = 0x00000000; buff2index.x |= (uint(clamp(float(all_sum0) / float(all_sum4) + 0.5, 0.0, 255.0)) << 24); buff2index.x |= (uint(clamp(float(all_sum1) / float(all_sum4) + 0.5, 0.0, 255.0)) << 16); buff2index.x |= (uint(clamp(float(all_sum2) / float(all_sum4) + 0.5, 0.0, 255.0)) << 8); buff2index.x |= (uint(clamp(float(all_sum3) / float(all_sum4) + 0.5, 0.0, 255.0)) << 0); }
}
}
]]
local ffi=require"ffi"
pcall(ffi.cdef,[[
typedef struct BUFFDATA {
void* data;
long struct_size;
long num;
} BUFFDATA;
typedef struct BUFFINT {
uint32_t x;
} BUFFINT;
]])
local index=w*h
local SRV=ffi.new("BUFFDATA1") local blo=ffi.new("BUFFINT10") local UAV=ffi.new("BUFFDATA2") local result_ch=ffi.new("int2",{0,1}) local d=require"M_ComputeShader_Module"
local error_num,str=d.s(code,1,SRV,2,UAV,"main",w,h,1,result_ch)
obj.putpixeldata(work)
改善点
・まだ試していませんが、三角関数の値は先にCPU側で計算しておいてバッファを介してGPUに送った方が速そうな気がします。
・1つのスレッドで3スレッド分ぐらいの処理をしても問題はなさそうなかんじがします。単純にスレッド数が多すぎて一回じゃ回りきっていないので、スレッド数を減らす方向でなんとかするともう少し速くなりそうです。
・M_ComputeShader_Module.dllの都合で、いらないバッファまでコピーしてます。これは完全に無駄なので、普通にc++でhlslを動かした方が良さそうです。
・サンプル数を1024という大きめの値でとっているので何とかなっている感がありますが、本来は回転行列の逆変換で求めた位置を線形補間して、その平均をとっていくかんじなので暇なときにでも直したいです。
小ネタ
groupsharedを初期化しないでいると、初期値が不定となるため疎らにノイズができて楽しいです。
独り言
デバッグめっちゃ大変でした。hlsl、型が厳密なのにエラーを出してくれないのです。
だ、だれか続きやってくれてもいいんですよ…??(チラッ
by metaphysical bard