android: Use OpenGLES2 for rendering the screen instead of ImageView

Former-commit-id: 80d96928dabaa215d0b8812e203d65193ac85cc8
This commit is contained in:
Michel Heily 2020-02-29 21:05:00 +02:00
parent b270265694
commit 46287ca88d
5 changed files with 279 additions and 25 deletions

View file

@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mrmichel.rustdroid_emu"> package="com.mrmichel.rustdroid_emu">
<uses-feature android:glEsVersion="0x00200000" android:required="true"></uses-feature>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

View file

@ -21,7 +21,6 @@ import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.Switch; import android.widget.Switch;
import android.widget.Toast; import android.widget.Toast;
@ -53,8 +52,8 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
private AudioThread audioThread; private AudioThread audioThread;
private AudioTrack audioTrack; private AudioTrack audioTrack;
private byte[] on_resume_saved_state = null; private byte[] on_resume_saved_state = null;
private ImageView screen;
private boolean turboMode = false; private boolean turboMode = false;
private GbaScreenView gbaScreenView;
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@ -233,7 +232,6 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
this.bios = getIntent().getByteArrayExtra("bios"); this.bios = getIntent().getByteArrayExtra("bios");
this.screen = findViewById(R.id.gbaMockImageView);
this.emulator = new Emulator(); this.emulator = new Emulator();
if (Build.VERSION.SDK_INT >= 23) { if (Build.VERSION.SDK_INT >= 23) {
@ -260,6 +258,8 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
AudioTrack.MODE_STREAM); AudioTrack.MODE_STREAM);
} }
this.audioTrack.play(); this.audioTrack.play();
this.gbaScreenView = findViewById(R.id.gba_view);
} }
@Override @Override
@ -336,10 +336,6 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
startActivityForResult(intent, LOAD_SNAPSHOT_REQUESTCODE); startActivityForResult(intent, LOAD_SNAPSHOT_REQUESTCODE);
} }
public void updateScreen(Bitmap bmp) {
this.screen.setImageBitmap(bmp);
}
private class EmulationRunnable implements Runnable { private class EmulationRunnable implements Runnable {
public static final long NANOSECONDS_PER_MILLISECOND = 1000000; public static final long NANOSECONDS_PER_MILLISECOND = 1000000;
@ -373,14 +369,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
} }
} }
emulatorActivity.runOnUiThread(new Runnable() { emulatorActivity.gbaScreenView.updateFrame(emulator.getFrameBuffer());
Bitmap bitmap = Bitmap.createBitmap(emulator.getFrameBuffer(), 240, 160, Bitmap.Config.RGB_565);
@Override
public void run() {
emulatorActivity.updateScreen(bitmap);
}
});
} }
public void pauseEmulation() { public void pauseEmulation() {

View file

@ -0,0 +1,240 @@
package com.mrmichel.rustdroid_emu.ui;
import android.content.Context;
import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.GLUtils;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class GbaScreenView extends GLSurfaceView implements GLSurfaceView.Renderer {
ScreenTexture texture;
/**
* Private class to manage the screen texture rendering
*/
private class ScreenTexture {
int shaderProgram;
int positionHandle;
int texCoordHandle;
int samplerHandle;
int textureId;
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBuffer indicesBuffer;
private Bitmap bitmap;
// square vertices
private float[] vertices = {
-1.0f, 1.0f, 0.0f, // top left
-1.0f, -1.0f, 0.0f, // bottom left
1.0f, -1.0f, 0.0f, // bottom right
1.0f, 1.0f, 0.0f, // top right
};
// texture space vertices
private float[] textureVertices = {
0.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f,
1.0f, 0.0f
};
// two triangles compose a rect
private byte[] indicies = {
0, 1, 2,
0, 2, 3
};
private static final String VERTEX_SHADER_CODE =
"attribute vec4 a_position; \n" +
"attribute vec2 a_texCoord; \n" +
"varying vec2 v_texCoord; \n" +
"void main() \n" +
"{ \n" +
" gl_Position = a_position; \n" +
" v_texCoord = a_texCoord; \n" +
"} \n";
private static final String FRAGMENT_SHADER_CODE =
"precision mediump float; \n" +
"varying vec2 v_texCoord; \n" +
"uniform sampler2D s_texture; \n" +
"void main() \n" +
"{ \n" +
" gl_FragColor = texture2D( s_texture, v_texCoord );\n" +
"} \n";
private int compileShader(int type, String code) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, code);
GLES20.glCompileShader(shader);
return shader;
}
private void update(int[] frameBuffer) {
bitmap.setPixels(frameBuffer, 0, 240, 0, 0, 240, 160);
}
private int createShaderProgram(String vertexShaderCode, String fragmentShaderCode) {
int vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
int program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
return program;
}
private int createTexture() {
int[] texturesIds = new int[1];
GLES20.glGenTextures(1, texturesIds, 0);
if (texturesIds[0] == GLES20.GL_FALSE) {
throw new RuntimeException("Error loading texture");
}
// bind the texture
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texturesIds[0]);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
// set the parameters
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);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
return texturesIds[0];
}
public ScreenTexture() {
this.bitmap = Bitmap.createBitmap(240, 160, Bitmap.Config.RGB_565);
GLES20.glEnable(GLES20.GL_TEXTURE_2D);
// create vertex array
vertexBuffer = ByteBuffer.allocateDirect(vertices.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);
// create texture coordinate array
textureBuffer = ByteBuffer.allocateDirect(textureVertices.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
textureBuffer.put(textureVertices);
textureBuffer.position(0);
// create triangle index array
indicesBuffer = ByteBuffer.allocateDirect(indicies.length).order(ByteOrder.nativeOrder());
indicesBuffer.put(indicies);
indicesBuffer.position(0);
textureId = createTexture();
shaderProgram = createShaderProgram(VERTEX_SHADER_CODE, FRAGMENT_SHADER_CODE);
// use the program
GLES20.glUseProgram(shaderProgram);
positionHandle = GLES20.glGetAttribLocation(shaderProgram, "a_position");
texCoordHandle = GLES20.glGetAttribLocation(shaderProgram, "a_texCoord");
samplerHandle = GLES20.glGetUniformLocation(shaderProgram, "s_texture");
// load the vertex position
GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);
GLES20.glEnableVertexAttribArray(positionHandle);
// load texture coordinate
GLES20.glVertexAttribPointer(texCoordHandle, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);
GLES20.glEnableVertexAttribArray(texCoordHandle);
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
}
protected void destroy(){
GLES20.glDeleteProgram(shaderProgram);
int[] textures = {textureId};
GLES20.glDeleteTextures(1, textures, 0);
}
public void render() {
// clear the color buffer
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// bind the texture
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
// Set the sampler texture unit to 0
GLES20.glUniform1i(samplerHandle, 0);
GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, 6, GLES20.GL_UNSIGNED_BYTE, indicesBuffer);
}
}
public GbaScreenView(Context context) {
super(context);
init();
}
public GbaScreenView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
this.setEGLContextClientVersion(2);
this.setPreserveEGLContextOnPause(true);
this.setRenderer(this);
this.setRenderMode(RENDERMODE_WHEN_DIRTY);
}
public void updateFrame(int[] frameBuffer) {
this.texture.update(frameBuffer);
requestRender();
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
texture = new ScreenTexture();
getHolder().setKeepScreenOn(true);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
gl.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
this.texture.render();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
holder.setKeepScreenOn(false);
this.texture.destroy();
super.surfaceDestroyed(holder);
}
}

