Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions 2d/total/blur.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package total

import (
"fmt"
"image"
"os"

"github.com/disintegration/imaging"
)

// Blur blurs the image using a gaussian function. Sigma parameter must be
// positive and indicates how much the image will be blurred. It runs on the
// Intel GPU when available and falls back to the CPU implementation
// (disintegration/imaging's Blur) otherwise.
//
// Blur 使用高斯函数模糊图像。Sigma 参数必须为正数,表示模糊程度。
// 当 Intel GPU 可用时在 GPU 上运行,否则回退到 CPU 实现
// (disintegration/imaging 的 Blur)。
func Blur(img image.Image, sigma float64) *image.NRGBA {
if sigma <= 0 {
return imaging.Clone(img)
}

if canUseBlurKernel {
dst, err := gpuBlur(img, sigma)
if err == nil {
return dst
}
fmt.Fprintln(os.Stderr, "[gg.blur_ocl] gpuBlur err:", err, "fallback to cpu")
canUseBlurKernel = false
}

return imaging.Blur(img, sigma)
}
308 changes: 308 additions & 0 deletions 2d/total/blur_ocl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
package total

import (
_ "embed"
"errors"
"fmt"
"image"
"image/color"
"math"
"os"
"unsafe"

"github.com/fumiama/gozel/gozel"
"github.com/fumiama/gozel/ze"

"github.com/FloatTech/gg/gpu"
)

//go:generate ocloc compile -file build/blur_ocl.cl -spv_only -options "-cl-mad-enable -cl-fast-relaxed-math -cl-finite-math-only -cl-single-precision-constant" -internal_options "-O3" -output build/blur_ocl
//go:generate llvm-spirv -to-text build/blur_ocl_.spv -o build/blur_ocl.spt

//go:embed build/blur_ocl_.spv
var blurspv []byte

var (
canUseBlurKernel = false
blurModel ze.ModuleHandle
)

// maxBlurRadius caps the gaussian window half-size to avoid excessive device loops.
//
// maxBlurRadius 限制高斯窗口半径上限,避免设备端循环过长。
const maxBlurRadius = 64

func init() {
if !gpu.IsAvailable() {
return
}

var err error
blurModel, err = gpu.ModuleCreateAndCheckKernels(blurspv, "blurh", "blurv")
if err != nil {
fmt.Fprintln(os.Stderr, "[gg.blur_ocl] gpu init err:", err)
return
}

canUseBlurKernel = true
}

// gaussianBlurWeights computes the normalized gaussian weights of window size
// 2*radius+1, where radius = ceil(3*sigma) capped at maxBlurRadius, mirroring
// disintegration/imaging's Blur.
func gaussianBlurWeights(sigma float64) (radius int, weights []float32) {
radius = int(math.Ceil(sigma * 3.0))
if radius < 1 {
radius = 1
}
if radius > maxBlurRadius {
radius = maxBlurRadius
}
weights = make([]float32, 2*radius+1)
sum := 0.0
for i := range weights {
x := float64(i - radius)
w := math.Exp(-(x * x) / (2 * sigma * sigma))
weights[i] = float32(w)
sum += w
}
inv := float32(1 / sum)
for i := range weights {
weights[i] *= inv
}
return radius, weights
}

