package org.inodes.gus.scummvm; import android.content.Context; import android.content.res.AssetManager; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.os.Handler; import android.os.HandlerThread; import android.os.Process; import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; import javax.microedition.khronos.opengles.GL; import javax.microedition.khronos.opengles.GL10; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGL11; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLContext; import javax.microedition.khronos.egl.EGLDisplay; import javax.microedition.khronos.egl.EGLSurface; import java.io.File; import java.util.concurrent.Semaphore; import java.util.Map; import java.util.LinkedHashMap; // At least in Android 2.1, eglCreateWindowSurface() requires an // EGLNativeWindowSurface object, which is hidden deep in the bowels // of libui. Until EGL is properly exposed, it's probably safer to // use the Java versions of most EGL functions :( public class ScummVM implements SurfaceHolder.Callback { private final static String LOG_TAG = "ScummVM.java"; private final int AUDIO_FRAME_SIZE = 2 * 2; // bytes. 16bit audio * stereo public static class AudioSetupException extends Exception {} private long nativeScummVM; // native code hangs itself here boolean scummVMRunning = false; private native void create(AssetManager am); public ScummVM(Context context) { create(context.getAssets()); // Init C++ code, set nativeScummVM } private native void nativeDestroy(); public synchronized void destroy() { if (nativeScummVM != 0) { nativeDestroy(); nativeScummVM = 0; } } protected void finalize() { destroy(); } // Surface creation: // GUI thread: create surface, release lock // ScummVM thread: acquire lock (block), read surface // // Surface deletion: // GUI thread: post event, acquire lock (block), return // ScummVM thread: read event, free surface, release lock // // In other words, ScummVM thread does this: // acquire lock // setup surface // when SCREEN_CHANGED arrives: // destroy surface // release lock // back to acquire lock static final int configSpec[] = { EGL10.EGL_RED_SIZE, 5, EGL10.EGL_GREEN_SIZE, 5, EGL10.EGL_BLUE_SIZE, 5, EGL10.EGL_DEPTH_SIZE, 0, EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT, EGL10.EGL_NONE, }; EGL10 egl; EGLDisplay eglDisplay = EGL10.EGL_NO_DISPLAY; EGLConfig eglConfig; EGLContext eglContext = EGL10.EGL_NO_CONTEXT; EGLSurface eglSurface = EGL10.EGL_NO_SURFACE; Semaphore surfaceLock = new Semaphore(0, true); SurfaceHolder nativeSurface; public void surfaceCreated(SurfaceHolder holder) { nativeSurface = holder; surfaceLock.release(); } public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // Disabled while I debug GL problems //pushEvent(new Event(Event.EVENT_SCREEN_CHANGED)); } public void surfaceDestroyed(SurfaceHolder holder) { pushEvent(new Event(Event.EVENT_SCREEN_CHANGED)); try { surfaceLock.acquire(); } catch (InterruptedException e) { Log.e(this.toString(), "Interrupted while waiting for surface lock", e); } } // For debugging private static final Map attribs; static { attribs = new LinkedHashMap(); attribs.put("CONFIG_ID", EGL10.EGL_CONFIG_ID); attribs.put("BUFFER_SIZE", EGL10.EGL_BUFFER_SIZE); attribs.put("RED_SIZE", EGL10.EGL_RED_SIZE); attribs.put("GREEN_SIZE", EGL10.EGL_GREEN_SIZE); attribs.put("BLUE_SIZE", EGL10.EGL_BLUE_SIZE); attribs.put("ALPHA_SIZE", EGL10.EGL_ALPHA_SIZE); //attribs.put("BIND_TO_RGB", EGL10.EGL_BIND_TO_TEXTURE_RGB); //attribs.put("BIND_TO_RGBA", EGL10.EGL_BIND_TO_TEXTURE_RGBA); attribs.put("CONFIG_CAVEAT", EGL10.EGL_CONFIG_CAVEAT); attribs.put("DEPTH_SIZE", EGL10.EGL_DEPTH_SIZE); attribs.put("LEVEL", EGL10.EGL_LEVEL); attribs.put("MAX_PBUFFER_WIDTH", EGL10.EGL_MAX_PBUFFER_WIDTH); attribs.put("MAX_PBUFFER_HEIGHT", EGL10.EGL_MAX_PBUFFER_HEIGHT); attribs.put("MAX_PBUFFER_PIXELS", EGL10.EGL_MAX_PBUFFER_PIXELS); //attribs.put("MAX_SWAP_INTERVAL", EGL10.EGL_MAX_SWAP_INTERVAL); //attribs.put("MIN_SWAP_INTERVAL", EGL10.EGL_MIN_SWAP_INTERVAL); attribs.put("NATIVE_RENDERABLE", EGL10.EGL_NATIVE_RENDERABLE); attribs.put("NATIVE_VISUAL_ID", EGL10.EGL_NATIVE_VISUAL_ID); attribs.put("NATIVE_VISUAL_TYPE", EGL10.EGL_NATIVE_VISUAL_TYPE); attribs.put("SAMPLE_BUFFERS", EGL10.EGL_SAMPLE_BUFFERS); attribs.put("SAMPLES", EGL10.EGL_SAMPLES); attribs.put("STENCIL_SIZE", EGL10.EGL_STENCIL_SIZE); attribs.put("SURFACE_TYPE", EGL10.EGL_SURFACE_TYPE); attribs.put("TRANSPARENT_TYPE", EGL10.EGL_TRANSPARENT_TYPE); attribs.put("TRANSPARENT_RED_VALUE", EGL10.EGL_TRANSPARENT_RED_VALUE); attribs.put("TRANSPARENT_GREEN_VALUE", EGL10.EGL_TRANSPARENT_GREEN_VALUE); attribs.put("TRANSPARENT_BLUE_VALUE", EGL10.EGL_TRANSPARENT_BLUE_VALUE); } private void dumpEglConfig(EGLConfig config) { int[] value = new int[1]; for (Map.Entry entry : attribs.entrySet()) { egl.eglGetConfigAttrib(eglDisplay, config, entry.getValue(), value); if (value[0] == EGL10.EGL_NONE) Log.d(LOG_TAG, entry.getKey() + ": NONE"); else Log.d(LOG_TAG, String.format("%s: %d", entry.getKey(), value[0])); } } // Called by ScummVM thread (from initBackend) private void createScummVMGLContext() { egl = (EGL10)EGLContext.getEGL(); eglDisplay = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); int[] version = new int[2]; egl.eglInitialize(eglDisplay, version); int[] num_config = new int[1]; egl.eglChooseConfig(eglDisplay, configSpec, null, 0, num_config); final int numConfigs = num_config[0]; if (numConfigs <= 0) throw new IllegalArgumentException("No configs match configSpec"); EGLConfig[] configs = new EGLConfig[numConfigs]; egl.eglChooseConfig(eglDisplay, configSpec, configs, numConfigs, num_config); if (false) { Log.d(LOG_TAG, String.format("Found %d EGL configurations.", numConfigs)); for (EGLConfig config : configs) dumpEglConfig(config); } // Android's eglChooseConfig is busted in several versions and // devices so we have to filter/rank the configs again ourselves. eglConfig = chooseEglConfig(configs); if (false) { Log.d(LOG_TAG, String.format("Chose EGL config from %d possibilities.", numConfigs)); dumpEglConfig(eglConfig); } eglContext = egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, null); if (eglContext == EGL10.EGL_NO_CONTEXT) throw new RuntimeException("Failed to create context"); } private EGLConfig chooseEglConfig(EGLConfig[] configs) { int best = 0; int bestScore = -1; int[] value = new int[1]; for (int i = 0; i < configs.length; i++) { EGLConfig config = configs[i]; int score = 10000; egl.eglGetConfigAttrib(eglDisplay, config, EGL10.EGL_SURFACE_TYPE, value); if ((value[0] & EGL10.EGL_WINDOW_BIT) == 0) continue; // must have egl.eglGetConfigAttrib(eglDisplay, config, EGL10.EGL_CONFIG_CAVEAT, value); if (value[0] != EGL10.EGL_NONE) score -= 1000; // Must be at least 555, but then smaller is better final int[] colorBits = {EGL10.EGL_RED_SIZE, EGL10.EGL_GREEN_SIZE, EGL10.EGL_BLUE_SIZE, EGL10.EGL_ALPHA_SIZE}; for (int component : colorBits) { egl.eglGetConfigAttrib(eglDisplay, config, component, value); if (value[0] >= 5) score += 10; // boost if >5 bits accuracy score -= value[0]; // penalize for wasted bits } egl.eglGetConfigAttrib(eglDisplay, config, EGL10.EGL_DEPTH_SIZE, value); score -= value[0]; // penalize for wasted bits if (score > bestScore) { best = i; bestScore = score; } } if (bestScore < 0) { Log.e(LOG_TAG, "Unable to find an acceptable EGL config, expect badness."); return configs[0]; } return configs[best]; } // Called by ScummVM thread static private boolean _log_version = true; protected void setupScummVMSurface() { try { surfaceLock.acquire(); } catch (InterruptedException e) { Log.e(LOG_TAG, "Interrupted while waiting for surface lock", e); return; } eglSurface = egl.eglCreateWindowSurface(eglDisplay, eglConfig, nativeSurface, null); if (eglSurface == EGL10.EGL_NO_SURFACE) Log.e(LOG_TAG, "CreateWindowSurface failed!"); egl.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext); GL10 gl = (GL10)eglContext.getGL(); if (_log_version) { Log.i(LOG_TAG, String.format("Using EGL %s (%s); GL %s/%s (%s)", egl.eglQueryString(eglDisplay, EGL10.EGL_VERSION), egl.eglQueryString(eglDisplay, EGL10.EGL_VENDOR), gl.glGetString(GL10.GL_VERSION), gl.glGetString(GL10.GL_RENDERER), gl.glGetString(GL10.GL_VENDOR))); _log_version = false; // only log this once } int[] value = new int[1]; egl.eglQuerySurface(eglDisplay, eglSurface, EGL10.EGL_WIDTH, value); int width = value[0]; egl.eglQuerySurface(eglDisplay, eglSurface, EGL10.EGL_HEIGHT, value); int height = value[0]; Log.i(LOG_TAG, String.format("New surface is %dx%d", width, height)); setSurfaceSize(width, height); } // Called by ScummVM thread protected void destroyScummVMSurface() { if (eglSurface != null) { egl.eglMakeCurrent(eglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); egl.eglDestroySurface(eglDisplay, eglSurface); eglSurface = EGL10.EGL_NO_SURFACE; } surfaceLock.release(); } public void setSurface(SurfaceHolder holder) { holder.addCallback(this); } final public boolean swapBuffers() { if (!egl.eglSwapBuffers(eglDisplay, eglSurface)) { int error = egl.eglGetError(); Log.w(LOG_TAG, String.format("eglSwapBuffers exited with error 0x%x", error)); if (error == EGL11.EGL_CONTEXT_LOST) return false; } return true; } // Set scummvm config options final public native static void loadConfigFile(String path); final public native static void setConfMan(String key, int value); final public native static void setConfMan(String key, String value); final public native void enableZoning(boolean enable); final public native void setSurfaceSize(int width, int height); // Feed an event to ScummVM. Safe to call from other threads. final public native void pushEvent(Event e); final private native void audioMixCallback(byte[] buf); // Runs the actual ScummVM program and returns when it does. // This should not be called from multiple threads simultaneously... final public native int scummVMMain(String[] argv); // Callbacks from C++ peer instance //protected GraphicsMode[] getSupportedGraphicsModes() {} protected void displayMessageOnOSD(String msg) {} protected void setWindowCaption(String caption) {} protected void showVirtualKeyboard(boolean enable) {} protected String[] getSysArchives() { return new String[0]; } protected String[] getPluginDirectories() { return new String[0]; } protected void initBackend() throws AudioSetupException { createScummVMGLContext(); initAudio(); } private static class AudioThread extends Thread { final private int buf_size; private boolean is_paused = false; final private ScummVM scummvm; final private AudioTrack audio_track; AudioThread(ScummVM scummvm, AudioTrack audio_track, int buf_size) { super("AudioThread"); this.scummvm = scummvm; this.audio_track = audio_track; this.buf_size = buf_size; setPriority(Thread.MAX_PRIORITY); setDaemon(true); } public void pauseAudio() { synchronized (this) { is_paused = true; } audio_track.pause(); } public void resumeAudio() { synchronized (this) { is_paused = false; notifyAll(); } audio_track.play(); } public void run() { byte[] buf = new byte[buf_size]; audio_track.play(); int offset = 0; try { while (true) { synchronized (this) { while (is_paused) wait(); } if (offset == buf.length) { // Grab new audio data scummvm.audioMixCallback(buf); offset = 0; } int len = buf.length - offset; int ret = audio_track.write(buf, offset, len); if (ret < 0) { Log.w(LOG_TAG, String.format( "AudioTrack.write(%dB) returned error %d", buf.length, ret)); break; } else if (ret != len) { Log.w(LOG_TAG, String.format( "Short audio write. Wrote %dB, not %dB", ret, buf.length)); // Buffer is full, so yield cpu for a while Thread.sleep(100); } offset += ret; } } catch (InterruptedException e) { Log.e(this.toString(), "Audio thread interrupted", e); } } } private AudioThread audio_thread; final public int audioSampleRate() { return AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC); } private void initAudio() throws AudioSetupException { int sample_rate = audioSampleRate(); int buf_size = AudioTrack.getMinBufferSize(sample_rate, AudioFormat.CHANNEL_CONFIGURATION_STEREO, AudioFormat.ENCODING_PCM_16BIT); if (buf_size < 0) { int guess = AUDIO_FRAME_SIZE * sample_rate / 100; // 10ms of audio Log.w(LOG_TAG, String.format( "Unable to get min audio buffer size (error %d). Guessing %dB.", buf_size, guess)); buf_size = guess; } Log.d(LOG_TAG, String.format("Using %dB buffer for %dHZ audio", buf_size, sample_rate)); AudioTrack audio_track = new AudioTrack(AudioManager.STREAM_MUSIC, sample_rate, AudioFormat.CHANNEL_CONFIGURATION_STEREO, AudioFormat.ENCODING_PCM_16BIT, buf_size, AudioTrack.MODE_STREAM); if (audio_track.getState() != AudioTrack.STATE_INITIALIZED) { Log.e(LOG_TAG, "Error initialising Android audio system."); throw new AudioSetupException(); } audio_thread = new AudioThread(this, audio_track, buf_size); audio_thread.start(); } public void pause() { audio_thread.pauseAudio(); // TODO: need to pause engine too } public void resume() { // TODO: need to resume engine too audio_thread.resumeAudio(); } static { // For grabbing with gdb... final boolean sleep_for_debugger = false; if (sleep_for_debugger) { try { Thread.sleep(20*1000); } catch (InterruptedException e) { } } //System.loadLibrary("scummvm"); File cache_dir = ScummVMApplication.getLastCacheDir(); String libname = System.mapLibraryName("scummvm"); File libpath = new File(cache_dir, libname); System.load(libpath.getPath()); } }