Various fixes and improvements

This commit is contained in:
spaceflint 2021-02-13 23:22:56 +02:00
parent 803415c988
commit f9a8376b93
17 changed files with 608 additions and 134 deletions

View File

@ -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

View File

@ -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;
}

13
BNA/src/Debug.cs Normal file
View File

@ -0,0 +1,13 @@
namespace System.Diagnostics
{
public static class Debug
{
public static void WriteLine(string message)
{
Microsoft.Xna.Framework.GameRunner.Log(message);
}
}
}

View File

@ -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);

View File

@ -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);
});

View File

@ -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<int, int[]> BufferSizeUsage = new Dictionary<int, int[]>();
public Dictionary<int, java.nio.Buffer> BufferCache = new Dictionary<int, java.nio.Buffer>();
//public java.util.concurrent.atomic.AtomicInteger BaseVertex = new java.util.concurrent.atomic.AtomicInteger();
}
}

View File

@ -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)
};*/
}
//

View File

@ -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];
}
}
//

View File

@ -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];

View File

@ -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<string>) 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);
}
}

View File

@ -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> keys;
}
}

View File

@ -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();
}
}
}
//

View File

@ -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))
{

View File

@ -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();
}
}
}

View File

@ -7,6 +7,10 @@
package="com.spaceflint.bluebonnet.xnademo1">
<!-- API 18, GLES 3.0 -->
<!-- note that if testing in the Android emulator, you may need to add
the following line in your ~/.android/advancedFeatures.ini file:
GLESDynamicVersion=on
!-->
<uses-sdk android:minSdkVersion="18" android:targetSdkVersion="29" />
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
@ -15,7 +19,10 @@
<application android:label="BNA_Demo1"
android:icon="@drawable/icon"
android:isGame="true" >
android:isGame="true"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
>
<!-- set android:screenOrientation if you need to lock orientation:
https://developer.android.com/guide/topics/manifest/activity-element#screen -->
@ -29,21 +36,38 @@
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:immersive="true"
android:launchMode="singleTask"
android:maxAspectRatio="9"
>
<!-- microsoft.xna.framework.log.tag sets the log identifier
for log messages printed via android.util.Log by the app.
if omitted, the default is 'BNA_Game' -->
<!-- the name of the main entrypoint class. if starts with a dot,
then it is appended to the namespace specifies in the package
attribute. the default is '.Program' -->
<meta-data android:name="BNA.main.class" android:value=".Program"/>
<meta-data android:name="microsoft.xna.framework.log.tag"
android:value="BNA_Demo1"/>
<!-- the log identifier for log messages printed via android.util.Log
by the app. the default is 'BNA_Game' -->
<meta-data android:name="BNA.log.tag" android:value="BNA_Demo1"/>
<!-- microsoft.xna.framework.main.class sets the name of the main
or entrypoint class. if starts with a dot, it is appended
to the namespace specifies in the package attribute. -->
<!-- specifies whether to call glGetError() after every GL call.
non-zero value enables error checks, zero skips error checks.
this has a negative impact on performance, and is not recommended
in production builds. the default is '0' -->
<meta-data android:name="BNA.check.gl.errors" android:value="0"/>
<meta-data android:name="microsoft.xna.framework.main.class"
android:value=".Program"/>
<!-- specifies whether to disable screen timeout, if set to a non-zero
value. the default is '0' -->
<meta-data android:name="BNA.keep.screen.on" android:value="0"/>
<!-- specifies whether to enable full screen immersive mode, if set
to a non-zero value. the default is '0' -->
<meta-data android:name="BNA.immersive.mode" android:value="0"/>
<!-- specifies the key to signal, for one frame, when the back button
is pressed. if omitted or specified as zero, default handling of
the back button occurs, which typically stops the activity.
Escape = Microsoft.Xna.Framework.Input.Keys.Escape = 27.
Back = Microsoft.Xna.Framework.Input.Keys.Back = 8. -->
<meta-data android:name="BNA.back.key" android:value="0"/>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -51,6 +75,15 @@
</intent-filter>
</activity>
<meta-data android:name="android.max_aspect" android:value="9" />
</application>
<supports-screens android:smallScreens="true"
android:normalScreens="true"
android:largeScreens="true"
android:xlargeScreens="true"
android:anyDensity="true" />
</manifest>

View File

@ -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

24
build_bna.bat Normal file
View File

@ -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