diff --git a/BNA/FNA.filter b/BNA/FNA.filter index 0cf5226..50d530e 100644 --- a/BNA/FNA.filter +++ b/BNA/FNA.filter @@ -45,6 +45,10 @@ *.Input.Touch.TouchLocationState *.Input.Touch.TouchPanel *.Input.Touch.TouchPanelCapabilities +*.Input.Keyboard +*.Input.KeyboardState +*.Input.Keys +*.Input.KeyState *.Graphics.BasicEffect *.Graphics.Blend diff --git a/BNA/src/Activity.cs b/BNA/src/Activity.cs index 675c910..086dd45 100644 --- a/BNA/src/Activity.cs +++ b/BNA/src/Activity.cs @@ -11,11 +11,45 @@ namespace Microsoft.Xna.Framework protected override void onCreate(android.os.Bundle savedInstanceState) { + // on some devices, this should be before call to base.onCreate + // requestWindowFeature(android.view.Window.FEATURE_NO_TITLE); + + logTag = GetMetaAttr_Str("log.tag", "BNA_Game"); + + backKeyCode = GetMetaAttr_Int("back.key"); + + if (android.os.Build.VERSION.SDK_INT >= 19) + { + immersiveMode = GetMetaAttr_Int("immersive.mode") != 0; + + if (immersiveMode && android.os.Build.VERSION.SDK_INT >= 28) + { + var layoutParams = getWindow().getAttributes(); + layoutParams.layoutInDisplayCutoutMode = + android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + getWindow().setAttributes(layoutParams); + } + } + + if (GetMetaAttr_Int("keep.screen.on") != 0) + { + getWindow().addFlags( + android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + /* + int flags = android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN + | android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS + | android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; + if (GetMetaAttr_Int("keep.screen.on") != 0) + flags |= android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + getWindow().setFlags(flags + | android.view.WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, + flags); + */ + base.onCreate(savedInstanceState); - - _LogTag = GetMetaAttr("log.tag") ?? _LogTag; - - new java.lang.Thread(gameRunner = new GameRunner(this)).start(); } // @@ -36,7 +70,7 @@ namespace Microsoft.Xna.Framework // // Android events forwarded to GameRunner: - // onPause, onResume, onDestroy, onTouchEvent + // onPause, onWindowFocusChanged, onDestroy, onTouchEvent, onBackPressed // protected override void onPause() @@ -45,11 +79,39 @@ namespace Microsoft.Xna.Framework base.onPause(); } - protected override void onResume() + public override void onWindowFocusChanged(bool hasFocus) + { + base.onWindowFocusChanged(hasFocus); + + if (hasFocus) + { + if (immersiveMode) + { + getWindow().getDecorView().setSystemUiVisibility( + android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | android.view.View.SYSTEM_UI_FLAG_FULLSCREEN + | android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } + + if (object.ReferenceEquals(gameRunner, null)) + { + new java.lang.Thread(gameRunner = new GameRunner(this)).start(); + } + else + { + gameRunner.ActivityResume(); + } + } + } + + /*protected override void onResume() { gameRunner?.ActivityResume(); base.onResume(); - } + }*/ protected override void onDestroy() { @@ -79,33 +141,47 @@ namespace Microsoft.Xna.Framework return true; } + public override void onBackPressed() + { + if (backKeyCode != 0) + gameRunner?.ActivityKey(backKeyCode); + else + base.onBackPressed(); + } + // - // GetMetaAttr + // GetMetaAttr_Str, GetMetaAttr_Int // - public string GetMetaAttr(string name, bool warn = false) + public string GetMetaAttr_Str(string name, string def) { - var info = getPackageManager().getActivityInfo(getComponentName(), - android.content.pm.PackageManager.GET_ACTIVITIES - | android.content.pm.PackageManager.GET_META_DATA); - name = "microsoft.xna.framework." + name; - var str = info?.metaData?.getString(name); + name = "BNA." + name; + var str = GetMetaData()?.getString(name); if (string.IsNullOrEmpty(str)) { - if (warn) - Activity.Log($"missing metadata attribute '{name}'"); - str = null; + Activity.Log($"missing metadata attribute '{name}'"); + str = def; } return str; } + public int GetMetaAttr_Int(string name) + => GetMetaData()?.getInt("BNA." + name) ?? 0; + + private android.os.Bundle GetMetaData() + => getPackageManager().getActivityInfo( + getComponentName(), + android.content.pm.PackageManager.GET_ACTIVITIES + | android.content.pm.PackageManager.GET_META_DATA) + ?.metaData; + // // Log // - public static void Log(string s) => android.util.Log.i(_LogTag, s); + public static void Log(string s) => android.util.Log.i(logTag, s); - private static string _LogTag = "BNA_Game"; + private static string logTag; // // data @@ -113,6 +189,8 @@ namespace Microsoft.Xna.Framework private GameRunner gameRunner; private bool restartActivity; + private bool immersiveMode; + private int backKeyCode; } diff --git a/BNA/src/Debug.cs b/BNA/src/Debug.cs new file mode 100644 index 0000000..1587243 --- /dev/null +++ b/BNA/src/Debug.cs @@ -0,0 +1,13 @@ + +namespace System.Diagnostics +{ + + public static class Debug + { + public static void WriteLine(string message) + { + Microsoft.Xna.Framework.GameRunner.Log(message); + } + } + +} diff --git a/BNA/src/Effect.cs b/BNA/src/Effect.cs index 276abf0..ed0fb2c 100644 --- a/BNA/src/Effect.cs +++ b/BNA/src/Effect.cs @@ -67,7 +67,7 @@ namespace Microsoft.Xna.Framework.Graphics public string CreateProgram2(string vertexText, string fragmentText) { string errText = null; - Renderer.Get(GraphicsDevice.GLDevice).Send( () => + Renderer.Get(GraphicsDevice.GLDevice).Send(true, () => { (vertexId, errText) = CompileShader( GLES20.GL_VERTEX_SHADER, "vertex", vertexText); @@ -93,7 +93,6 @@ namespace Microsoft.Xna.Framework.Graphics (int, string) CompileShader(int kind, string errKind, string text) { - //GameRunner.Log($"SHADER PROGRAM: [[[" + text + "]]]"); string errText = null; int shaderId = GLES20.glCreateShader(kind); int errCode = GLES20.glGetError(); @@ -237,7 +236,7 @@ namespace Microsoft.Xna.Framework.Graphics public (string name, int type, int size)[] GetProgramUniforms() { (string, int, int)[] result = null; - Renderer.Get(GraphicsDevice.GLDevice).Send( () => + Renderer.Get(GraphicsDevice.GLDevice).Send(true, () => { var count = new int[1]; GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, count, 0); @@ -272,7 +271,7 @@ namespace Microsoft.Xna.Framework.Graphics public void INTERNAL_applyEffect(uint pass) { var graphicsDevice = GraphicsDevice; - Renderer.Get(graphicsDevice.GLDevice).Send( () => + Renderer.Get(graphicsDevice.GLDevice).Send(false, () => { GLES20.glUseProgram(programId); int n = Parameters.Count; @@ -297,7 +296,7 @@ namespace Microsoft.Xna.Framework.Graphics { if (! base.IsDisposed) { - Renderer.Get(GraphicsDevice.GLDevice).Send( () => + Renderer.Get(GraphicsDevice.GLDevice).Send(true, () => { GLES20.glDeleteShader(fragmentId); GLES20.glDeleteShader(vertexId); diff --git a/BNA/src/FNA3D.cs b/BNA/src/FNA3D.cs index 20ba433..5cd7672 100644 --- a/BNA/src/FNA3D.cs +++ b/BNA/src/FNA3D.cs @@ -40,7 +40,7 @@ namespace Microsoft.Xna.Framework.Graphics } } - Renderer.Get(device).Send( () => + Renderer.Get(device).Send(false, () => { GLES20.glViewport(v.x, v.y, v.w, v.h); GLES20.glDepthRangef(v.minDepth, v.maxDepth); @@ -54,7 +54,7 @@ namespace Microsoft.Xna.Framework.Graphics public static void FNA3D_SetScissorRect(IntPtr device, ref Rectangle scissor) { var s = scissor; - Renderer.Get(device).Send( () => + Renderer.Get(device).Send(false, () => { GLES20.glScissor(s.X, s.Y, s.Width, s.Height); }); @@ -69,7 +69,7 @@ namespace Microsoft.Xna.Framework.Graphics { var clearColor = color; var renderer = Renderer.Get(device); - renderer.Send( () => + renderer.Send(false, () => { var state = (State) renderer.UserData; var WriteMask = state.WriteMask; @@ -180,8 +180,7 @@ namespace Microsoft.Xna.Framework.Graphics { throw new PlatformNotSupportedException(); } - var renderer = Renderer.Get(device); - renderer.Present(); + Renderer.Get(device).Present(); } // @@ -192,7 +191,7 @@ namespace Microsoft.Xna.Framework.Graphics { var input = blendState; var renderer = Renderer.Get(device); - Renderer.Get(device).Send( () => + Renderer.Get(device).Send(false, () => { var state = (State) renderer.UserData; @@ -323,7 +322,6 @@ namespace Microsoft.Xna.Framework.Graphics public static void FNA3D_SetDepthStencilState(IntPtr device, ref FNA3D_DepthStencilState depthStencilState) { - // AndroidActivity.LogI(">>>>> SET DEPTH AND STENCIL STATE"); } // @@ -335,7 +333,7 @@ namespace Microsoft.Xna.Framework.Graphics { var input = rasterizerState; var renderer = Renderer.Get(device); - Renderer.Get(device).Send( () => + Renderer.Get(device).Send(false, () => { var state = (State) renderer.UserData; @@ -382,6 +380,30 @@ namespace Microsoft.Xna.Framework.Graphics }); } + // + // FNA3D_GetBackbufferSize + // + + public static void FNA3D_GetBackbufferSize(IntPtr device, out int w, out int h) + { + var bounds = GameRunner.Singleton.ClientBounds; + w = bounds.Width; + h = bounds.Height; + } + + // + // FNA3D_GetBackbufferSurfaceFormat + // + + public static SurfaceFormat FNA3D_GetBackbufferSurfaceFormat(IntPtr device) + { + return SurfaceFormat.Color; + } + + // + // FNA3D_GetMaxMultiSampleCount + // + public static int FNA3D_GetMaxMultiSampleCount(IntPtr device, SurfaceFormat format, int preferredMultiSampleCount) { @@ -426,7 +448,7 @@ namespace Microsoft.Xna.Framework.Graphics int indexOffset = startIndex * elementSize; primitiveCount = PrimitiveCount(primitiveType, primitiveCount); - Renderer.Get(device).Send( () => + Renderer.Get(device).Send(false, () => { GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, (int) indices); GLES30.glDrawRangeElements(drawMode, minVertexIndex, maxVertexIndex, @@ -464,7 +486,7 @@ namespace Microsoft.Xna.Framework.Graphics int drawMode = PrimitiveTypeToDrawMode[(int) primitiveType]; primitiveCount = PrimitiveCount(primitiveType, primitiveCount); - Renderer.Get(device).Send( () => + Renderer.Get(device).Send(false, () => { GLES20.glDrawArrays(drawMode, vertexStart, primitiveCount); }); diff --git a/BNA/src/FNA3D_Buf.cs b/BNA/src/FNA3D_Buf.cs index 428e03f..d09e895 100644 --- a/BNA/src/FNA3D_Buf.cs +++ b/BNA/src/FNA3D_Buf.cs @@ -30,7 +30,7 @@ namespace Microsoft.Xna.Framework.Graphics private static int CreateBuffer(Renderer renderer, int target, byte dynamic, int size) { int bufferId = 0; - renderer.Send( () => + renderer.Send(true, () => { int[] id = new int[1]; GLES20.glGenBuffers(1, id, 0); @@ -74,7 +74,7 @@ namespace Microsoft.Xna.Framework.Graphics public static void FNA3D_AddDisposeVertexBuffer(IntPtr device, IntPtr buffer) { var renderer = Renderer.Get(device); - renderer.Send( () => + renderer.Send(true, () => { GLES20.glDeleteBuffers(1, new int[] { (int) buffer }, 0); @@ -102,7 +102,7 @@ namespace Microsoft.Xna.Framework.Graphics var dataBuffer = BufferSerializer.Convert( dataPointer, dataLength, state, bufferId); - renderer.Send( () => + renderer.Send(false, () => { GLES20.glBindBuffer(target, bufferId); @@ -151,11 +151,18 @@ namespace Microsoft.Xna.Framework.Graphics int numBindings, byte bindingsUpdated, int baseVertex) { + /* skip if FNA says the bindings have not been updated + if (baseVertex == ((State) renderer.UserData).BaseVertex.getAndSet(baseVertex)) + { + if (bindingsUpdated == 0) + return; + }*/ + var bindingsCopy = new FNA3D_VertexBufferBinding[numBindings]; for (int i = 0; i < numBindings; i++) bindingsCopy[i] = bindings[i]; - Renderer.Get(device).Send( () => + Renderer.Get(device).Send(false, () => { int nextAttribIndex = 0; foreach (var binding in bindingsCopy) @@ -278,8 +285,8 @@ namespace Microsoft.Xna.Framework.Graphics // we use GCHandle::FromIntPtr to convert that address to an object reference. // see also: system.runtime.interopservices.GCHandle struct in baselib. int offset = (int) data; - newBuffer = Convert(GCHandle.FromIntPtr(data - offset).Target, - offset, length, oldBuffer); + newBuffer = Convert2(GCHandle.FromIntPtr(data - offset).Target, + offset, length, oldBuffer); if (newBuffer != oldBuffer) { @@ -293,8 +300,8 @@ namespace Microsoft.Xna.Framework.Graphics } - public static java.nio.Buffer Convert(object data, int offset, int length, - java.nio.Buffer buffer) + private static java.nio.Buffer Convert2(object data, int offset, int length, + java.nio.Buffer buffer) { if (data is short[]) { @@ -491,6 +498,7 @@ namespace Microsoft.Xna.Framework.Graphics { public Dictionary BufferSizeUsage = new Dictionary(); public Dictionary BufferCache = new Dictionary(); + //public java.util.concurrent.atomic.AtomicInteger BaseVertex = new java.util.concurrent.atomic.AtomicInteger(); } } diff --git a/BNA/src/FNA3D_Dev.cs b/BNA/src/FNA3D_Dev.cs index 9cabcf1..e74a24d 100644 --- a/BNA/src/FNA3D_Dev.cs +++ b/BNA/src/FNA3D_Dev.cs @@ -27,20 +27,23 @@ namespace Microsoft.Xna.Framework.Graphics default: throw new ArgumentException("depthStencilFormat"); } + int swapInterval = presentationParameters.presentationInterval switch + { + PresentInterval.Two => 2, + PresentInterval.Immediate => 0, + // Default, One, or any other value: + _ => 1 + }; + + bool checkErrors = GameRunner.Singleton.CheckGlErrors(); + var device = Renderer.Create(GameRunner.Singleton.Activity, GameRunner.Singleton.OnSurfaceChanged, - 8, 8, 8, 0, depthSize, stencilSize); + 8, 8, 8, 0, depthSize, stencilSize, + swapInterval, checkErrors); + FNA3D_ResetBackbuffer(device, ref presentationParameters); return device; - - /*var renderer = Renderer.Get(device); - renderer.UserData = new State() - { - BackBufferWidth = presentationParameters.backBufferWidth, - BackBufferHeight = presentationParameters.backBufferHeight, - AdjustViewport = // see also FNA3D_SetViewport - (presentationParameters.displayOrientation == DisplayOrientation.Default) - };*/ } // diff --git a/BNA/src/FNA3D_Rt.cs b/BNA/src/FNA3D_Rt.cs index dc07b0b..33345fb 100644 --- a/BNA/src/FNA3D_Rt.cs +++ b/BNA/src/FNA3D_Rt.cs @@ -27,7 +27,7 @@ namespace Microsoft.Xna.Framework.Graphics renderTargetsCopy[i] = renderTargets[i]; var renderer = Renderer.Get(device); - renderer.Send( () => + renderer.Send(false, () => { var state = (State) renderer.UserData; if (state.TargetFramebuffer == 0) @@ -99,7 +99,7 @@ namespace Microsoft.Xna.Framework.Graphics throw new PlatformNotSupportedException(); var renderer = Renderer.Get(device); - renderer.Send( () => + renderer.Send(false, () => { GLES20.glBindFramebuffer(GLES30.GL_DRAW_FRAMEBUFFER, 0); @@ -132,7 +132,7 @@ namespace Microsoft.Xna.Framework.Graphics int texture = (int) renderTarget.texture; var renderer = Renderer.Get(device); - renderer.Send( () => + renderer.Send(false, () => { int textureUnit = GLES20.GL_TEXTURE0 + renderer.TextureUnits - 1; GLES20.glActiveTexture(textureUnit); @@ -165,7 +165,7 @@ namespace Microsoft.Xna.Framework.Graphics int bufferId = 0; var renderer = Renderer.Get(device); - renderer.Send( () => + renderer.Send(true, () => { var state = (State) renderer.UserData; @@ -192,7 +192,7 @@ namespace Microsoft.Xna.Framework.Graphics public static void FNA3D_AddDisposeRenderbuffer(IntPtr device, IntPtr renderbuffer) { var renderer = Renderer.Get(device); - renderer.Send( () => + renderer.Send(true, () => { GLES20.glDeleteRenderbuffers(1, new int[] { (int) renderbuffer }, 0); }); @@ -216,6 +216,8 @@ namespace Microsoft.Xna.Framework.Graphics int x, int y, int w, int h, int level, object dataObject, int dataOffset, int dataLength) { + int[] tempIntArray = null; + java.nio.Buffer buffer = dataObject switch { sbyte[] byteArray => @@ -224,10 +226,18 @@ namespace Microsoft.Xna.Framework.Graphics int[] intArray => java.nio.IntBuffer.wrap(intArray, dataOffset / 4, dataLength / 4), + Color[] _ => + // GameRunner constructor sets the marshal size of Color to -1, + // so we expect only negative or zero values here + java.nio.IntBuffer.wrap( + tempIntArray = new int[dataLength <= 0 + ? (dataLength = -dataLength) + : throw new ArgumentException()]), + _ => throw new ArgumentException(dataObject?.GetType().ToString()), }; - renderer.Send( () => + renderer.Send(true, () => { var state = (State) renderer.UserData; if (state.SourceFramebuffer == 0) @@ -257,6 +267,16 @@ namespace Microsoft.Xna.Framework.Graphics GLES20.glBindFramebuffer(GLES30.GL_READ_FRAMEBUFFER, 0); }); + + if (tempIntArray != null) + { + if (dataOffset > 0) + throw new ArgumentException(); + // convert int[] array from the GL call to a Color[] array + var colorArray = (Color[]) dataObject; + for (int i = 0; i < dataLength; i++) + colorArray[i - dataOffset].PackedValue = (uint) tempIntArray[i]; + } } // diff --git a/BNA/src/FNA3D_Tex.cs b/BNA/src/FNA3D_Tex.cs index c225483..91cc0bf 100644 --- a/BNA/src/FNA3D_Tex.cs +++ b/BNA/src/FNA3D_Tex.cs @@ -90,7 +90,7 @@ namespace Microsoft.Xna.Framework.Graphics } int textureId = 0; - renderer.Send( () => + renderer.Send(true, () => { int id = CreateTexture(renderer, GLES20.GL_TEXTURE_2D, format, levelCount); if (id != 0) @@ -138,7 +138,7 @@ namespace Microsoft.Xna.Framework.Graphics public static void FNA3D_AddDisposeTexture(IntPtr device, IntPtr texture) { var renderer = Renderer.Get(device); - renderer.Send( () => + renderer.Send(true, () => { GLES20.glDeleteTextures(1, new int[] { (int) texture }, 0); @@ -165,10 +165,13 @@ namespace Microsoft.Xna.Framework.Graphics int[] intArray => java.nio.IntBuffer.wrap(intArray, dataOffset / 4, dataLength / 4), + Color[] colorArray => + bufferFromColorArray(colorArray, dataOffset, dataLength), + _ => throw new ArgumentException(dataObject?.GetType().ToString()), }; - renderer.Send( () => + renderer.Send(false, () => { GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); @@ -200,6 +203,20 @@ namespace Microsoft.Xna.Framework.Graphics } }); + + java.nio.Buffer bufferFromColorArray(Color[] array, int offset, int length) + { + // GameRunner constructor sets the marshal size of Color to -1, + // so we expect only negative or zero values here + if (offset > 0 || length > 0) + throw new ArgumentException(); + // convert Color[] array into an int[] array for the GL call + length = -length; + var intArray = new int[length]; + for (int i = 0; i < length; i++) + intArray[i] = (int) array[i - offset].PackedValue; + return java.nio.IntBuffer.wrap(intArray); + } } public static void FNA3D_SetTextureData2D(IntPtr device, IntPtr texture, @@ -344,7 +361,7 @@ namespace Microsoft.Xna.Framework.Graphics int textureId = (int) texture; var renderer = Renderer.Get(device); - renderer.Send( () => + renderer.Send(false, () => { var state = (State) renderer.UserData; var config = state.TextureConfigs[textureId]; diff --git a/BNA/src/GameRunner.cs b/BNA/src/GameRunner.cs index 5a78b48..50f998e 100644 --- a/BNA/src/GameRunner.cs +++ b/BNA/src/GameRunner.cs @@ -14,8 +14,6 @@ namespace Microsoft.Xna.Framework private Activity activity; private System.Collections.Hashtable dict; - private int clientWidth, clientHeight; - private Rectangle clientBounds; private bool recreateActivity; private java.util.concurrent.atomic.AtomicInteger inModal; @@ -31,6 +29,9 @@ namespace Microsoft.Xna.Framework private const int CONFIG_EVENT = 1; private const int TOUCH_EVENT = 2; + private const int KEY_EVENT = 4; + + private int eventKeyCode; // // constructor @@ -40,7 +41,15 @@ namespace Microsoft.Xna.Framework { this.activity = activity; - UpdateConfiguration(false); + // in Bluebonnet, the following method is used to specify the + // size that Marshal.SizeOf should return for non-primitive types. + // this is used to enable Texture2D.GetData/SetData to accept + // Color[] arrays. see also SetTextureData in FNA3D_Tex + System.Runtime.InteropServices.Marshal.SetComObjectData( + typeof(System.Runtime.InteropServices.Marshal), + typeof(Color), -1); + + UpdateConfiguration(); inModal = new java.util.concurrent.atomic.AtomicInteger(); shouldPause = new java.util.concurrent.atomic.AtomicInteger(); @@ -71,6 +80,12 @@ namespace Microsoft.Xna.Framework set => inModal.set(value ? 1 : 0); } + // + // CheckGlErrors + // + + public bool CheckGlErrors() => activity.GetMetaAttr_Int("check.gl.errors") != 0; + // // Thread run() method // @@ -103,6 +118,8 @@ namespace Microsoft.Xna.Framework GameRunner.Log("========================================"); GameRunner.Log(e.ToString()); GameRunner.Log("========================================"); + if (! object.ReferenceEquals(e.InnerException, null)) + e = e.InnerException; System.Windows.Forms.MessageBox.Show(e.ToString()); } @@ -111,19 +128,13 @@ namespace Microsoft.Xna.Framework { Type clsType = null; - var clsName = activity.GetMetaAttr("main.class", true); - if (clsName != null) - { - if (clsName[0] == '.') - clsName = activity.getPackageName() + clsName; - - clsType = System.Type.GetType(clsName, false, true); - } + var clsName = activity.GetMetaAttr_Str("main.class", ".Program"); + if (clsName[0] == '.') + clsName = activity.getPackageName() + clsName; + clsType = System.Type.GetType(clsName, false, true); if (clsType == null) - { throw new Exception($"main class '{clsName}' not found"); - } return clsType; } @@ -131,7 +142,7 @@ namespace Microsoft.Xna.Framework void CallMainMethod(Type mainClass) { - var method = mainClass.GetMethod("Main"); + var method = mainClass.GetMethod("Main") ?? mainClass.GetMethod("main"); if (method.IsStatic) { method.Invoke(null, new object[method.GetParameters().Length]); @@ -150,6 +161,7 @@ namespace Microsoft.Xna.Framework public void MainLoop(Game game) { int pauseCount = 0; + bool clearKeys = false; while (game.RunApplication) { @@ -197,12 +209,19 @@ namespace Microsoft.Xna.Framework eventBits = shouldEvents.get(); if ((eventBits & CONFIG_EVENT) != 0) - UpdateConfiguration(true); + UpdateConfiguration(); if ((eventBits & TOUCH_EVENT) != 0) { Microsoft.Xna.Framework.Input.Mouse - .HandleEvents(clientWidth, clientHeight); + .HandleEvents((int) dict["width"], (int) dict["height"]); + } + + if ((eventBits & KEY_EVENT) != 0) + { + Microsoft.Xna.Framework.Input.Keyboard.keys.Add( + (Microsoft.Xna.Framework.Input.Keys) eventKeyCode); + clearKeys = true; } } @@ -211,6 +230,16 @@ namespace Microsoft.Xna.Framework // game.Tick(); + + // + // a simulated key is signalled during a single frame + // + + if (clearKeys) + { + clearKeys = false; + Microsoft.Xna.Framework.Input.Keyboard.keys.Clear(); + } } InModal = true; @@ -263,12 +292,18 @@ namespace Microsoft.Xna.Framework public void ActivityPause() { - if (! InModal) + if (shouldResume.get() == 0) { - shouldPause.incrementAndGet(); - waitForPause.block(); - if (shouldExit.get() == 0) - waitForPause.close(); + Microsoft.Xna.Framework.Media.MediaPlayer.ActivityPauseOrResume(true); + Microsoft.Xna.Framework.Audio.SoundEffect.ActivityPauseOrResume(true); + + if (! InModal) + { + shouldPause.incrementAndGet(); + waitForPause.block(); + if (shouldExit.get() == 0) + waitForPause.close(); + } } } @@ -279,7 +314,16 @@ namespace Microsoft.Xna.Framework public void ActivityResume() { if (shouldResume.compareAndSet(1, 0)) + { + // force a call to UpdateConfiguration after main loop wakes up. + // this will not actually invoke callbacks if nothing has changed. + OnSurfaceChanged(); + waitForResume.open(); + + Microsoft.Xna.Framework.Media.MediaPlayer.ActivityPauseOrResume(false); + Microsoft.Xna.Framework.Audio.SoundEffect.ActivityPauseOrResume(false); + } } // @@ -308,6 +352,23 @@ namespace Microsoft.Xna.Framework } } + // + // ActivityKey + // + + public void ActivityKey(int keyCode) + { + for (;;) + { + int v = shouldEvents.get(); + if (shouldEvents.compareAndSet(v, v | KEY_EVENT)) + { + eventKeyCode = keyCode; + break; + } + } + } + // // OnSurfaceChanged // @@ -326,26 +387,76 @@ namespace Microsoft.Xna.Framework // UpdateConfiguration // - void UpdateConfiguration(bool withCallback) + void UpdateConfiguration() { - var metrics = new android.util.DisplayMetrics(); - activity.getWindowManager().getDefaultDisplay().getRealMetrics(metrics); + var metrics = GetDisplayMetrics(activity); + int width = metrics.widthPixels; + int height = metrics.heightPixels; - clientWidth = metrics.widthPixels; - clientHeight = metrics.heightPixels; - clientBounds = new Rectangle(0, 0, clientWidth, clientHeight); + var dict = new System.Collections.Hashtable(); - if (dict == null) - dict = new System.Collections.Hashtable(); + dict["width"] = width; + dict["height"] = height; + dict["dpi"] = (int) ((metrics.xdpi + metrics.ydpi) * 0.5f); - // int dpi - pixels per inch - dict["dpi"] = (int) ((metrics.xdpi + metrics.ydpi) * 0.5f); - - if (withCallback) + if (object.ReferenceEquals(this.dict, null)) { + // on first call, set the dict without invoking callbacks + SetExtra(dict, width, height); + this.dict = dict; + } + else if (! DictsEqual(dict, this.dict)) + { + SetExtra(dict, width, height); + this.dict = dict; + OnClientSizeChanged(); OnOrientationChanged(); } + + + android.util.DisplayMetrics GetDisplayMetrics(android.app.Activity activity) + { + bool isMultiWindow = false; + if (android.os.Build.VERSION.SDK_INT >= 24) + isMultiWindow = activity.isInMultiWindowMode(); + + var metrics = new android.util.DisplayMetrics(); + var display = activity.getWindowManager().getDefaultDisplay(); + if (! isMultiWindow) + { + // getRealMetrics gets real size of the entire screen. + // this is what we need in full screen immersive mode. + display.getRealMetrics(metrics); + } + else + { + // getMetrics gets the size of the split window, minus + // window frames. this is useful in multi-window mode. + display.getMetrics(metrics); + } + + return metrics; + } + + + bool DictsEqual(System.Collections.Hashtable dict1, + System.Collections.Hashtable dict2) + { + foreach (var key in dict1.Keys) + { + if (! dict1[key].Equals(dict2[key])) + return false; + } + return true; + } + + + void SetExtra(System.Collections.Hashtable dict, int width, int height) + { + dict["bounds"] = new Rectangle(0, 0, width, height); + dict["openUri"] = (Action) OpenUri; + } } // @@ -359,11 +470,32 @@ namespace Microsoft.Xna.Framework return null; } + // + // OpenUri + // + + private void OpenUri(string uri) + { + activity.runOnUiThread(((java.lang.Runnable.Delegate) (() => + { + try + { + activity.startActivity( + new android.content.Intent(android.content.Intent.ACTION_VIEW, + android.net.Uri.parse(uri))); + } + catch (Exception e) + { + GameRunner.Log(e.ToString()); + } + })).AsInterface()); + } + // // GameWindow interface // - public override Rectangle ClientBounds => clientBounds; + public override Rectangle ClientBounds => (Rectangle) dict["bounds"]; public override string ScreenDeviceName => "Android"; @@ -374,8 +506,8 @@ namespace Microsoft.Xna.Framework public override DisplayOrientation CurrentOrientation { - get => (clientWidth < clientHeight) ? DisplayOrientation.Portrait - : DisplayOrientation.LandscapeLeft; + get => ((int) dict["width"] < (int) dict["height"]) + ? DisplayOrientation.Portrait : DisplayOrientation.LandscapeLeft; set { bool portrait = 0 != (value & DisplayOrientation.Portrait); @@ -387,7 +519,7 @@ namespace Microsoft.Xna.Framework else if (landscape && (! portrait)) r = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE; else - r = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER; + return; activity.setRequestedOrientation(r); } } diff --git a/BNA/src/Import.cs b/BNA/src/Import.cs index 71db5ce..cd48ff4 100644 --- a/BNA/src/Import.cs +++ b/BNA/src/Import.cs @@ -201,6 +201,12 @@ namespace Microsoft.Xna.Framework.Input public int Y { get; set; } public ButtonState LeftButton { get; set; } } + + [java.attr.Discard] // discard in output + public static class Keyboard + { + public static List keys; + } } diff --git a/BNA/src/MediaPlayer.cs b/BNA/src/MediaPlayer.cs index 24da1fe..c5fdc37 100644 --- a/BNA/src/MediaPlayer.cs +++ b/BNA/src/MediaPlayer.cs @@ -15,6 +15,7 @@ namespace Microsoft.Xna.Framework.Media [java.attr.RetainType] private static float volume; [java.attr.RetainType] private static bool muted; [java.attr.RetainType] private static bool looping; + [java.attr.RetainType] private static bool wasPlayingBeforeActivityPause; // // static constructor @@ -283,6 +284,28 @@ namespace Microsoft.Xna.Framework.Media } } + + // + // ActivityPauseOrResume + // + + public static void ActivityPauseOrResume(bool pausing) + { + if (pausing) + { + if (State == MediaState.Playing) + { + wasPlayingBeforeActivityPause = true; + Pause(); + } + } + else if (wasPlayingBeforeActivityPause) + { + wasPlayingBeforeActivityPause = false; + Resume(); + } + } + } // diff --git a/BNA/src/Renderer.cs b/BNA/src/Renderer.cs index e0083f3..be8fbb2 100644 --- a/BNA/src/Renderer.cs +++ b/BNA/src/Renderer.cs @@ -20,6 +20,8 @@ namespace Microsoft.Xna.Framework.Graphics private android.os.ConditionVariable waitObject; private java.util.concurrent.atomic.AtomicInteger paused; private Action actionOnChanged; + private int swapInterval; + private bool checkErrors; public object UserData; @@ -39,11 +41,14 @@ namespace Microsoft.Xna.Framework.Graphics private Renderer(android.app.Activity activity, Action onChanged, int redSize, int greenSize, int blueSize, - int alphaSize, int depthSize, int stencilSize) + int alphaSize, int depthSize, int stencilSize, + int swapInterval, bool checkErrors) { waitObject = new android.os.ConditionVariable(); paused = new java.util.concurrent.atomic.AtomicInteger(); actionOnChanged = onChanged; + this.swapInterval = swapInterval; + this.checkErrors = checkErrors; activity.runOnUiThread(((java.lang.Runnable.Delegate) (() => { @@ -55,7 +60,6 @@ namespace Microsoft.Xna.Framework.Graphics surface.setRenderer(this); surface.setRenderMode(android.opengl.GLSurfaceView.RENDERMODE_WHEN_DIRTY); activity.setContentView(surface); - })).AsInterface()); // wait for one onDrawFrame callback, which tells us that @@ -78,16 +82,21 @@ namespace Microsoft.Xna.Framework.Graphics // Send // - public void Send(Action action) + public void Send(bool wait, Action action) { Exception exc = null; if (paused.get() == 0) { - var cond = new android.os.ConditionVariable(); - surface.queueEvent(((java.lang.Runnable.Delegate) (() => + if (! waitObject.block(2000)) { - var error = GLES20.glGetError(); - if (error == GLES20.GL_NO_ERROR) + // see also Present(). a timeout here means that onDrawFrame + // was never called, so the surface was probably destroyed. + paused.compareAndSet(0, -1); + } + else + { + var cond = wait ? new android.os.ConditionVariable() : null; + surface.queueEvent(((java.lang.Runnable.Delegate) (() => { try { @@ -97,18 +106,21 @@ namespace Microsoft.Xna.Framework.Graphics { exc = exc2; } - error = GLES20.glGetError(); - } - if (error != GLES20.GL_NO_ERROR) - exc = new Exception($"GL Error {error}"); - cond.open(); - })).AsInterface()); - cond.block(); + if (checkErrors) + { + var error = GLES20.glGetError(); + if (error != GLES20.GL_NO_ERROR) + exc = new Exception($"GL Error {error}"); + } + if (cond != null) + cond.open(); + })).AsInterface()); + if (cond != null) + cond.block(); + } } if (exc != null) - { throw new AggregateException(exc.Message, exc); - } } // @@ -119,7 +131,19 @@ namespace Microsoft.Xna.Framework.Graphics { waitObject.close(); surface.requestRender(); - waitObject.block(); + + // GLSurfaceView runs queued events before checking if render + // was requested; but if the event queue is empty, it checks + // if render requested, then calls onDrawFrame(). thus the + // sequence of events is as follows: + + // - Present() blocks waitObject and requests render + // - any events already queued by Send() are processed by + // GLSurfaceView, before it checks for a requested render. + // - Send() delays any new events until waitObject is opened. + // - OnDrawFrame() is called and opens waitObject, and when + // it returns to GLSurfaceView, the GL buffers are swapped. + // - with waitObject open, Send() queues new events. } // @@ -132,6 +156,17 @@ namespace Microsoft.Xna.Framework.Graphics // if onSurfaceCreated is called while resuming from pause, // it means the GL context was lost paused.compareAndSet(1, -1); + + // assign high priority to the rendering thread + java.lang.Thread.currentThread() + .setPriority(java.lang.Thread.MAX_PRIORITY); + + // set swap interval + if (swapInterval != 1) + { + var eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + EGL14.eglSwapInterval(eglDisplay, swapInterval); + } } [java.attr.RetainName] @@ -188,7 +223,8 @@ namespace Microsoft.Xna.Framework.Graphics public static IntPtr Create(android.app.Activity activity, Action onChanged, int redSize, int greenSize, int blueSize, - int alphaSize, int depthSize, int stencilSize) + int alphaSize, int depthSize, int stencilSize, + int swapInterval, bool checkErrors) { for (;;) { @@ -216,7 +252,8 @@ namespace Microsoft.Xna.Framework.Graphics deviceId = deviceId, renderer = new Renderer(activity, onChanged, redSize, greenSize, blueSize, - alphaSize, depthSize, stencilSize), + alphaSize, depthSize, stencilSize, + swapInterval, checkErrors), activity = new java.lang.@ref.WeakReference(activity), }); @@ -290,7 +327,7 @@ namespace Microsoft.Xna.Framework.Graphics foreach (var renderer in GetRenderersForActivity(activity)) { renderer.surface.onPause(); - renderer.paused.set(1); + renderer.paused.compareAndSet(0, 1); } } @@ -304,9 +341,20 @@ namespace Microsoft.Xna.Framework.Graphics { if (renderer.paused.get() != 0) { - renderer.waitObject.close(); + // we cannot use waitObject for resuming, because the surface + // may have been destroyed between pause and resume, in which + // case onDrawFrame would not get called. + + var cond = new android.os.ConditionVariable(); + renderer.surface.queueEvent(((java.lang.Runnable.Delegate) ( + () => cond.open() )).AsInterface()); + renderer.surface.onResume(); - renderer.waitObject.block(); + if (! cond.block(2000)) + { + // something is wrong if the queued event did not run + return false; + } if (! renderer.paused.compareAndSet(1, 0)) { diff --git a/BNA/src/SoundEffect.cs b/BNA/src/SoundEffect.cs index f5e4f38..44f97db 100644 --- a/BNA/src/SoundEffect.cs +++ b/BNA/src/SoundEffect.cs @@ -264,6 +264,29 @@ namespace Microsoft.Xna.Framework.Audio public static float DistanceScale { get; set; } public static float DopplerScale { get; set; } public static float SpeedOfSound { get; set; } + + // + // ActivityPauseOrResume + // + + public static void ActivityPauseOrResume(bool pausing) + { + try + { + instancesLock.@lock(); + int num = instancesList.size(); + for (int idx = 0; idx < num; idx++) + { + var instRef = (java.lang.@ref.WeakReference) instancesList.get(idx); + ((SoundEffectInstance) instRef.get())?.ActivityPauseOrResume(pausing); + } + } + finally + { + instancesLock.unlock(); + } + } + } @@ -279,6 +302,7 @@ namespace Microsoft.Xna.Framework.Audio [java.attr.RetainType] private android.media.AudioTrack track; [java.attr.RetainType] private float pitch, pan, volume; [java.attr.RetainType] private bool isLooped; + [java.attr.RetainType] private bool wasPlayingBeforeActivityPause; // // Constructor (for SoundEffect.CreateInstance) @@ -595,6 +619,27 @@ namespace Microsoft.Xna.Framework.Audio public void Apply3D(AudioListener listener, AudioEmitter emitter) { } public void Apply3D(AudioListener[] listeners, AudioEmitter emitter) { } + // + // ActivityPauseOrResume + // + + public void ActivityPauseOrResume(bool pausing) + { + if (pausing) + { + if (State == SoundState.Playing) + { + wasPlayingBeforeActivityPause = true; + Pause(); + } + } + else if (wasPlayingBeforeActivityPause) + { + wasPlayingBeforeActivityPause = false; + Resume(); + } + } + } diff --git a/Demo1/AndroidManifest.xml b/Demo1/AndroidManifest.xml index a1b15ab..01f6ba2 100644 --- a/Demo1/AndroidManifest.xml +++ b/Demo1/AndroidManifest.xml @@ -7,6 +7,10 @@ package="com.spaceflint.bluebonnet.xnademo1"> + @@ -15,7 +19,10 @@ + android:isGame="true" + android:resizeableActivity="true" + android:supportsPictureInPicture="true" + > @@ -29,21 +36,38 @@ android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:immersive="true" android:launchMode="singleTask" + android:maxAspectRatio="9" > - + + - + + - + + - + + + + + + + + @@ -51,6 +75,15 @@ + + + + + diff --git a/MakeAPK.project b/MakeAPK.project index 06411e4..07554f7 100644 --- a/MakeAPK.project +++ b/MakeAPK.project @@ -24,6 +24,7 @@ APK_TEMP_DIR = directory where APK processing occurs e.g. $(OutputDir)/MyGame/Debug/Content + IMPORTANT: this directory will be deleted and recreated! KEYSTORE_FILE = path to a keystore file used in APK signing @@ -33,8 +34,6 @@ BLUEBONNET_EXE = path to Bluebonnet.exe program file - BLUEBONNET_JAR = path to Baselib.jar file from Bluebonnet - ANDROID_JAR = path to Android.jar file, for desired API level e.g. $(ANDROID_HOME)/android-28/android.jar diff --git a/build_bna.bat b/build_bna.bat new file mode 100644 index 0000000..e7b10c0 --- /dev/null +++ b/build_bna.bat @@ -0,0 +1,24 @@ +@echo off +if "%ANDROID_JAR%" == "" ( + echo Missing environment variable ANDROID_JAR. + echo It should specify the full path to an Android.jar file in the platforms directory of the Android SDK. + goto :EOF +) +if "%FNA_DLL%" == "" ( + echo Missing environment variable FNA_DLL. + echo It should specify the full path to the FNA.DLL file. + goto :EOF +) +if "%BLUEBONNET_EXE%" == "" ( + echo Missing environment variable BLUEBONNET_EXE. + echo It should specify the full path to the Bluebonnet executable. + goto :EOF +) + +echo ======================================== +echo Building BNA. Command: +echo MSBuild BNA -p:Configuration=Release +echo ======================================== +MSBuild BNA -p:Configuration=Release + +:EOF