Last active
October 29, 2024 22:44
-
-
Save MightySeal/7ddf8a14ca01453d824adbc63a59b4aa to your computer and use it in GitHub Desktop.
From realtime CameraX filters to General Purpose GPU computing in simple steps, Droidcon London 2024
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package io.mightyseal.shadercamera.gpgpu | |
| import android.graphics.Bitmap | |
| import android.opengl.EGL14 | |
| import android.opengl.EGLConfig | |
| import android.opengl.EGLExt | |
| import android.opengl.GLES31 | |
| import android.opengl.GLES32 | |
| import android.opengl.GLUtils | |
| import io.mightyseal.shadercamera.camera.checkGlErrorOrThrow | |
| import io.mightyseal.shadercamera.camera.createByteBuffer | |
| import io.mightyseal.shadercamera.camera.createRawBufferOfInts | |
| import timber.log.Timber | |
| import java.nio.ByteBuffer | |
| import java.nio.ByteOrder | |
| class GpGpuComputeImage( | |
| private val bitmap: Bitmap, | |
| private val shaderProvider: () -> String | |
| ) { | |
| init { | |
| val eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY) | |
| if (eglDisplay == EGL14.EGL_NO_DISPLAY) { | |
| throw IllegalStateException("Unable to get EGL14 display ${GLUtils.getEGLErrorString(EGL14.eglGetError())}") | |
| } | |
| val version = IntArray(2) | |
| if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) { | |
| throw IllegalStateException("Unable to initialize EGL14 ${GLUtils.getEGLErrorString(EGL14.eglGetError())}") | |
| } | |
| val configs = arrayOfNulls<EGLConfig>(1) | |
| val numConfigs = IntArray(1) | |
| val attribToChooseConfig = intArrayOf( | |
| EGL14.EGL_RED_SIZE, 8, | |
| EGL14.EGL_GREEN_SIZE, 8, | |
| EGL14.EGL_BLUE_SIZE, 8, | |
| EGL14.EGL_ALPHA_SIZE, 8, | |
| EGL14.EGL_DEPTH_SIZE, 0, | |
| EGL14.EGL_STENCIL_SIZE, 0, | |
| EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, | |
| EGLExt.EGL_RECORDABLE_ANDROID, EGL14.EGL_TRUE, | |
| EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT or EGL14.EGL_PBUFFER_BIT, | |
| EGL14.EGL_NONE | |
| ) | |
| if (!EGL14.eglChooseConfig( | |
| eglDisplay, | |
| attribToChooseConfig, | |
| 0, | |
| configs, | |
| 0, | |
| configs.size, | |
| numConfigs, | |
| 0 | |
| ) | |
| ) { | |
| throw IllegalStateException("Unable to find a suitable EGLConfig") | |
| } | |
| val config = configs[0] ?: throw IllegalStateException("EGLConfig was not initialized") | |
| val attribToCreateContext = intArrayOf( | |
| EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE | |
| ) | |
| val eglContext = EGL14.eglCreateContext( | |
| eglDisplay, config, EGL14.EGL_NO_CONTEXT, | |
| attribToCreateContext, 0 | |
| ) | |
| // Confirm with query | |
| val values = IntArray(1) | |
| EGL14.eglQueryContext( | |
| eglDisplay, | |
| eglContext, | |
| EGL14.EGL_CONTEXT_CLIENT_VERSION, | |
| values, | |
| 0 | |
| ) | |
| EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, eglContext) | |
| } | |
| fun compute(): OutputImageData { | |
| val shader = initShader() | |
| val computeProgram = GLES32.glCreateProgram() | |
| checkGlErrorOrThrow("glCreateProgram") | |
| GLES32.glAttachShader(computeProgram, shader) | |
| checkGlErrorOrThrow("glAttachShader") | |
| GLES32.glLinkProgram(computeProgram) | |
| val linkStatus = IntArray(1) | |
| GLES32.glGetProgramiv(computeProgram, GLES31.GL_LINK_STATUS, linkStatus, 0) | |
| if (linkStatus[0] != GLES32.GL_TRUE) { | |
| val error = GLES32.glGetProgramInfoLog(computeProgram) | |
| Timber.e("Program compilation failed $error") | |
| GLES32.glDeleteProgram(computeProgram) | |
| throw IllegalStateException("Program link failed: $error") | |
| } | |
| GLES32.glUseProgram(computeProgram) | |
| setUniformInputs(bitmap.width, bitmap.height) | |
| useInput(GpuComputeImageData(bitmap)) | |
| val (ssboId, size) = useOutput(bitmap.height * bitmap.width * Int.SIZE_BYTES) | |
| GLES32.glDispatchCompute(bitmap.width, bitmap.height, 1) | |
| GLES32.glMemoryBarrier(GLES32.GL_SHADER_STORAGE_BARRIER_BIT) | |
| checkGlErrorOrThrow("glUniform1i width") | |
| checkGlErrorOrThrow("glDispatchCompute") | |
| val output = readResult(ssboId, size, bitmap.width, bitmap.height) | |
| checkGlErrorOrThrow("readResult") | |
| return output | |
| } | |
| private fun initShader(): Int { | |
| val shader = GLES32.glCreateShader(GLES32.GL_COMPUTE_SHADER) | |
| checkGlErrorOrThrow("glCreateShader") | |
| GLES32.glShaderSource(shader, shaderProvider()) | |
| GLES32.glCompileShader(shader) | |
| checkGlErrorOrThrow("glCompileShader") | |
| val compiled = IntArray(1) | |
| GLES32.glGetShaderiv(shader, GLES32.GL_COMPILE_STATUS, compiled, 0) | |
| if (compiled[0] == GLES32.GL_FALSE) { | |
| val error = GLES32.glGetShaderInfoLog(shader) | |
| Timber.e(error) | |
| GLES32.glDeleteShader(shader) | |
| throw IllegalStateException("Shader compilation failed: $error") | |
| } | |
| return shader | |
| } | |
| private fun useOutput(imageSize: Int): Pair<Int, Int> { | |
| val (buffer, size) = OutputImageData.createBuffer(imageSize) | |
| val bo = intArrayOf(0) | |
| GLES32.glGenBuffers(1, bo, 0) | |
| GLES32.glBindBuffer(GLES32.GL_SHADER_STORAGE_BUFFER, bo[0]) | |
| GLES32.glBufferData( | |
| GLES32.GL_SHADER_STORAGE_BUFFER, | |
| size, | |
| buffer, | |
| GLES32.GL_STATIC_READ | |
| ) | |
| GLES32.glBindBufferBase(GLES32.GL_SHADER_STORAGE_BUFFER, OUTPUT_BINDING_INDEX, bo[0]) | |
| checkGlErrorOrThrow("useOutput") | |
| return Pair(bo[0], size) | |
| } | |
| private fun setUniformInputs(width: Int, height: Int) { | |
| GLES32.glUniform1i(WIDTH_BINDING_INDEX, width) | |
| checkGlErrorOrThrow("glUniform1i width") | |
| GLES32.glUniform1i(HEIGHT_BINDING_INDEX, height) | |
| checkGlErrorOrThrow("glUniform1i height") | |
| } | |
| private fun useInput(data: GpuComputeImageData) { | |
| val (inputBuffer, bufferSize) = data.toBuffer() | |
| val bo = intArrayOf(0) | |
| GLES32.glGenBuffers(1, bo, 0) | |
| GLES32.glBindBuffer(GLES32.GL_SHADER_STORAGE_BUFFER, bo[0]) | |
| GLES32.glBufferData( | |
| GLES32.GL_SHADER_STORAGE_BUFFER, | |
| bufferSize, | |
| inputBuffer, | |
| GLES32.GL_STATIC_READ | |
| ) | |
| GLES32.glBindBufferBase(GLES32.GL_SHADER_STORAGE_BUFFER, INPUT_BINDING_INDEX, bo[0]) | |
| checkGlErrorOrThrow("useInput") | |
| } | |
| private fun readResult(ssboId: Int, size: Int, width: Int, height: Int): OutputImageData { | |
| GLES32.glBindBuffer(GLES31.GL_SHADER_STORAGE_BUFFER, ssboId) | |
| val buffer = GLES32.glMapBufferRange( | |
| GLES31.GL_SHADER_STORAGE_BUFFER, | |
| 0, | |
| size, | |
| GLES31.GL_MAP_READ_BIT | |
| ) as ByteBuffer | |
| buffer.order(ByteOrder.nativeOrder()) | |
| val intBuffer = buffer.asIntBuffer() | |
| val outBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) | |
| outBitmap.copyPixelsFromBuffer(intBuffer) | |
| val result = OutputImageData(outBitmap) | |
| GLES32.glUnmapBuffer(GLES31.GL_SHADER_STORAGE_BUFFER) | |
| GLES32.glBindBuffer(GLES31.GL_SHADER_STORAGE_BUFFER, 0) | |
| return result | |
| } | |
| companion object { | |
| const val INPUT_BINDING_INDEX = 0 | |
| const val OUTPUT_BINDING_INDEX = 1 | |
| const val WIDTH_BINDING_INDEX = 2 | |
| const val HEIGHT_BINDING_INDEX = 3 | |
| } | |
| } | |
| private data class GpuComputeImageData( | |
| val bitmap: Bitmap | |
| ) | |
| private fun GpuComputeImageData.toBuffer(): Pair<ByteBuffer, Int> { | |
| val size = bitmap.height * bitmap.width * Int.SIZE_BYTES | |
| val buffer = createByteBuffer(size) | |
| bitmap.copyPixelsToBuffer(buffer) | |
| buffer.position(0) | |
| return buffer to size | |
| } | |
| data class OutputImageData( | |
| val bitmap: Bitmap | |
| ) { | |
| companion object | |
| } | |
| private fun OutputImageData.Companion.createBuffer(size: Int): Pair<ByteBuffer, Int> { | |
| val buffer = createRawBufferOfInts(size) | |
| return buffer to size | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #version 320 es | |
| layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in; | |
| layout(std430, binding=0) readonly buffer InputImage { | |
| int pixels[]; | |
| } inputImage; | |
| layout(std430, binding=1) writeonly buffer OutputImage { | |
| int pixels[]; | |
| } outputImage; | |
| layout(location = 2) uniform int width; | |
| layout(location = 3) uniform int height; | |
| int transform(int x, int y) { | |
| return inputImage.pixels[y * width + x]; | |
| } | |
| vec3 grayscale(vec3 color) { | |
| return vec3(color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722); | |
| } | |
| void main() { | |
| ivec2 storePos = ivec2(gl_GlobalInvocationID.xy); | |
| if (storePos.x >= width || storePos.y >= height) return; | |
| int pix = transform(storePos.x, storePos.y); | |
| // ARGB | |
| int alpha = (pix >> 24) & 0xff; | |
| float red = float((pix >> 16) & 0xff); | |
| float green = float((pix >> 8) & 0xff); | |
| float blue = float(pix & 0xff); | |
| vec3 floatColor = vec3(red, green, blue); | |
| vec3 transformed = grayscale(floatColor); | |
| int outPix = alpha << 24 | int(transformed.r) << 16 | int(transformed.g) << 8 | int(transformed.b); | |
| outputImage.pixels[storePos.y * width + storePos.x] = outPix; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment