android: Add color correction shader setting

Former-commit-id: 894a0d6d6a07b16d3333739d565bba853ce13fc4
This commit is contained in:
Michel Heily 2020-03-13 23:44:45 +02:00
parent 2aa0eec95e
commit 3db61d1804
13 changed files with 235 additions and 112 deletions

View file

@ -51,6 +51,7 @@ dependencies {
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.1.0'
implementation "androidx.documentfile:documentfile:1.0.1" implementation "androidx.documentfile:documentfile:1.0.1"
implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.preference:preference:1.1.0'
implementation 'commons-io:commons-io:2.6'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.0' androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'

View file

@ -13,10 +13,10 @@
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/icon" android:icon="@drawable/icon"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@drawable/icon"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity android:name=".ui.snapshots.SnapshotPickerActivity"></activity> <activity android:name=".ui.snapshots.SnapshotPickerActivity" />
<activity <activity
android:name=".ui.snapshots.SnapshotListFragment" android:name=".ui.snapshots.SnapshotListFragment"
android:label="@string/title_activity_snapshot" /> android:label="@string/title_activity_snapshot" />

View file

@ -377,7 +377,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
SharedPreferences sharedPreferences = SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(this /* Activity context */); PreferenceManager.getDefaultSharedPreferences(this /* Activity context */);
Boolean skipBios = sharedPreferences.getBoolean("skip_bios", false); boolean skipBios = sharedPreferences.getBoolean("skip_bios", false);
if (null != savedInstanceState && (saveFilePath = savedInstanceState.getString("saveFile")) != null) { if (null != savedInstanceState && (saveFilePath = savedInstanceState.getString("saveFile")) != null) {
final EmulatorActivity thisActivity = this; final EmulatorActivity thisActivity = this;
@ -451,6 +451,10 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
case R.id.action_save_snapshot: case R.id.action_save_snapshot:
doSaveSnapshot(); doSaveSnapshot();
return true; return true;
case R.id.action_settings:
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
return true;
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }

View file

@ -1,13 +1,24 @@
package com.mrmichel.rustdroid_emu.ui; package com.mrmichel.rustdroid_emu.ui;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.opengl.GLES20; import android.opengl.GLES20;
import android.opengl.GLSurfaceView; import android.opengl.GLSurfaceView;
import android.opengl.GLUtils; import android.opengl.GLUtils;
import androidx.preference.PreferenceManager;
import com.mrmichel.rustdroid_emu.R;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
import java.nio.charset.StandardCharsets;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10; import javax.microedition.khronos.opengles.GL10;
@ -16,12 +27,53 @@ public class ScreenRenderer implements GLSurfaceView.Renderer {
private ScreenTexture texture; private ScreenTexture texture;
private boolean ready = false; private boolean ready = false;
private Context context;
public ScreenRenderer(Context context) {
this.context = context;
}
public void updateTexture(int[] frameBuffer) {
this.texture.update(frameBuffer);
}
public void initTextureIfNotInitialized() {
if (this.texture == null) {
this.texture = new ScreenTexture(this.context);
}
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
initTextureIfNotInitialized();
ready = 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();
}
public boolean isReady() {
return ready;
}
public void setColorCorrection(boolean colorCorrection) {
this.texture.setColorCorrection(colorCorrection);
}
/** /**
* Private class to manage the screen texture rendering * Private class to manage the screen texture rendering
*/ */
private class ScreenTexture { private class ScreenTexture {
int shaderProgram; int normalShaderProgram;
int colorCorrectionShaderProgram;
int currentShaderProgram;
int positionHandle; int positionHandle;
int texCoordHandle; int texCoordHandle;
int samplerHandle; int samplerHandle;
@ -32,6 +84,7 @@ public class ScreenRenderer implements GLSurfaceView.Renderer {
private ByteBuffer indicesBuffer; private ByteBuffer indicesBuffer;
private Bitmap bitmap; private Bitmap bitmap;
private Context context;
// square vertices // square vertices
private float[] vertices = { private float[] vertices = {
@ -55,26 +108,75 @@ public class ScreenRenderer implements GLSurfaceView.Renderer {
0, 2, 3 0, 2, 3
}; };
private static final String VERTEX_SHADER_CODE = public ScreenTexture(Context context) {
"attribute vec4 a_position; \n" + this.context = context;
"attribute vec2 a_texCoord; \n" + this.bitmap = Bitmap.createBitmap(240, 160, Bitmap.Config.RGB_565);
"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 = GLES20.glEnable(GLES20.GL_TEXTURE_2D);
"precision mediump float; \n" +
"varying vec2 v_texCoord; \n" +
"uniform sampler2D s_texture; \n" +
"void main() \n" +
"{ \n" +
" vec4 color = texture2D( s_texture, v_texCoord ); \n" +
" gl_FragColor = color; \n" +
"} \n";
// 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();
String vertexShader = readShaderResource(R.raw.screen_texture_vertex_shader);
String normalFragmentShader = readShaderResource(R.raw.screen_texture_fragment_shader);
String colorCorrectionFragmentShader = readShaderResource(R.raw.screen_texture_color_correction_fragment_shader);
normalShaderProgram = createShaderProgram(vertexShader, normalFragmentShader);
colorCorrectionShaderProgram = createShaderProgram(vertexShader, colorCorrectionFragmentShader);
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.context);
boolean colorCorrection = sharedPreferences.getBoolean("color_correction", false);
setColorCorrection(colorCorrection);
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
}
private void setColorCorrection(boolean colorCorrection) {
if (colorCorrection) {
currentShaderProgram = colorCorrectionShaderProgram;
} else {
currentShaderProgram = normalShaderProgram;
}
// use the program
GLES20.glUseProgram(currentShaderProgram);
positionHandle = GLES20.glGetAttribLocation(currentShaderProgram, "a_position");
texCoordHandle = GLES20.glGetAttribLocation(currentShaderProgram, "a_texCoord");
samplerHandle = GLES20.glGetUniformLocation(currentShaderProgram, "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.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);
}
private int compileShader(int type, String code) { private int compileShader(int type, String code) {
int shader = GLES20.glCreateShader(type); int shader = GLES20.glCreateShader(type);
@ -123,58 +225,28 @@ public class ScreenRenderer implements GLSurfaceView.Renderer {
return texturesIds[0]; return texturesIds[0];
} }
public ScreenTexture() { private String readShaderResource(int resourceId) {
this.bitmap = Bitmap.createBitmap(240, 160, Bitmap.Config.RGB_565); InputStream in = context.getResources().openRawResource(resourceId);
String code;
GLES20.glEnable(GLES20.GL_TEXTURE_2D); try {
code = IOUtils.toString(in, StandardCharsets.UTF_8);
// create vertex array } catch (IOException e) {
vertexBuffer = ByteBuffer.allocateDirect(vertices.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); code = "";
vertexBuffer.put(vertices); }
vertexBuffer.position(0); return code;
// 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(){ protected void destroy() {
GLES20.glDeleteProgram(shaderProgram); GLES20.glDeleteProgram(normalShaderProgram);
GLES20.glDeleteProgram(colorCorrectionShaderProgram);
int[] textures = {textureId}; int[] textures = {textureId};
GLES20.glDeleteTextures(1, textures, 0); GLES20.glDeleteTextures(1, textures, 0);
} }
public void render() { public void render() {
// use the shader program
GLES20.glUseProgram(currentShaderProgram);
// clear the color buffer // clear the color buffer
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
@ -190,34 +262,4 @@ public class ScreenRenderer implements GLSurfaceView.Renderer {
GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, 6, GLES20.GL_UNSIGNED_BYTE, indicesBuffer); GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, 6, GLES20.GL_UNSIGNED_BYTE, indicesBuffer);
} }
} }
public void updateTexture(int[] frameBuffer) {
this.texture.update(frameBuffer);
}
public void initTextureIfNotInitialized() {
if (this.texture == null) {
this.texture = new ScreenTexture();
}
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
initTextureIfNotInitialized();
ready = 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();
}
public boolean isReady() {
return ready;
}
} }

View file

@ -1,10 +1,13 @@
package com.mrmichel.rustdroid_emu.ui; package com.mrmichel.rustdroid_emu.ui;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.opengl.GLSurfaceView; import android.opengl.GLSurfaceView;
import android.util.AttributeSet; import android.util.AttributeSet;
public class ScreenView extends GLSurfaceView { import androidx.preference.PreferenceManager;
public class ScreenView extends GLSurfaceView implements SharedPreferences.OnSharedPreferenceChangeListener {
private ScreenRenderer mRenderer; private ScreenRenderer mRenderer;
public ScreenView(Context context) { public ScreenView(Context context) {
@ -21,7 +24,11 @@ public class ScreenView extends GLSurfaceView {
this.setEGLContextClientVersion(2); this.setEGLContextClientVersion(2);
this.setPreserveEGLContextOnPause(true); this.setPreserveEGLContextOnPause(true);
mRenderer = new ScreenRenderer(); SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(getContext());
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
mRenderer = new ScreenRenderer(getContext());
this.setRenderer(mRenderer); this.setRenderer(mRenderer);
this.setRenderMode(RENDERMODE_WHEN_DIRTY); this.setRenderMode(RENDERMODE_WHEN_DIRTY);
} }
@ -34,4 +41,12 @@ public class ScreenView extends GLSurfaceView {
public ScreenRenderer getRenderer() { public ScreenRenderer getRenderer() {
return mRenderer; return mRenderer;
} }
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals("color_correction")) {
boolean colorCorrection = sharedPreferences.getBoolean("color_correction", false);
mRenderer.setColorCorrection(colorCorrection);
}
}
} }

View file

@ -2,18 +2,32 @@ package com.mrmichel.rustdroid_emu.ui;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceFragmentCompat;
import com.mrmichel.rustdroid_emu.R; import com.mrmichel.rustdroid_emu.R;
public class SettingsActivity extends AppCompatActivity { public class SettingsActivity extends AppCompatActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings); setContentView(R.layout.activity_settings);
getSupportFragmentManager() getSupportFragmentManager()
.beginTransaction() .beginTransaction()
.replace(R.id.settings_container, new SettingsFragment()) .replace(R.id.settings, new SettingsFragment())
.commit(); .commit();
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
public static class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.app_preferences, rootKey);
}
} }
} }

