diff --git a/AndroidApp/app/src/main/AndroidManifest.xml b/AndroidApp/app/src/main/AndroidManifest.xml index e8c71fa..866732c 100644 --- a/AndroidApp/app/src/main/AndroidManifest.xml +++ b/AndroidApp/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/EmulatorActivity.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/EmulatorActivity.java index fdfcbb6..d802ece 100644 --- a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/EmulatorActivity.java +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/EmulatorActivity.java @@ -21,7 +21,6 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; -import android.widget.ImageView; import android.widget.Switch; import android.widget.Toast; @@ -53,8 +52,8 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL private AudioThread audioThread; private AudioTrack audioTrack; private byte[] on_resume_saved_state = null; - private ImageView screen; private boolean turboMode = false; + private GbaScreenView gbaScreenView; @Override public void onClick(View v) { @@ -233,7 +232,6 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL this.bios = getIntent().getByteArrayExtra("bios"); - this.screen = findViewById(R.id.gbaMockImageView); this.emulator = new Emulator(); if (Build.VERSION.SDK_INT >= 23) { @@ -260,6 +258,8 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL AudioTrack.MODE_STREAM); } this.audioTrack.play(); + + this.gbaScreenView = findViewById(R.id.gba_view); } @Override @@ -336,10 +336,6 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL startActivityForResult(intent, LOAD_SNAPSHOT_REQUESTCODE); } - public void updateScreen(Bitmap bmp) { - this.screen.setImageBitmap(bmp); - } - private class EmulationRunnable implements Runnable { public static final long NANOSECONDS_PER_MILLISECOND = 1000000; @@ -373,14 +369,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL } } - emulatorActivity.runOnUiThread(new Runnable() { - Bitmap bitmap = Bitmap.createBitmap(emulator.getFrameBuffer(), 240, 160, Bitmap.Config.RGB_565); - - @Override - public void run() { - emulatorActivity.updateScreen(bitmap); - } - }); + emulatorActivity.gbaScreenView.updateFrame(emulator.getFrameBuffer()); } public void pauseEmulation() { diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/GbaScreenView.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/GbaScreenView.java new file mode 100644 index 0000000..d6719f8 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/GbaScreenView.java @@ -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); + } +} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SplashActivity.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SplashActivity.java index 253116f..7dfeb7a 100644 --- a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SplashActivity.java +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SplashActivity.java @@ -2,12 +2,17 @@ package com.mrmichel.rustdroid_emu.ui; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.Manifest; +import android.app.ActivityManager; +import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.ConfigurationInfo; import android.content.pm.PackageManager; import android.net.Uri; 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 protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.splash_activity); + checkOpenGLES20(); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { diff --git a/AndroidApp/app/src/main/res/layout/content_emulator.xml b/AndroidApp/app/src/main/res/layout/content_emulator.xml index 4fb1594..e0512bf 100644 --- a/AndroidApp/app/src/main/res/layout/content_emulator.xml +++ b/AndroidApp/app/src/main/res/layout/content_emulator.xml @@ -17,14 +17,12 @@ app:layout_constraintStart_toStartOf="parent" /> - + app:layout_constraintTop_toBottomOf="@+id/gba_view" />