View file

@ -2,12 +2,17 @@ package com.mrmichel.rustdroid_emu.ui;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import android.Manifest; import android.Manifest;
import android.app.ActivityManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ConfigurationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
@ -43,11 +48,32 @@ public class SplashActivity extends AppCompatActivity {
} }
} }
private void checkOpenGLES20() {
ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
ConfigurationInfo configurationInfo = am.getDeviceConfigurationInfo();
if (configurationInfo.reqGlEsVersion >= 0x20000) {
// Supported
} else {
new AlertDialog.Builder(this)
.setTitle("OpenGLES 2")
.setMessage("Your device doesn't support GLES20. reqGLEsVersion = " + configurationInfo.reqGlEsVersion)
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finishAffinity();
}
})
.setIcon(android.R.drawable.ic_dialog_alert)
.show();
}
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.splash_activity); setContentView(R.layout.splash_activity);
checkOpenGLES20();
if (ContextCompat.checkSelfPermission(this, if (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) { != PackageManager.PERMISSION_GRANTED) {

View file

@ -17,14 +17,12 @@
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<ImageView <com.mrmichel.rustdroid_emu.ui.GbaScreenView
android:id="@+id/gbaMockImageView" android:id="@+id/gba_view"
android:layout_width="400dp" android:layout_width="match_parent"
android:layout_height="296dp" android:layout_height="0dp"
android:layout_marginStart="20dp" android:layout_marginTop="120dp"
android:layout_marginTop="12dp" app:layout_constraintDimensionRatio="H, 3:2"
android:layout_marginEnd="20dp"
android:background="#2D2D33"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.517" app:layout_constraintHorizontal_bias="0.517"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -90,7 +88,7 @@
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:text="L" android:text="L"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/gbaMockImageView" /> app:layout_constraintTop_toBottomOf="@+id/gba_view" />
<Button <Button
android:id="@+id/buttonR" android:id="@+id/buttonR"
@ -100,7 +98,7 @@
android:layout_marginEnd="2dp" android:layout_marginEnd="2dp"
android:text="R" android:text="R"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/gbaMockImageView" /> app:layout_constraintTop_toBottomOf="@+id/gba_view" />
<Switch <Switch
android:id="@+id/tbTurbo" android:id="@+id/tbTurbo"