View file

@ -13,7 +13,6 @@ import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.webkit.MimeTypeMap;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.GridView; import android.widget.GridView;

View file

@ -1,13 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical" >
<FrameLayout <FrameLayout
android:id="@+id/settings_container" android:id="@+id/settings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent" />
</FrameLayout>
</LinearLayout> </LinearLayout>

View file

@ -0,0 +1,21 @@
/*
Port of byuu's color correction shader as described in https://byuu.net/video/color-emulation
*/
precision mediump float;
varying vec2 v_texCoord;
uniform sampler2D s_texture;
void main()
{
float lcdGamma = 4.0, outGamma = 2.2;
vec4 color = texture2D( s_texture, v_texCoord );
color.rgb = pow(color.rgb, vec3(lcdGamma));
gl_FragColor.r = pow(( 0.0 * color.b + 50.0 * color.g + 255.0 * color.r) / 255.0, 1.0 / outGamma) * 255.0 / 280.0;
gl_FragColor.g = pow(( 30.0 * color.b + 230.0 * color.g + 10.0 * color.r) / 255.0, 1.0 / outGamma) * 255.0 / 280.0;
gl_FragColor.b = pow((220.0 * color.b + 10.0 * color.g + 50.0 * color.r) / 255.0, 1.0 / outGamma) * 255.0 / 280.0;
gl_FragColor.a = 1.0;
}