func gpuBlur(img image.Image, sigma float64) (dst *image.NRGBA, err error) {
if sigma <= 0 {
return nil, errors.New("sigma must be positive")
}
rgbaimg := ImageToRGBA(img)
w, h := rgbaimg.Bounds().Dx(), rgbaimg.Bounds().Dy()
if w <= 0 || h <= 0 {
return nil, errors.New("empty image")
}
npix := uintptr(w) * uintptr(h)
if npix*16 > 1<<31 {
return nil, errors.New("image too large to blur on gpu")
}
pixels := rgbaimg.Pix

radius, weights := gaussianBlurWeights(sigma)

krnH, err := blurModel.KernelCreate("blurh")
if err != nil {
return nil, err
}
defer krnH.Destroy()
krnV, err := blurModel.KernelCreate("blurv")
if err != nil {
return nil, err
}
defer krnV.Destroy()

// Allocate input and output image buffers (host + device)
imgSize := npix * unsafe.Sizeof(color.RGBA{})
inHost, inDev, err := gpu.MemAllocHostDevicePair(imgSize, unsafe.Sizeof(color.RGBA{}))
if err != nil {
return nil, err
}
defer gpu.MemFree(inHost)
defer gpu.MemFree(inDev)

outHost, outDev, err := gpu.MemAllocHostDevicePair(imgSize, unsafe.Sizeof(color.RGBA{}))
if err != nil {
return nil, err
}
defer gpu.MemFree(outHost)
defer gpu.MemFree(outDev)

// Allocate intermediate premultiplied float4 buffer (device only)
tmpDev, err := gpu.MemAllocDevice(npix*unsafe.Sizeof([4]float32{}), unsafe.Sizeof([4]float32{}))
if err != nil {
return nil, err
}
defer gpu.MemFree(tmpDev)

// Allocate weights buffer (host + device)
wtsSize := uintptr(len(weights)) * unsafe.Sizeof(float32(0))
wtsHost, wtsDev, err := gpu.MemAllocHostDevicePair(wtsSize, unsafe.Sizeof(float32(0)))
if err != nil {
return nil, err
}
defer gpu.MemFree(wtsHost)
defer gpu.MemFree(wtsDev)

copy(unsafe.Slice((*uint8)(inHost), len(pixels)), pixels)
copy(unsafe.Slice((*float32)(wtsHost), len(weights)), weights)

// Set horizontal pass arguments
err = krnH.SetArgumentValue(0, &inDev)
if err != nil {
return nil, err
}
err = krnH.SetArgumentValue(1, &tmpDev)
if err != nil {
return nil, err
}
err = krnH.SetArgumentValue(2, uint32(w))
if err != nil {
return nil, err
}
err = krnH.SetArgumentValue(3, uint32(h))
if err != nil {
return nil, err
}
err = krnH.SetArgumentValue(4, int32(radius))
if err != nil {
return nil, err
}
err = krnH.SetArgumentValue(5, &wtsDev)
if err != nil {
return nil, err
}

// Set vertical pass arguments
err = krnV.SetArgumentValue(0, &tmpDev)
if err != nil {
return nil, err
}
err = krnV.SetArgumentValue(1, &outDev)
if err != nil {
return nil, err
}
err = krnV.SetArgumentValue(2, uint32(w))
if err != nil {
return nil, err
}
err = krnV.SetArgumentValue(3, uint32(h))
if err != nil {
return nil, err
}
err = krnV.SetArgumentValue(4, int32(radius))
if err != nil {
return nil, err
}
err = krnV.SetArgumentValue(5, &wtsDev)
if err != nil {
return nil, err
}

// Determine group size
gX, gY, _, err := krnH.SuggestGroupSize(uint32(w), uint32(h), 1)
if err != nil {
return nil, err
}
err = krnH.SetGroupSize(gX, gY, 1)
if err != nil {
return nil, err
}
err = krnV.SetGroupSize(gX, gY, 1)
if err != nil {
return nil, err
}
gcx := uint32(math.Ceil(float64(w) / float64(gX)))
gcy := uint32(math.Ceil(float64(h) / float64(gY)))

// Build command list
lst, err := gpu.CommandListCreate()
if err != nil {
return nil, err
}
defer lst.Destroy()

// Event: input pixels copy done
inEv, cl1, err := gpu.EventCreate(gozel.ZE_EVENT_SCOPE_FLAG_HOST, 0)
if err != nil {
return nil, err
}
defer cl1()
defer inEv.Destroy()

// Event: weights copy done
wtEv, cl2, err := gpu.EventCreate(gozel.ZE_EVENT_SCOPE_FLAG_HOST, 0)
if err != nil {
return nil, err
}
defer cl2()
defer wtEv.Destroy()

// Copy input pixels: host -> device
err = lst.AppendMemoryCopy(inDev, inHost, imgSize, inEv)
if err != nil {
return nil, err
}

// Copy weights: host -> device
err = lst.AppendMemoryCopy(wtsDev, wtsHost, wtsSize, wtEv)
if err != nil {
return nil, err
}

// Event: horizontal pass done
hEv, cl3, err := gpu.EventCreate(gozel.ZE_EVENT_SCOPE_FLAG_HOST, 0)
if err != nil {
return nil, err
}
defer cl3()
defer hEv.Destroy()

// Launch horizontal pass
err = lst.AppendLaunchKernel(krnH, &gozel.ZeGroupCount{
Groupcountx: gcx, Groupcounty: gcy, Groupcountz: 1,
}, hEv, inEv, wtEv)
if err != nil {
return nil, err
}

// Event: vertical pass done
vEv, cl4, err := gpu.EventCreate(gozel.ZE_EVENT_SCOPE_FLAG_HOST, 0)
if err != nil {
return nil, err
}
defer cl4()
defer vEv.Destroy()

// Launch vertical pass
err = lst.AppendLaunchKernel(krnV, &gozel.ZeGroupCount{
Groupcountx: gcx, Groupcounty: gcy, Groupcountz: 1,
}, vEv, hEv)
if err != nil {
return nil, err
}

// Event: output copy done
outEv, cl5, err := gpu.EventCreate(gozel.ZE_EVENT_SCOPE_FLAG_HOST, 0)
if err != nil {
return nil, err
}
defer cl5()
defer outEv.Destroy()

// Copy output: device -> host (wait for vertical pass)
err = lst.AppendMemoryCopy(outHost, outDev, imgSize, outEv, vEv)
if err != nil {
return nil, err
}

// Close and execute
err = lst.Close()
if err != nil {
return nil, err
}

err = gpu.ExecCommandLists(lst)
if err != nil {
return nil, err
}

// Wait for output copy to complete
err = outEv.HostSynchronize(math.MaxUint64)
if err != nil {
return nil, err
}

dst = image.NewNRGBA(image.Rect(0, 0, w, h))
copy(dst.Pix, unsafe.Slice((*uint8)(outHost), len(dst.Pix)))
return dst, nil
}
Loading