feat: бинокулярный видеорендер в app-glasses + иммерсивный режим + тест-план

- render/: Grafika-связка (EglCore/WindowSurface/FullFrameRect/Texture2dProgram/GlUtil/Drawable2d/EglSurfaceBase) — перенос из pair-view
- ui/BinocularVideoPlayer: один SimpleExoPlayer → SurfaceTexture → EGL-блит в оба глаза (EglVideoRenderer шарится между LEFT/RIGHT)
- ui/ViewUtils: Binocular/BinocularScreenSide/BinocularScreenSize (Compose, из pair-view)
- ui/GlassesMovieScreen: «нос к носу», calculateVideoSizeFull, FLAG_KEEP_SCREEN_ON
- MovieController: PlayVideo/MediaCommand (PLAY/PAUSE/TOGGLE/SEEK/CLOSE), доступ к ExoPlayer только с main-потока
- HostConnection: PlayVideo→плеер, реальная позиция в PlaybackPosition (withContext Main)
- MainActivity: иммерсивный режим (setDecorFitsSystemWindows(false) + hide systemBars)
- TESTING.md: 10 тест-кейсов human-style; QA-прогон (opencode, 88.19) — 10/10 ПРОЙДЕН
This commit is contained in:
Hermes Agent
2026-08-19 04:15:55 +03:00
parent 298d8e0b69
commit 4458678698
16 changed files with 2148 additions and 7 deletions
@@ -0,0 +1,180 @@
package pw.binom.viewmate.glasses.render
import java.nio.FloatBuffer
class Drawable2d(shape: Prefab) {
companion object {
private const val SIZEOF_FLOAT: Int = 4
/**
* Simple equilateral triangle (1.0 per side). Centered on (0,0).
*/
private val TRIANGLE_COORDS: FloatArray = floatArrayOf(
0.0f, 0.577350269f, // 0 top
-0.5f, -0.288675135f, // 1 bottom left
0.5f, -0.288675135f // 2 bottom right
)
private val TRIANGLE_TEX_COORDS: FloatArray = floatArrayOf(
0.5f, 0.0f, // 0 top center
0.0f, 1.0f, // 1 bottom left
1.0f, 1.0f, // 2 bottom right
)
private val TRIANGLE_BUF: FloatBuffer = GlUtil.createFloatBuffer(TRIANGLE_COORDS)
private val TRIANGLE_TEX_BUF = GlUtil.createFloatBuffer(TRIANGLE_TEX_COORDS)
/**
* Simple square, specified as a triangle strip. The square is centered on (0,0) and has
* a size of 1x1.
*
* Triangles are 0-1-2 and 2-1-3 (counter-clockwise winding).
*/
private val RECTANGLE_COORDS = floatArrayOf(
-0.5f, -0.5f, // 0 bottom left
0.5f, -0.5f, // 1 bottom right
-0.5f, 0.5f, // 2 top left
0.5f, 0.5f, // 3 top right
)
private val RECTANGLE_TEX_COORDS = floatArrayOf(
0.0f, 1.0f, // 0 bottom left
1.0f, 1.0f, // 1 bottom right
0.0f, 0.0f, // 2 top left
1.0f, 0.0f // 3 top right
)
private val RECTANGLE_BUF = GlUtil.createFloatBuffer(RECTANGLE_COORDS)
private val RECTANGLE_TEX_BUF = GlUtil.createFloatBuffer(RECTANGLE_TEX_COORDS)
/**
* A "full" square, extending from -1 to +1 in both dimensions. When the model/view/projection
* matrix is identity, this will exactly cover the viewport.
*
* The texture coordinates are Y-inverted relative to RECTANGLE. (This seems to work out
* right with external textures from SurfaceTexture.)
*/
private val FULL_RECTANGLE_COORDS = floatArrayOf(
-1.0f, -1.0f, // 0 bottom left
1.0f, -1.0f, // 1 bottom right
-1.0f, 1.0f, // 2 top left
1.0f, 1.0f, // 3 top right
)
private val FULL_RECTANGLE_TEX_COORDS = floatArrayOf(
0.0f, 0.0f, // 0 bottom left
1.0f, 0.0f, // 1 bottom right
0.0f, 1.0f, // 2 top left
1.0f, 1.0f // 3 top right
)
private val FULL_RECTANGLE_BUF = GlUtil.createFloatBuffer(FULL_RECTANGLE_COORDS)
private val FULL_RECTANGLE_TEX_BUF = GlUtil.createFloatBuffer(FULL_RECTANGLE_TEX_COORDS)
}
val mVertexArray: FloatBuffer
val mTexCoordArray: FloatBuffer
private var mPrefab: Prefab? = null
val mVertexCount: Int
val mCoordsPerVertex: Int
val mVertexStride: Int
val mTexCoordStride: Int
/**
* Enum values for constructor.
*/
enum class Prefab {
TRIANGLE, RECTANGLE, FULL_RECTANGLE
}
/**
* Prepares a drawable from a "pre-fabricated" shape definition.
*
* Does no EGL/GL operations, so this can be done at any time.
*/
init {
when (shape) {
Prefab.TRIANGLE -> {
mVertexArray = TRIANGLE_BUF
mTexCoordArray = TRIANGLE_TEX_BUF
mCoordsPerVertex = 2
mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT
mVertexCount = TRIANGLE_COORDS.size / mCoordsPerVertex
}
Prefab.RECTANGLE -> {
mVertexArray = RECTANGLE_BUF
mTexCoordArray = RECTANGLE_TEX_BUF
mCoordsPerVertex = 2
mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT
mVertexCount = RECTANGLE_COORDS.size / mCoordsPerVertex
}
Prefab.FULL_RECTANGLE -> {
mVertexArray = FULL_RECTANGLE_BUF
mTexCoordArray = FULL_RECTANGLE_TEX_BUF
mCoordsPerVertex = 2
mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT
mVertexCount = FULL_RECTANGLE_COORDS.size / mCoordsPerVertex
}
else -> throw RuntimeException("Unknown shape $shape")
}
mTexCoordStride = 2 * SIZEOF_FLOAT
mPrefab = shape
}
/**
* Returns the array of vertices.
*
* To avoid allocations, this returns internal state. The caller must not modify it.
*/
fun getVertexArray() = mVertexArray
/**
* Returns the array of texture coordinates.
*
* To avoid allocations, this returns internal state. The caller must not modify it.
*/
fun getTexCoordArray() = mTexCoordArray
/**
* Returns the number of vertices stored in the vertex array.
*/
fun getVertexCount(): Int {
return mVertexCount
}
/**
* Returns the width, in bytes, of the data for each vertex.
*/
fun getVertexStride(): Int {
return mVertexStride
}
/**
* Returns the width, in bytes, of the data for each texture coordinate.
*/
fun getTexCoordStride(): Int {
return mTexCoordStride
}
/**
* Returns the number of position coordinates per vertex. This will be 2 or 3.
*/
fun getCoordsPerVertex(): Int {
return mCoordsPerVertex
}
override fun toString(): String {
return if (mPrefab != null) {
"[Drawable2d: $mPrefab]"
} else {
"[Drawable2d: ...]"
}
}
}
@@ -0,0 +1,348 @@
package pw.binom.viewmate.glasses.render
import android.graphics.SurfaceTexture
import android.opengl.EGL14
import android.opengl.EGLConfig
import android.opengl.EGLContext
import android.opengl.EGLDisplay
import android.opengl.EGLExt
import android.opengl.EGLSurface
import android.view.Surface
import pw.binom.viewmate.glasses.log
class EglCore {
companion object {
const val TAG = "GlUtil"
/**
* Constructor flag: surface must be recordable. This discourages EGL from using a
* pixel format that cannot be converted efficiently to something usable by the video
* encoder.
*/
const val FLAG_RECORDABLE: Int = 0x01
/**
* Constructor flag: ask for GLES3, fall back to GLES2 if not available. Without this
* flag, GLES2 is used.
*/
const val FLAG_TRY_GLES3: Int = 0x02
// Android-specific extension.
const val EGL_RECORDABLE_ANDROID: Int = 0x3142
}
private var mEGLDisplay: EGLDisplay? = EGL14.EGL_NO_DISPLAY
private var mEGLContext: EGLContext = EGL14.EGL_NO_CONTEXT
private var mEGLConfig: EGLConfig? = null
private var mGlVersion = -1
/**
* Prepares EGL display and context.
*
* Equivalent to EglCore(null, 0).
*/
constructor() : this(null, 0) {
}
/**
* Prepares EGL display and context.
*
* @param sharedContext The context to share, or null if sharing is not desired.
* @param flags Configuration bit flags, e.g. FLAG_RECORDABLE.
*/
constructor(sharedContext: EGLContext?, flags: Int) {
var sharedContext: EGLContext? = sharedContext
if (mEGLDisplay !== EGL14.EGL_NO_DISPLAY) {
throw RuntimeException("EGL already set up")
}
if (sharedContext == null) {
sharedContext = EGL14.EGL_NO_CONTEXT
}
mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
if (mEGLDisplay === EGL14.EGL_NO_DISPLAY) {
throw RuntimeException("unable to get EGL14 display")
}
val version = IntArray(2)
if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
mEGLDisplay = null
throw RuntimeException("unable to initialize EGL14")
}
// Try to get a GLES3 context, if requested.
if ((flags and FLAG_TRY_GLES3) != 0) {
val config: EGLConfig? = getConfig(flags, 3)
if (config != null) {
val attrib3_list = intArrayOf(
EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
EGL14.EGL_NONE
)
val context: EGLContext = EGL14.eglCreateContext(
mEGLDisplay, config, sharedContext,
attrib3_list, 0
)
if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
mEGLConfig = config
mEGLContext = context
mGlVersion = 3
}
}
}
if (mEGLContext === EGL14.EGL_NO_CONTEXT) { // GLES 2 only, or GLES 3 attempt failed
val config: EGLConfig = getConfig(flags, 2)
?: throw RuntimeException("Unable to find a suitable EGLConfig")
val attrib2_list = intArrayOf(
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL14.EGL_NONE
)
val context: EGLContext = EGL14.eglCreateContext(
mEGLDisplay, config, sharedContext,
attrib2_list, 0
)
checkEglError("eglCreateContext")
mEGLConfig = config
mEGLContext = context
mGlVersion = 2
}
// Confirm with query.
val values = IntArray(1)
EGL14.eglQueryContext(
mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION,
values, 0
)
log(TAG, "EGLContext created, client version " + values[0])
}
/**
* Finds a suitable EGLConfig.
*
* @param flags Bit flags from constructor.
* @param version Must be 2 or 3.
*/
private fun getConfig(flags: Int, version: Int): EGLConfig? {
var renderableType = EGL14.EGL_OPENGL_ES2_BIT
if (version >= 3) {
renderableType = renderableType or EGLExt.EGL_OPENGL_ES3_BIT_KHR
}
// The actual surface is generally RGBA or RGBX, so situationally omitting alpha
// doesn't really help. It can also lead to a huge performance hit on glReadPixels()
// when reading into a GL_RGBA buffer.
val attribList = 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, 16,
//EGL14.EGL_STENCIL_SIZE, 8,
EGL14.EGL_RENDERABLE_TYPE, renderableType,
EGL14.EGL_NONE, 0, // placeholder for recordable [@-3]
EGL14.EGL_NONE
)
if ((flags and FLAG_RECORDABLE) != 0) {
attribList[attribList.size - 3] = EGL_RECORDABLE_ANDROID
attribList[attribList.size - 2] = 1
}
val configs: Array<EGLConfig?> = arrayOfNulls<EGLConfig>(1)
val numConfigs = IntArray(1)
if (!EGL14.eglChooseConfig(
mEGLDisplay, attribList, 0, configs, 0, configs.size,
numConfigs, 0
)
) {
log(TAG, "unable to find RGB8888 / $version EGLConfig")
return null
}
return configs[0]
}
/**
* Discards all resources held by this class, notably the EGL context. This must be
* called from the thread where the context was created.
*
* On completion, no context will be current.
*/
fun release() {
if (mEGLDisplay !== EGL14.EGL_NO_DISPLAY) {
// Android is unusual in that it uses a reference-counted EGLDisplay. So for
// every eglInitialize() we need an eglTerminate().
EGL14.eglMakeCurrent(
mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
EGL14.EGL_NO_CONTEXT
)
EGL14.eglDestroyContext(mEGLDisplay, mEGLContext)
EGL14.eglReleaseThread()
EGL14.eglTerminate(mEGLDisplay)
}
mEGLDisplay = EGL14.EGL_NO_DISPLAY
mEGLContext = EGL14.EGL_NO_CONTEXT
mEGLConfig = null
}
/**
* Destroys the specified surface. Note the EGLSurface won't actually be destroyed if it's
* still current in a context.
*/
fun releaseSurface(eglSurface: EGLSurface?) {
EGL14.eglDestroySurface(mEGLDisplay, eglSurface)
}
fun createWindowSurface(surface: Surface): EGLSurface = createWindowSurface(surface)
fun createWindowSurface(surface: SurfaceTexture): EGLSurface = createWindowSurface(surface)
/**
* Creates an EGL surface associated with a Surface.
*
* If this is destined for MediaCodec, the EGLConfig should have the "recordable" attribute.
*/
fun createWindowSurface(surface: Any): EGLSurface {
if (surface !is Surface && surface !is SurfaceTexture) {
throw RuntimeException("invalid surface: $surface")
}
// Create a window surface, and attach it to the Surface we received.
val surfaceAttribs = intArrayOf(
EGL14.EGL_NONE
)
val eglSurface: EGLSurface? = EGL14.eglCreateWindowSurface(
mEGLDisplay, mEGLConfig, surface,
surfaceAttribs, 0
)
checkEglError("eglCreateWindowSurface")
if (eglSurface == null) {
throw RuntimeException("surface was null")
}
return eglSurface
}
/**
* Creates an EGL surface associated with an offscreen buffer.
*/
fun createOffscreenSurface(width: Int, height: Int): EGLSurface {
val surfaceAttribs = intArrayOf(
EGL14.EGL_WIDTH, width,
EGL14.EGL_HEIGHT, height,
EGL14.EGL_NONE
)
val eglSurface: EGLSurface? = EGL14.eglCreatePbufferSurface(
mEGLDisplay, mEGLConfig,
surfaceAttribs, 0
)
checkEglError("eglCreatePbufferSurface")
if (eglSurface == null) {
throw RuntimeException("surface was null")
}
return eglSurface
}
/**
* Makes our EGL context current, using the supplied surface for both "draw" and "read".
*/
fun makeCurrent(eglSurface: EGLSurface?) {
if (mEGLDisplay === EGL14.EGL_NO_DISPLAY) {
// called makeCurrent() before create?
log(TAG, "NOTE: makeCurrent w/o display")
}
if (!EGL14.eglMakeCurrent(mEGLDisplay, eglSurface, eglSurface, mEGLContext)) {
throw RuntimeException("eglMakeCurrent failed")
}
}
/**
* Makes our EGL context current, using the supplied "draw" and "read" surfaces.
*/
fun makeCurrent(drawSurface: EGLSurface?, readSurface: EGLSurface?) {
if (mEGLDisplay === EGL14.EGL_NO_DISPLAY) {
// called makeCurrent() before create?
log(TAG, "NOTE: makeCurrent w/o display")
}
if (!EGL14.eglMakeCurrent(mEGLDisplay, drawSurface, readSurface, mEGLContext)) {
throw RuntimeException("eglMakeCurrent(draw,read) failed")
}
}
/**
* Makes no context current.
*/
fun makeNothingCurrent() {
if (!EGL14.eglMakeCurrent(
mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
EGL14.EGL_NO_CONTEXT
)
) {
throw RuntimeException("eglMakeCurrent failed")
}
}
/**
* Calls eglSwapBuffers. Use this to "publish" the current frame.
*
* @return false on failure
*/
fun swapBuffers(eglSurface: EGLSurface?): Boolean {
return EGL14.eglSwapBuffers(mEGLDisplay, eglSurface)
}
/**
* Sends the presentation time stamp to EGL. Time is expressed in nanoseconds.
*/
fun setPresentationTime(eglSurface: EGLSurface?, nsecs: Long) {
EGLExt.eglPresentationTimeANDROID(mEGLDisplay, eglSurface, nsecs)
}
/**
* Returns true if our context and the specified surface are current.
*/
fun isCurrent(eglSurface: EGLSurface): Boolean {
return mEGLContext.equals(EGL14.eglGetCurrentContext()) &&
eglSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW))
}
/**
* Performs a simple surface query.
*/
fun querySurface(eglSurface: EGLSurface?, what: Int): Int {
val value = IntArray(1)
EGL14.eglQuerySurface(mEGLDisplay, eglSurface, what, value, 0)
return value[0]
}
/**
* Queries a string value.
*/
fun queryString(what: Int): String {
return EGL14.eglQueryString(mEGLDisplay, what)
}
/**
* Returns the GLES version this context is configured for (currently 2 or 3).
*/
fun getGlVersion(): Int {
return mGlVersion
}
/**
* Writes the current display, context, and surface to the log.
*/
fun logCurrent(msg: String) {
val display: EGLDisplay = EGL14.eglGetCurrentDisplay()
val context: EGLContext = EGL14.eglGetCurrentContext()
val surface: EGLSurface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW)
log(
TAG, "Current EGL ($msg): display=$display, context=$context, surface=$surface"
)
}
/**
* Checks for EGL errors. Throws an exception if an error has been raised.
*/
private fun checkEglError(msg: String) {
val error: Int
if ((EGL14.eglGetError().also { error = it }) != EGL14.EGL_SUCCESS) {
throw RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error))
}
}
}
@@ -0,0 +1,178 @@
package pw.binom.viewmate.glasses.render
import android.graphics.Bitmap
import android.opengl.EGL14
import android.opengl.EGLSurface
import android.opengl.GLES20
import pw.binom.viewmate.glasses.log
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrder
open class EglSurfaceBase {
companion object {
const val TAG: String = "GlUtil"
}
// EglCore object we're associated with. It may be associated with multiple surfaces.
protected lateinit var mEglCore: EglCore
private var mEGLSurface: EGLSurface = EGL14.EGL_NO_SURFACE
private var mWidth = -1
private var mHeight = -1
protected constructor(eglCore: EglCore) {
mEglCore = eglCore
}
/**
* Creates a window surface.
*
* @param surface May be a Surface or SurfaceTexture.
*/
fun createWindowSurface(surface: Any) {
check(mEGLSurface === EGL14.EGL_NO_SURFACE) { "surface already created" }
mEGLSurface = mEglCore.createWindowSurface(surface)
// Don't cache width/height here, because the size of the underlying surface can change
// out from under us (see e.g. HardwareScalerActivity).
//mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH);
//mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT);
}
/**
* Creates an off-screen surface.
*/
fun createOffscreenSurface(width: Int, height: Int) {
check(mEGLSurface === EGL14.EGL_NO_SURFACE) { "surface already created" }
mEGLSurface = mEglCore.createOffscreenSurface(width, height)
mWidth = width
mHeight = height
}
/**
* Returns the surface's width, in pixels.
*
* If this is called on a window surface, and the underlying surface is in the process
* of changing size, we may not see the new size right away (e.g. in the "surfaceChanged"
* callback). The size should match after the next buffer swap.
*/
fun getWidth(): Int {
return if (mWidth < 0) {
mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH)
} else {
mWidth
}
}
/**
* Returns the surface's height, in pixels.
*/
fun getHeight(): Int {
return if (mHeight < 0) {
mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT)
} else {
mHeight
}
}
/**
* Release the EGL surface.
*/
fun releaseEglSurface() {
mEglCore.releaseSurface(mEGLSurface)
mEGLSurface = EGL14.EGL_NO_SURFACE
mHeight = -1
mWidth = mHeight
}
/**
* Makes our EGL context and surface current.
*/
fun makeCurrent() {
mEglCore.makeCurrent(mEGLSurface)
}
/**
* Makes our EGL context and surface current for drawing, using the supplied surface
* for reading.
*/
fun makeCurrentReadFrom(readSurface: EglSurfaceBase) {
mEglCore.makeCurrent(mEGLSurface, readSurface.mEGLSurface)
}
/**
* Calls eglSwapBuffers. Use this to "publish" the current frame.
*
* @return false on failure
*/
fun swapBuffers(): Boolean {
val result: Boolean = mEglCore.swapBuffers(mEGLSurface)
if (!result) {
log(TAG, "WARNING: swapBuffers() failed")
}
return result
}
/**
* Sends the presentation time stamp to EGL.
*
* @param nsecs Timestamp, in nanoseconds.
*/
fun setPresentationTime(nsecs: Long) {
mEglCore.setPresentationTime(mEGLSurface, nsecs)
}
/**
* Saves the EGL surface to a file.
*
* Expects that this object's EGL surface is current.
*/
@Throws(IOException::class)
fun saveFrame(file: File) {
if (!mEglCore.isCurrent(mEGLSurface)) {
throw RuntimeException("Expected EGL context/surface is not current")
}
// glReadPixels fills in a "direct" ByteBuffer with what is essentially big-endian RGBA
// data (i.e. a byte of red, followed by a byte of green...). While the Bitmap
// constructor that takes an int[] wants little-endian ARGB (blue/red swapped), the
// Bitmap "copy pixels" method wants the same format GL provides.
val filename = file.toString()
val width = getWidth()
val height = getHeight()
val buf = ByteBuffer.allocateDirect(width * height * 4)
buf.order(ByteOrder.LITTLE_ENDIAN)
GLES20.glReadPixels(
0, 0, width, height,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf
)
checkGlError("glReadPixels")
buf.rewind()
var bos: BufferedOutputStream? = null
try {
bos = BufferedOutputStream(FileOutputStream(filename))
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bmp.copyPixelsFromBuffer(buf)
bmp.compress(Bitmap.CompressFormat.PNG, 90, bos)
bmp.recycle()
} finally {
bos?.close()
}
log(TAG, "Saved $width" + "x" + "$height frame as '$filename'")
}
private fun checkGlError(op: String) {
val error = GLES20.glGetError()
if (error != GLES20.GL_NO_ERROR) {
val msg = op + ": glError 0x" + Integer.toHexString(error)
log(TAG, msg)
throw RuntimeException(msg)
}
}
}
@@ -0,0 +1,70 @@
package pw.binom.viewmate.glasses.render
/**
* @param program The program to use. FullFrameRect takes ownership, and will release
* the program when no longer needed.
*/
class FullFrameRect(program: Texture2dProgram) {
private val mRectDrawable = Drawable2d(Drawable2d.Prefab.FULL_RECTANGLE)
private var mProgram: Texture2dProgram? = program
/**
* Releases resources.
*
* This must be called with the appropriate EGL context current (i.e. the one that was
* current when the constructor was called). If we're about to destroy the EGL context,
* there's no value in having the caller make it current just to do this cleanup, so you
* can pass a flag that will tell this function to skip any EGL-context-specific cleanup.
*/
fun release(doEglCleanup: Boolean) {
if (doEglCleanup) {
mProgram?.release()
}
mProgram = null
}
/**
* Returns the program currently in use.
*/
fun getProgram(): Texture2dProgram? = mProgram
/**
* Changes the program. The previous program will be released.
*
* The appropriate EGL context must be current.
*/
fun changeProgram(program: Texture2dProgram?) {
mProgram?.release()
mProgram = program
}
/**
* Creates a texture object suitable for use with drawFrame().
*/
fun createTextureObject(): Int {
val mProgram = mProgram
checkNotNull(mProgram) { "FrameRect already closed" }
return mProgram.createTextureObject()
}
/**
* Draws a viewport-filling rect, texturing it with the specified texture object.
*/
fun drawFrame(textureId: Int, texMatrix: FloatArray) {
val mProgram = mProgram
checkNotNull(mProgram) { "FrameRect already closed" }
// Use the identity matrix for MVP so our 2x2 FULL_RECTANGLE covers the viewport.
mProgram.draw(
mvpMatrix = GlUtil.IDENTITY_MATRIX,
vertexBuffer = mRectDrawable.mVertexArray,
firstVertex = 0,
vertexCount = mRectDrawable.mVertexCount,
coordsPerVertex = mRectDrawable.mCoordsPerVertex,
vertexStride = mRectDrawable.mVertexStride,
texMatrix = texMatrix,
texBuffer = mRectDrawable.mTexCoordArray,
textureId = textureId,
texStride = mRectDrawable.mTexCoordStride,
)
}
}
@@ -0,0 +1,177 @@
package pw.binom.viewmate.glasses.render
import android.opengl.GLES20
import android.opengl.GLES30
import android.opengl.Matrix
import pw.binom.viewmate.glasses.log
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
object GlUtil {
const val TAG: String = "Grafika"
/** Identity matrix for general use. Don't modify or life will get weird. */
val IDENTITY_MATRIX = FloatArray(16).also {
Matrix.setIdentityM(it, 0)
}
private const val SIZEOF_FLOAT: Int = 4
/**
* Creates a new program from the supplied vertex and fragment shaders.
*
* @return A handle to the program, or 0 on failure.
*/
fun createProgram(vertexSource: String?, fragmentSource: String?): Int {
val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource)
if (vertexShader == 0) {
error("Can't create Vertex Shader")
return 0
}
val pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource)
if (pixelShader == 0) {
error("Can't create Pixel Shader")
return 0
}
var program = GLES20.glCreateProgram()
checkGlError("glCreateProgram")
if (program == 0) {
log(TAG, "Could not create program")
}
GLES20.glAttachShader(program, vertexShader)
checkGlError("glAttachShader")
GLES20.glAttachShader(program, pixelShader)
checkGlError("glAttachShader")
GLES20.glLinkProgram(program)
val linkStatus = IntArray(1)
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0)
if (linkStatus[0] != GLES20.GL_TRUE) {
log(TAG, "Could not link program: ")
log(TAG, GLES20.glGetProgramInfoLog(program))
GLES20.glDeleteProgram(program)
program = 0
}
return program
}
/**
* Compiles the provided shader source.
*
* @return A handle to the shader, or 0 on failure.
*/
fun loadShader(shaderType: Int, source: String?): Int {
var shader = GLES20.glCreateShader(shaderType)
checkGlError("glCreateShader type=$shaderType")
GLES20.glShaderSource(shader, source)
GLES20.glCompileShader(shader)
val compiled = IntArray(1)
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0)
if (compiled[0] == 0) {
log(TAG, "Could not compile shader $shaderType:")
log(TAG, " " + GLES20.glGetShaderInfoLog(shader))
GLES20.glDeleteShader(shader)
shader = 0
}
return shader
}
/**
* Checks to see if a GLES error has been raised.
*/
fun checkGlError(op: String) {
val error = GLES20.glGetError()
if (error != GLES20.GL_NO_ERROR) {
val msg = op + ": glError 0x" + Integer.toHexString(error)
log(TAG, msg)
throw RuntimeException(msg)
}
}
/**
* Checks to see if the location we obtained is valid. GLES returns -1 if a label
* could not be found, but does not set the GL error.
*
* Throws a RuntimeException if the location is invalid.
*/
fun checkLocation(location: Int, label: String) {
if (location < 0) {
throw RuntimeException("Unable to locate '$label' in program")
}
}
/**
* Creates a texture from raw data.
*
* @param data Image data, in a "direct" ByteBuffer.
* @param width Texture width, in pixels (not bytes).
* @param height Texture height, in pixels.
* @param format Image data format (use constant appropriate for glTexImage2D(), e.g. GL_RGBA).
* @return Handle to texture.
*/
fun createImageTexture(data: ByteBuffer?, width: Int, height: Int, format: Int): Int {
val textureHandles = IntArray(1)
GLES20.glGenTextures(1, textureHandles, 0)
val textureHandle = textureHandles[0]
GlUtil.checkGlError("glGenTextures")
// Bind the texture handle to the 2D texture target.
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle)
// Configure min/mag filtering, i.e. what scaling method do we use if what we're rendering
// is smaller or larger than the source image.
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_LINEAR
)
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR
)
GlUtil.checkGlError("loadImageTexture")
// Load the data from the buffer into the texture handle.
GLES20.glTexImage2D(
GLES20.GL_TEXTURE_2D, /*level*/0, format,
width, height, /*border*/0, format, GLES20.GL_UNSIGNED_BYTE, data
)
GlUtil.checkGlError("loadImageTexture")
return textureHandle
}
/**
* Allocates a direct float buffer, and populates it with the float array data.
*/
fun createFloatBuffer(coords: FloatArray): FloatBuffer {
// Allocate a direct ByteBuffer, using 4 bytes per float, and copy coords into it.
val bb = ByteBuffer.allocateDirect(coords.size * SIZEOF_FLOAT)
bb.order(ByteOrder.nativeOrder())
val fb = bb.asFloatBuffer()
fb.put(coords)
fb.position(0)
return fb
}
/**
* Writes GL version info to the log.
*/
fun logVersionInfo() {
log(TAG, "vendor : " + GLES20.glGetString(GLES20.GL_VENDOR))
log(TAG, "renderer: " + GLES20.glGetString(GLES20.GL_RENDERER))
log(TAG, "version : " + GLES20.glGetString(GLES20.GL_VERSION))
if (false) {
val values = IntArray(1)
GLES30.glGetIntegerv(GLES30.GL_MAJOR_VERSION, values, 0)
val majorVersion = values[0]
GLES30.glGetIntegerv(GLES30.GL_MINOR_VERSION, values, 0)
val minorVersion = values[0]
if (GLES30.glGetError() == GLES30.GL_NO_ERROR) {
log(TAG, "iversion: $majorVersion.$minorVersion")
}
}
}
}
@@ -0,0 +1,311 @@
package pw.binom.viewmate.glasses.render
import android.opengl.GLES11Ext
import android.opengl.GLES20
import pw.binom.viewmate.glasses.log
import pw.binom.viewmate.glasses.render.GlUtil.checkGlError
import pw.binom.viewmate.glasses.render.GlUtil.checkLocation
import pw.binom.viewmate.glasses.render.GlUtil.createProgram
import java.nio.FloatBuffer
class Texture2dProgram(private val mProgramType: ProgramType) {
companion object {
private const val TAG: String = GlUtil.TAG
// Simple vertex shader, used for all programs.
private const val VERTEX_SHADER = "uniform mat4 uMVPMatrix;\n" +
"uniform mat4 uTexMatrix;\n" +
"attribute vec4 aPosition;\n" +
"attribute vec4 aTextureCoord;\n" +
"varying vec2 vTextureCoord;\n" +
"void main() {\n" +
" gl_Position = uMVPMatrix * aPosition;\n" +
" vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" +
"}\n"
// Simple fragment shader for use with "normal" 2D textures.
private const val FRAGMENT_SHADER_2D = "precision mediump float;\n" +
"varying vec2 vTextureCoord;\n" +
"uniform sampler2D sTexture;\n" +
"void main() {\n" +
" gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +
"}\n"
// Simple fragment shader for use with external 2D textures (e.g. what we get from
// SurfaceTexture).
private const val FRAGMENT_SHADER_EXT = "#extension GL_OES_EGL_image_external : require\n" +
"precision mediump float;\n" +
"varying vec2 vTextureCoord;\n" +
"uniform samplerExternalOES sTexture;\n" +
"void main() {\n" +
" gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +
"}\n"
// Fragment shader that converts color to black & white with a simple transformation.
private val FRAGMENT_SHADER_EXT_BW = "#extension GL_OES_EGL_image_external : require\n" +
"precision mediump float;\n" +
"varying vec2 vTextureCoord;\n" +
"uniform samplerExternalOES sTexture;\n" +
"void main() {\n" +
" vec4 tc = texture2D(sTexture, vTextureCoord);\n" +
" float color = tc.r * 0.3 + tc.g * 0.59 + tc.b * 0.11;\n" +
" gl_FragColor = vec4(color, color, color, 1.0);\n" +
"}\n"
// Fragment shader with a convolution filter. The upper-left half will be drawn normally,
// the lower-right half will have the filter applied, and a thin red line will be drawn
// at the border.
const val KERNEL_SIZE = 9
private val FRAGMENT_SHADER_EXT_FILT = "#extension GL_OES_EGL_image_external : require\n" +
"#define KERNEL_SIZE " + KERNEL_SIZE + "\n" +
"precision highp float;\n" +
"varying vec2 vTextureCoord;\n" +
"uniform samplerExternalOES sTexture;\n" +
"uniform float uKernel[KERNEL_SIZE];\n" +
"uniform vec2 uTexOffset[KERNEL_SIZE];\n" +
"uniform float uColorAdjust;\n" +
"void main() {\n" +
" int i = 0;\n" +
" vec4 sum = vec4(0.0);\n" +
" if (vTextureCoord.x < vTextureCoord.y - 0.005) {\n" +
" for (i = 0; i < KERNEL_SIZE; i++) {\n" +
" vec4 texc = texture2D(sTexture, vTextureCoord + uTexOffset[i]);\n" +
" sum += texc * uKernel[i];\n" +
" }\n" +
" sum += uColorAdjust;\n" +
" } else if (vTextureCoord.x > vTextureCoord.y + 0.005) {\n" +
" sum = texture2D(sTexture, vTextureCoord);\n" +
" } else {\n" +
" sum.r = 1.0;\n" +
" }\n" +
" gl_FragColor = sum;\n" +
"}\n"
}
enum class ProgramType {
TEXTURE_2D, TEXTURE_EXT, TEXTURE_EXT_BW, TEXTURE_EXT_FILT
}
// Handles to the GL program and various components of it.
private var mProgramHandle = 0
private val muMVPMatrixLoc: Int
private val muTexMatrixLoc: Int
private var muKernelLoc = 0
private val muTexOffsetLoc: Int
private val muColorAdjustLoc: Int
private val maPositionLoc: Int
private val maTextureCoordLoc: Int
private val mTextureTarget: Int
private val mKernel = FloatArray(KERNEL_SIZE)
private var mTexOffset: FloatArray? = null
private var mColorAdjust = 0f
/**
* Prepares the program in the current EGL context.
*/
init {
when (mProgramType) {
ProgramType.TEXTURE_2D -> {
mTextureTarget = GLES20.GL_TEXTURE_2D
mProgramHandle = createProgram(VERTEX_SHADER, FRAGMENT_SHADER_2D)
}
ProgramType.TEXTURE_EXT -> {
mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES
mProgramHandle = createProgram(VERTEX_SHADER, FRAGMENT_SHADER_EXT)
}
ProgramType.TEXTURE_EXT_BW -> {
mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES
mProgramHandle = createProgram(VERTEX_SHADER, FRAGMENT_SHADER_EXT_BW)
}
ProgramType.TEXTURE_EXT_FILT -> {
mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES
mProgramHandle = createProgram(VERTEX_SHADER, FRAGMENT_SHADER_EXT_FILT)
}
else -> throw RuntimeException("Unhandled type " + mProgramType)
}
if (mProgramHandle == 0) {
throw RuntimeException("Unable to create program")
}
log(TAG, "Created program $mProgramHandle ($mProgramType)")
// get locations of attributes and uniforms
maPositionLoc = GLES20.glGetAttribLocation(mProgramHandle, "aPosition")
checkLocation(maPositionLoc, "aPosition")
maTextureCoordLoc = GLES20.glGetAttribLocation(mProgramHandle, "aTextureCoord")
checkLocation(maTextureCoordLoc, "aTextureCoord")
muMVPMatrixLoc = GLES20.glGetUniformLocation(mProgramHandle, "uMVPMatrix")
checkLocation(muMVPMatrixLoc, "uMVPMatrix")
muTexMatrixLoc = GLES20.glGetUniformLocation(mProgramHandle, "uTexMatrix")
checkLocation(muTexMatrixLoc, "uTexMatrix")
muKernelLoc = GLES20.glGetUniformLocation(mProgramHandle, "uKernel")
if (muKernelLoc < 0) {
// no kernel in this one
muKernelLoc = -1
muTexOffsetLoc = -1
muColorAdjustLoc = -1
} else {
// has kernel, must also have tex offset and color adj
muTexOffsetLoc = GLES20.glGetUniformLocation(mProgramHandle, "uTexOffset")
checkLocation(muTexOffsetLoc, "uTexOffset")
muColorAdjustLoc = GLES20.glGetUniformLocation(mProgramHandle, "uColorAdjust")
checkLocation(muColorAdjustLoc, "uColorAdjust")
// initialize default values
setKernel(floatArrayOf(0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f), 0f)
setTexSize(256, 256)
}
}
/**
* Releases the program.
*
* The appropriate EGL context must be current (i.e. the one that was used to create
* the program).
*/
fun release() {
log(TAG, "deleting program $mProgramHandle")
GLES20.glDeleteProgram(mProgramHandle)
mProgramHandle = -1
}
/**
* Returns the program type.
*/
fun getProgramType() = mProgramType
/**
* Creates a texture object suitable for use with this program.
*
* On exit, the texture will be bound.
*/
fun createTextureObject(): Int {
val textures = IntArray(1)
GLES20.glGenTextures(1, textures, 0)
checkGlError("glGenTextures")
val texId = textures[0]
GLES20.glBindTexture(mTextureTarget, texId)
checkGlError("glBindTexture $texId")
GLES20.glTexParameterf(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_NEAREST.toFloat()
)
GLES20.glTexParameterf(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR.toFloat()
)
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_CLAMP_TO_EDGE
)
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_CLAMP_TO_EDGE
)
checkGlError("glTexParameter")
return texId
}
/**
* Configures the convolution filter values.
*
* @param values Normalized filter values; must be KERNEL_SIZE elements.
*/
fun setKernel(values: FloatArray, colorAdj: Float) {
require(values.size == KERNEL_SIZE) {
"Kernel size is " + values.size +
" vs. " + KERNEL_SIZE
}
System.arraycopy(values, 0, mKernel, 0, KERNEL_SIZE)
mColorAdjust = colorAdj
}
/**
* Sets the size of the texture. This is used to find adjacent texels when filtering.
*/
fun setTexSize(width: Int, height: Int) {
val rw = 1.0f / width
val rh = 1.0f / height
// Don't need to create a new array here, but it's syntactically convenient.
mTexOffset = floatArrayOf(
-rw, -rh, 0f, -rh, rw, -rh,
-rw, 0f, 0f, 0f, rw, 0f,
-rw, rh, 0f, rh, rw, rh
)
}
/**
* Issues the draw call. Does the full setup on every call.
*/
fun draw(
mvpMatrix: FloatArray?, vertexBuffer: FloatBuffer, firstVertex: Int,
vertexCount: Int, coordsPerVertex: Int, vertexStride: Int,
texMatrix: FloatArray?, texBuffer: FloatBuffer, textureId: Int, texStride: Int
) {
checkGlError("draw start")
// Select the program.
GLES20.glUseProgram(mProgramHandle)
checkGlError("glUseProgram")
// Set the texture.
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(mTextureTarget, textureId)
// Copy the model / view / projection matrix over.
GLES20.glUniformMatrix4fv(muMVPMatrixLoc, 1, false, mvpMatrix, 0)
checkGlError("glUniformMatrix4fv")
// Copy the texture transformation matrix over.
GLES20.glUniformMatrix4fv(muTexMatrixLoc, 1, false, texMatrix, 0)
checkGlError("glUniformMatrix4fv")
// Enable the "aPosition" vertex attribute.
GLES20.glEnableVertexAttribArray(maPositionLoc)
checkGlError("glEnableVertexAttribArray")
// Connect vertexBuffer to "aPosition".
GLES20.glVertexAttribPointer(
maPositionLoc, coordsPerVertex,
GLES20.GL_FLOAT, false, vertexStride, vertexBuffer
)
checkGlError("glVertexAttribPointer")
// Enable the "aTextureCoord" vertex attribute.
GLES20.glEnableVertexAttribArray(maTextureCoordLoc)
checkGlError("glEnableVertexAttribArray")
// Connect texBuffer to "aTextureCoord".
GLES20.glVertexAttribPointer(
maTextureCoordLoc, 2,
GLES20.GL_FLOAT, false, texStride, texBuffer
)
checkGlError("glVertexAttribPointer")
// Populate the convolution kernel, if present.
if (muKernelLoc >= 0) {
GLES20.glUniform1fv(muKernelLoc, KERNEL_SIZE, mKernel, 0)
GLES20.glUniform2fv(muTexOffsetLoc, KERNEL_SIZE, mTexOffset, 0)
GLES20.glUniform1f(muColorAdjustLoc, mColorAdjust)
}
// Draw the rect.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, firstVertex, vertexCount)
checkGlError("glDrawArrays")
// Done -- disable vertex array, texture, and program.
GLES20.glDisableVertexAttribArray(maPositionLoc)
GLES20.glDisableVertexAttribArray(maTextureCoordLoc)
GLES20.glBindTexture(mTextureTarget, 0)
GLES20.glUseProgram(0)
}
}
@@ -0,0 +1,67 @@
package pw.binom.viewmate.glasses.render
import android.graphics.SurfaceTexture
import android.view.Surface
class WindowSurface : EglSurfaceBase {
private var mSurface: Surface? = null
private var mReleaseSurface = false
/**
* Associates an EGL surface with the native window surface.
*
* Set releaseSurface to true if you want the Surface to be released when release() is
* called. This is convenient, but can interfere with framework classes that expect to
* manage the Surface themselves (e.g. if you release a SurfaceView's Surface, the
* surfaceDestroyed() callback won't fire).
*/
constructor(eglCore: EglCore, surface: Surface, releaseSurface: Boolean) : super(eglCore) {
createWindowSurface(surface)
mSurface = surface
mReleaseSurface = releaseSurface
}
/**
* Associates an EGL surface with the SurfaceTexture.
*/
constructor(eglCore: EglCore, surfaceTexture: SurfaceTexture) : super(eglCore) {
createWindowSurface(surfaceTexture)
}
/**
* Releases any resources associated with the EGL surface (and, if configured to do so,
* with the Surface as well).
*
* Does not require that the surface's EGL context be current.
*/
fun release() {
releaseEglSurface()
val mSurface = mSurface
if (mSurface != null) {
if (mReleaseSurface) {
mSurface.release()
}
this.mSurface = null
}
}
/**
* Recreate the EGLSurface, using the new EglBase. The caller should have already
* freed the old EGLSurface with releaseEglSurface().
*
* This is useful when we want to update the EGLSurface associated with a Surface.
* For example, if we want to share with a different EGLContext, which can only
* be done by tearing down and recreating the context. (That's handled by the caller;
* this just creates a new EGLSurface for the Surface we were handed earlier.)
*
* If the previous EGLSurface isn't fully destroyed, e.g. it's still current on a
* context somewhere, the create call will fail with complaints from the Surface
* about already being connected.
*/
fun recreate(newEglCore: EglCore) {
val mSurface = mSurface
checkNotNull(mSurface) { "not yet implemented for SurfaceTexture" }
mEglCore = newEglCore // switch to new context
createWindowSurface(mSurface) // create new surface
}
}