View file

@ -0,0 +1,8 @@
precision mediump float;
varying vec2 v_texCoord;
uniform sampler2D s_texture;
void main()
{
vec4 color = texture2D( s_texture, v_texCoord );
gl_FragColor = color;
}

View file

@ -0,0 +1,8 @@
attribute vec4 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main()
{
gl_Position = a_position;
v_texCoord = a_texCoord;
}

View file

@ -16,4 +16,9 @@
<string name="action_delete">Delete</string> <string name="action_delete">Delete</string>
<string name="title_activity_snapshot">Snapshot Manager</string> <string name="title_activity_snapshot">Snapshot Manager</string>
<string name="color_correction_setting">Apply color correction shader</string>
<string name="color_correction_summary">
Makes colors look closer to the real GBA LCD screen.
</string>
</resources> </resources>

View file

@ -1,8 +1,18 @@
<PreferenceScreen <PreferenceScreen
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<CheckBoxPreference <PreferenceCategory app:title="Emulation">
<SwitchPreferenceCompat
app:key="skip_bios" app:key="skip_bios"
app:title="Skip bios boot animation"/> app:title="Skip bios boot animation"/>
</PreferenceCategory>
<PreferenceCategory app:title="Graphics">
<SwitchPreferenceCompat
app:key="color_correction"
app:title="@string/color_correction_setting"
app:summary="@string/color_correction_summary"
/>
</PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>