Fix one landscape orientation rendering image upside down

`SurfaceTexture.getTransformMatrix()` for a custom SurfaceProvider does
not vary with `Preview.targetRotation` — it only encodes the static
sensor-to-buffer transform. The v1.1.8 rebind-on-rotation fix did
update Preview's target rotation, but the matrix returned to the GL
renderer was identical across all four device orientations. Combined
with the activity rotating under fullSensor (so the GL clip-space "up"
direction tracks the device, not the world), one of the two landscape
orientations rendered the image upside down on a real device. The
emulator masked this because its virtual scene is roughly symmetric.

Compose the missing piece on the GL thread: rotate the texcoord
sampling pattern around its centre by the inverse of the activity
rotation before sampling the camera texture. The four orientations
now produce four distinct matrices, keeping world-up at screen-up in
all of them while leaving portrait unchanged.

Bump to 1.1.9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-05-11 14:46:28 +02:00
commit 6d7be66341
2 changed files with 45 additions and 5 deletions

View file

@ -5,6 +5,7 @@ import android.graphics.SurfaceTexture
import android.opengl.GLES11Ext
import android.opengl.GLES20
import android.opengl.GLSurfaceView
import android.opengl.Matrix
import android.util.Log
import android.view.Surface
import java.nio.ByteBuffer
@ -56,6 +57,8 @@ class TiltShiftRenderer(
// SurfaceTexture transform matrix, refreshed each frame on the GL thread.
private val texMatrix = FloatArray(16)
// Sensor-to-buffer matrix from SurfaceTexture before display-rotation correction.
private val sensorMatrix = FloatArray(16)
// Current effect parameters (updated from UI thread)
@Volatile
@ -127,9 +130,11 @@ class TiltShiftRenderer(
override fun onDrawFrame(gl: GL10?) {
val st = surfaceTexture
st?.updateTexImage()
// Pull the latest sensor-to-display transform; updated by SurfaceTexture each frame
// and reflects whatever rotation CameraX requested via Preview.targetRotation.
st?.getTransformMatrix(texMatrix)
// SurfaceTexture's transform matrix only handles the sensor-to-buffer
// orientation; for a custom SurfaceProvider it does NOT vary with
// Preview.targetRotation. Apply the display-rotation correction here.
st?.getTransformMatrix(sensorMatrix)
composeTexMatrix()
if (vertexBufferDirty) {
recomputeVertices()
@ -199,6 +204,41 @@ class TiltShiftRenderer(
}
}
/**
* Combines the sensor-to-buffer matrix with a rotation that compensates for
* the activity's current rotation. The activity rotates with the device
* (screenOrientation="fullSensor"), so the GL clip-space "up" direction
* tracks the device rather than the world. To keep world-up at screen-up
* regardless of orientation, rotate the texcoord sampling pattern by the
* inverse of the activity rotation. Without this correction, the two
* landscape orientations would render the same matrix and one would appear
* upside-down on a real device.
*/
private fun composeTexMatrix() {
// Inverse of the activity rotation: rotating the sampling pattern by
// -activityAngle puts the world-aligned point originally at screen P
// at the same screen P after the activity has rotated.
val angle = when (displayRotation) {
Surface.ROTATION_90 -> -90f
Surface.ROTATION_180 -> 180f
Surface.ROTATION_270 -> 90f
else -> 0f
}
if (angle == 0f) {
System.arraycopy(sensorMatrix, 0, texMatrix, 0, 16)
return
}
// Build rotation around the (0.5, 0.5) texcoord center.
Matrix.setIdentityM(texMatrix, 0)
Matrix.translateM(texMatrix, 0, 0.5f, 0.5f, 0f)
Matrix.rotateM(texMatrix, 0, angle, 0f, 0f, 1f)
Matrix.translateM(texMatrix, 0, -0.5f, -0.5f, 0f)
// texMatrix = sensorMatrix * rotation (rotation is applied to the
// texcoord first, then the sensor-to-buffer transform).
val rot = texMatrix.copyOf()
Matrix.multiplyMM(texMatrix, 0, sensorMatrix, 0, rot, 0)
}
fun release() {
shader.release()
surfaceTexture?.release()

View file

@ -1,4 +1,4 @@
versionMajor=1
versionMinor=1
versionPatch=8
versionCode=10
versionPatch=9
versionCode=11