commit 0b2510826bdc430cf804b18b4a412adeb477fed8 Author: spaceflint <> Date: Sun Nov 15 11:14:42 2020 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13d7896 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.obj/ +.vs/ +packages/ +/TODO diff --git a/BNA/BNA.csproj b/BNA/BNA.csproj new file mode 100644 index 0000000..f1935ed --- /dev/null +++ b/BNA/BNA.csproj @@ -0,0 +1,58 @@ + + + + {53E4C9F9-CE12-4067-8336-663D3582DCBA} + Library + BNA + BNA + OnOutputUpdated + true + + None + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %22:@(FilterItem, '%22 %22:')%22 + + + + + + + + + + \ No newline at end of file diff --git a/BNA/FNA.filter b/BNA/FNA.filter new file mode 100644 index 0000000..35438da --- /dev/null +++ b/BNA/FNA.filter @@ -0,0 +1,104 @@ + +*.Bounding* +*.Color +*.ContainmentType +*.DisplayOrientation + +*.DrawableGameComponent +*.Game +*.GameComponent +*.GameComponentCollection +*.GameComponentCollectionEventArgs +*.GameServiceContainer +*.GameTime +*.GameWindow + +*.GraphicsDeviceInformation +*.GraphicsDeviceManager + +*.IDrawable +*.IGameComponent +*.IGraphicsDeviceManager +*.IUpdateable + +*.MathHelper +*.Matrix + +*.PlayerIndex +*.Point +*.PreparingDeviceSettingsEventArgs + +*.Quaternion + +*.Rectangle + +*.Vector2 +*.Vector3 +*.Vector4 + +*.Input.ButtonState +*.Input.MouseState +*.Input.Touch.GestureDetector +*.Input.Touch.GestureSample +*.Input.Touch.GestureType +*.Input.Touch.TouchLocation +*.Input.Touch.TouchLocationState +*.Input.Touch.TouchPanel +*.Input.Touch.TouchPanelCapabilities + +*.Graphics.BasicEffect +*.Graphics.Blend +*.Graphics.BlendFunction +*.Graphics.BlendState +*.Graphics.BufferUsage +*.Graphics.ClearOptions +*.Graphics.ColorWriteChannels +*.Graphics.CompareFunction +*.Graphics.CubeMapFace +*.Graphics.CullMode +*.Graphics.DepthFormat +*.Graphics.DepthStencilState +*.Graphics.DirectionalLight +*.Graphics.DisplayMode +*.Graphics.DisplayModeCollection +*.Graphics.DxtUtil +*.Graphics.DynamicVertexBuffer +*.Graphics.EffectDirtyFlags +*.Graphics.EffectHelpers +*.Graphics.EffectParameterCollection +*.Graphics.EffectPass +*.Graphics.EffectPassCollection +*.Graphics.EffectTechnique +*.Graphics.EffectTechniqueCollection +*.Graphics.FillMode +*.Graphics.GraphicsAdapter +*.Graphics.GraphicsDevice +*.Graphics.GraphicsProfile +*.Graphics.GraphicsResource +*.Graphics.IGraphicsDeviceService +*.Graphics.IEffect* +*.Graphics.IndexBuffer +*.Graphics.IndexElementSize +*.Graphics.IRenderTarget +*.Graphics.IVertexType +*.Graphics.NoSuitableGraphicsDeviceException +*.Graphics.PipelineCache +*.Graphics.PresentationParameters +*.Graphics.PresentInterval +*.Graphics.PrimitiveType +*.Graphics.RasterizerState +*.Graphics.RenderTarget* +*.Graphics.SamplerState +*.Graphics.SamplerStateCollection +*.Graphics.Sprite* +*.Graphics.StateHash +*.Graphics.StencilOperation +*.Graphics.SurfaceFormat +*.Graphics.Texture* +*.Graphics.Vertex* +*.Graphics.Viewport + +*.Graphics.PackedVector.* + +*.Content.* +*.Storage.* diff --git a/BNA/src/Activity.cs b/BNA/src/Activity.cs new file mode 100644 index 0000000..675c910 --- /dev/null +++ b/BNA/src/Activity.cs @@ -0,0 +1,119 @@ + +namespace Microsoft.Xna.Framework +{ + + public class Activity : android.app.Activity + { + + // + // Android onCreate + // + + protected override void onCreate(android.os.Bundle savedInstanceState) + { + base.onCreate(savedInstanceState); + + _LogTag = GetMetaAttr("log.tag") ?? _LogTag; + + new java.lang.Thread(gameRunner = new GameRunner(this)).start(); + } + + // + // FinishAndRestart + // + + public void FinishAndRestart(bool restart) + { + if (! (isFinishing() || isChangingConfigurations())) + { + runOnUiThread(((java.lang.Runnable.Delegate) (() => + { + finish(); + restartActivity = restart; + })).AsInterface()); + } + } + + // + // Android events forwarded to GameRunner: + // onPause, onResume, onDestroy, onTouchEvent + // + + protected override void onPause() + { + gameRunner?.ActivityPause(); + base.onPause(); + } + + protected override void onResume() + { + gameRunner?.ActivityResume(); + base.onResume(); + } + + protected override void onDestroy() + { + gameRunner?.ActivityDestroy(); + base.onDestroy(); + + if (restartActivity) + { + // note, do not use activity.recreate() here, as it occasionally + // keeps the old surface locked for a few seconds + startActivity(getIntent() + .addFlags(android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION) + .addFlags(android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME) + .addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK) + ); + } + + // we have to destroy the activity process to get rid of leaking + // static references that can never be garbage collected + + java.lang.System.exit(0); + } + + public override bool onTouchEvent(android.view.MotionEvent motionEvent) + { + gameRunner?.ActivityTouch(motionEvent); + return true; + } + + // + // GetMetaAttr + // + + public string GetMetaAttr(string name, bool warn = false) + { + 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); + if (string.IsNullOrEmpty(str)) + { + if (warn) + Activity.Log($"missing metadata attribute '{name}'"); + str = null; + } + return str; + } + + // + // Log + // + + public static void Log(string s) => android.util.Log.i(_LogTag, s); + + private static string _LogTag = "BNA_Game"; + + // + // data + // + + private GameRunner gameRunner; + private bool restartActivity; + + } + +} diff --git a/BNA/src/Effect.cs b/BNA/src/Effect.cs new file mode 100644 index 0000000..d661568 --- /dev/null +++ b/BNA/src/Effect.cs @@ -0,0 +1,881 @@ + +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using android.opengl; +#pragma warning disable 0436 + +namespace Microsoft.Xna.Framework.Graphics +{ + public class Effect : GraphicsResource + { + + // + // Effect + // + + public Effect(GraphicsDevice graphicsDevice, byte[] effectCode) + { + GraphicsDevice = graphicsDevice; + CreateProgram(System.Text.Encoding.ASCII.GetString(effectCode)); + CollectUniforms(); + } + + + + // + // CreateProgram + // + + void CreateProgram(string text) + { + var (vertOfs, vertHdr) = HeaderOffset(text, "--- vertex ---", 0); + var (fragOfs, fragHdr) = HeaderOffset(text, "--- fragment ---", vertOfs); + var (stopOfs, stopHdr) = HeaderOffset(text, "--- end ---", fragOfs); + + string versionDef = "#version 300 es\n"; + string precisionDef = "#ifdef GL_FRAGMENT_PRECISION_HIGH \n" + + "precision highp float; \n" + + "#else \n" + + "precision mediump float; \n" + + "#endif \n"; + + string vertText = versionDef + text.Substring(vertOfs + vertHdr, + fragOfs - (vertOfs + vertHdr)); + string fragText = versionDef + precisionDef + + text.Substring(fragOfs + fragHdr, + stopOfs - (fragOfs + fragHdr)); + + var err = CreateProgram2(vertText, fragText); + if (err != null) + throw new System.InvalidProgramException("Effect: " + err); + + SetTechnique(text.Substring(0, vertOfs)); + + (int, int) HeaderOffset(string text, string header, int index) + { + index = text.IndexOf(header, index); + if (index == -1) + throw new System.InvalidProgramException("Effect: " + header); + return (index, header.Length); + } + } + + // + // CreateProgram2 + // + + public string CreateProgram2(string vertexText, string fragmentText) + { + string errText = null; + Renderer.Get(GraphicsDevice.GLDevice).Send( () => + { + (vertexId, errText) = CompileShader( + GLES20.GL_VERTEX_SHADER, "vertex", vertexText); + if (errText == null) + { + (fragmentId, errText) = CompileShader( + GLES20.GL_FRAGMENT_SHADER, "fragment", fragmentText); + if (errText == null) + { + (programId, errText) = LinkProgram(vertexId, fragmentId); + if (errText == null) + { + return; // success + } + } + GLES20.glDeleteShader(fragmentId); + } + GLES20.glDeleteShader(vertexId); + }); + return errText; + + // CompileShader + + (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(); + if (shaderId == 0 || errCode != 0) + errText = "glCreateShader"; + else + { + GLES20.glShaderSource(shaderId, text); + errCode = GLES20.glGetError(); + if (errCode != 0) + errText = "glShaderSource"; + else + { + GLES20.glCompileShader(shaderId); + errCode = GLES20.glGetError(); + if (errCode != 0) + errText = "glCompileShader"; + else + { + var status = new int[1]; + GLES20.glGetShaderiv( + shaderId, GLES20.GL_COMPILE_STATUS, status, 0); + errCode = GLES20.glGetError(); + if (errCode == 0 && status[0] != 0) + { + return (shaderId, null); // success + } + errText = "compile error: " + + GLES20.glGetShaderInfoLog(shaderId); + } + GLES20.glDeleteShader(shaderId); + } + } + if (errCode != 0) + errText = "GL error " + errCode + ": " + errText; + errText = "in " + errKind + " shader: " + errText; + return (0, errText); + } + + // LinkProgram + + (int, string) LinkProgram(int vertexId, int fragmentId) + { + string errText = null; + int programId = GLES20.glCreateProgram(); + int errCode = GLES20.glGetError(); + if (programId == 0 || errCode != 0) + errText = "glCreateProgram"; + else + { + GLES20.glAttachShader(programId, vertexId); + errCode = GLES20.glGetError(); + if (errCode != 0) + errText = "glAttachShader (vertex)"; + else + { + GLES20.glAttachShader(programId, fragmentId); + errCode = GLES20.glGetError(); + if (errCode != 0) + errText = "glAttachShader (fragment)"; + else + { + GLES20.glLinkProgram(programId); + errCode = GLES20.glGetError(); + if (errCode != 0) + errText = "glLinkProgram"; + else + { + var status = new int[1]; + GLES20.glGetProgramiv( + programId, GLES20.GL_LINK_STATUS, status, 0); + errCode = GLES20.glGetError(); + if (errCode == 0 && status[0] != 0) + { + return (programId, null); // success + } + errText = "link error: " + + GLES20.glGetProgramInfoLog(programId); + } + GLES20.glDetachShader(programId, fragmentId); + } + GLES20.glDetachShader(programId, vertexId); + } + GLES20.glDeleteProgram(programId); + } + if (errCode != 0) + errText = "GL error " + errCode + ": " + errText; + errText = "in shader program: " + errText; + return (0, errText); + } + } + + // + // SetTechnique + // + + void SetTechnique(string text) + { + string name; + string search = "#technique "; + int idx = text.IndexOf(search); + if (idx != -1) + name = text.Substring(idx + search.Length).Trim(); + else + name = "Default"; + + var passes = new List(); + passes.Add(new EffectPass(name, null, this, IntPtr.Zero, 0)); + var list = new List(); + technique = new EffectTechnique(name, IntPtr.Zero, + new EffectPassCollection(passes), null); + list.Add(technique); + Techniques = new EffectTechniqueCollection(list); + } + + + + // + // CollectUniforms + // + + void CollectUniforms() + { + var list = new List(); + var uniforms = GetProgramUniforms(); + if (uniforms != null) + { + for (int i = 0; i < uniforms.Length; i++) + { + var (name, type, size) = uniforms[i]; + list.Add(new EffectParameter(name, type, size)); + } + } + Parameters = new EffectParameterCollection(list); + } + + // + // GetProgramUniforms + // + + public (string name, int type, int size)[] GetProgramUniforms() + { + (string, int, int)[] result = null; + Renderer.Get(GraphicsDevice.GLDevice).Send( () => + { + var count = new int[1]; + GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, count, 0); + byte[] nameBuf = new byte[count[0] + 1]; + GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_UNIFORMS, count, 0); + if (count[0] == 0) + return; + + result = new (string name, int type, int size)[count[0]]; + var type = new int[1]; + var size = new int[1]; + for (int i = 0; i < result.Length; i++) + { + GLES20.glGetActiveUniform(programId, i, nameBuf.Length, + /* length, lengthOffset */ count, 0, + /* size, sizeOffset */ size, 0, + /* type, typeOffset */ type, 0, + /* name, nameOffset */ (sbyte[]) (object) nameBuf, 0); + var nameStr = System.Text.Encoding.ASCII.GetString(nameBuf, 0, count[0]); + result[i] = (nameStr, type[0], size[0]); + } + }); + return result; + } + + + + // + // INTERNAL_applyEffect + // + + public void INTERNAL_applyEffect(uint pass) + { + var graphicsDevice = GraphicsDevice; + Renderer.Get(graphicsDevice.GLDevice).Send( () => + { + GLES20.glUseProgram(programId); + int n = Parameters.Count; + for (int i = 0; i < n; i++) + { + if (! Parameters[i].Apply(i, graphicsDevice)) + { + throw new ArgumentException( + $"uniform {Parameters[i].Name} (#{i}) in effect {technique.Name}"); + } + } + }); + } + + + + // + // Dispose + // + + protected override void Dispose(bool disposing) + { + if (! base.IsDisposed) + { + Renderer.Get(GraphicsDevice.GLDevice).Send( () => + { + GLES20.glDeleteShader(fragmentId); + GLES20.glDeleteShader(vertexId); + GLES20.glDeleteProgram(programId); + fragmentId = 0; + vertexId = 0; + programId = 0; + }); + } + } + + + + // + // data + // + + private int programId; + private int vertexId, fragmentId; + + public EffectParameterCollection Parameters { get; private set; } + public EffectTechniqueCollection Techniques { get; private set; } + + private EffectTechnique technique; + public EffectTechnique CurrentTechnique + { + get => technique; + set => throw new System.PlatformNotSupportedException(); + } + + protected internal virtual void OnApply() { } + } + + + + public sealed class EffectParameter + { + public unsafe EffectParameter(string name, int type, int size) + { + Name = name; + this.type = type; + + int floatCount = 0; + int intCount = 0; + + if (type == android.opengl.GLES20.GL_FLOAT) + { + floatCount = 1; + if (size == 1 && name == "MultiplierY") + kind = 'Y'; + } + + else if (type == android.opengl.GLES20.GL_FLOAT_VEC2) + floatCount = 2; + else if (type == android.opengl.GLES20.GL_FLOAT_VEC3) + floatCount = 3; + else if (type == android.opengl.GLES20.GL_FLOAT_VEC4) + floatCount = 4; + + else if (type == android.opengl.GLES20.GL_FLOAT_MAT4) + floatCount = 4 * 4; + else if (type == android.opengl.GLES20.GL_FLOAT_MAT3) + floatCount = 3 * 3; + else if (type == android.opengl.GLES20.GL_FLOAT_MAT2) + floatCount = 2 * 2; + else if ( type == android.opengl.GLES30.GL_FLOAT_MAT3x4 + || type == android.opengl.GLES30.GL_FLOAT_MAT4x3) + { + floatCount = 4 * 3; + } + + else if ( type == android.opengl.GLES20.GL_INT + || type == android.opengl.GLES20.GL_BOOL) + { + intCount = 1; + } + + else if ( type == android.opengl.GLES20.GL_SAMPLER_2D + || type == android.opengl.GLES30.GL_SAMPLER_3D + || type == android.opengl.GLES20.GL_SAMPLER_CUBE) + { + kind = 'S'; + } + + else + { + throw new System.InvalidProgramException( + $"Effect: unsupported type {type:X8} in uniform '{name}'"); + } + + if (floatCount != 0) + { + var floatArray = new float[floatCount * size]; + fixed (float* floatPointer = &floatArray[0]) + values = (IntPtr) (void*) floatPointer; + storage = floatArray; + storageCopy = java.util.Arrays.copyOf(floatArray, floatArray.Length); + } + + else if (intCount != 0) + { + var intArray = new int[intCount * size]; + fixed (int* intPointer = &intArray[0]) + values = (IntPtr) (void*) intPointer; + storage = intArray; + storageCopy = java.util.Arrays.copyOf(intArray, intArray.Length); + } + + //GameRunner.Log($"EFFECT PARAMETER {Name} VALUES {values} STORAGE {this.storage}"); + } + + // + // bool value + // + + public bool GetValueBoolean() => GetValueBooleanArray(1)[0]; + + public bool[] GetValueBooleanArray(int count) + { + CheckCount(count); + var intArray = IntArray(android.opengl.GLES20.GL_BOOL, count); + var value = new bool[count]; + for (int i = 0; i < count; i++) + value[i] = intArray[i] != 0; + return value; + } + + public void SetValue(bool[] value) + { + int count = value.Length; + var intArray = IntArray(android.opengl.GLES20.GL_BOOL, count); + for (int i = 0; i < count; i++) + intArray[i] = value[i] ? 1 : 0; + } + + public void SetValue(bool value) => SetValue(new bool[] { value }); + + // + // int value + // + + public int GetValueInt32() => GetValueInt32Array(1)[0]; + + public int[] GetValueInt32Array(int count) + { + CheckCount(count); + return java.util.Arrays.copyOf( + IntArray(android.opengl.GLES20.GL_INT, count), count); + } + + public void SetValue(int[] value) + { + int count = value.Length; + var intArray = IntArray(android.opengl.GLES20.GL_INT, count); + for (int i = 0; i < count; i++) + intArray[i] = value[i]; + } + + public void SetValue(int value) => SetValue(new int[] { value }); + + // + // float value + // + + public float GetValueSingle() => GetValueSingleArray(1)[0]; + + public float[] GetValueSingleArray(int count) + { + CheckCount(count); + return java.util.Arrays.copyOf( + FloatArray(android.opengl.GLES20.GL_FLOAT, count), count); + } + + public void SetValue(float[] value) + { + int count = value.Length; + var floatArray = FloatArray(android.opengl.GLES20.GL_FLOAT, count); + for (int i = 0; i < count; i++) + floatArray[i] = value[i]; + } + + public void SetValue(float value) => SetValue(new float[] { value }); + + // + // Matrix + // + + public Matrix GetValueMatrix() => GetValueMatrixArray(1)[0]; + + public Matrix[] GetValueMatrixArray(int count) + { + CheckCount(count); + + int i = 0, j; + float[] floatArray; + // allocate an array of values without allocating each element + var value = (Matrix[]) java.lang.reflect.Array.newInstance( + (java.lang.Class) typeof(Matrix), count); + Matrix matrix; + + if (type == android.opengl.GLES20.GL_FLOAT_MAT4) + { + floatArray = FloatArray(type, 4 * 4 * count); + for (j = 0; j < count; j++) + { + matrix.M11 = floatArray[i++]; // 0 + matrix.M21 = floatArray[i++]; + matrix.M31 = floatArray[i++]; + matrix.M41 = floatArray[i++]; + matrix.M12 = floatArray[i++]; // 4 + matrix.M22 = floatArray[i++]; + matrix.M32 = floatArray[i++]; + matrix.M42 = floatArray[i++]; + matrix.M13 = floatArray[i++]; // 8 + matrix.M23 = floatArray[i++]; + matrix.M33 = floatArray[i++]; + matrix.M43 = floatArray[i++]; + matrix.M14 = floatArray[i++]; // 12 + matrix.M24 = floatArray[i++]; + matrix.M34 = floatArray[i++]; + matrix.M44 = floatArray[i++]; + java.lang.reflect.Array.set(value, j, matrix); // matrix is cloned + } + } + + else if (type == android.opengl.GLES20.GL_FLOAT_MAT3) + { + matrix.M41 = matrix.M42 = matrix.M43 = + matrix.M14 = matrix.M24 = matrix.M34 = matrix.M44 = 0; + floatArray = FloatArray(type, 3 * 3 * count); + for (j = 0; j < count; j++) + { + matrix.M11 = floatArray[i++]; // 0 + matrix.M21 = floatArray[i++]; + matrix.M31 = floatArray[i++]; + matrix.M12 = floatArray[i++]; // 3 + matrix.M22 = floatArray[i++]; + matrix.M32 = floatArray[i++]; + matrix.M13 = floatArray[i++]; // 6 + matrix.M23 = floatArray[i++]; + matrix.M33 = floatArray[i++]; + java.lang.reflect.Array.set(value, j, matrix); // matrix is cloned + } + } + + else + throw new InvalidCastException(Name + " MAT TYPE " + type); + + return value; + } + + public void SetValue(Matrix[] value) + { + int count = value.Length; + + int i = 0, j; + float[] floatArray; + Matrix matrix; + + if (type == android.opengl.GLES20.GL_FLOAT_MAT4) + { + floatArray = FloatArray(type, 4 * 4 * count); + for (j = 0; j < count; j++) + { + matrix = (Matrix) java.lang.reflect.Array.get(value, j); + floatArray[i++] = matrix.M11; // 0 + floatArray[i++] = matrix.M21; + floatArray[i++] = matrix.M31; + floatArray[i++] = matrix.M41; + floatArray[i++] = matrix.M12; // 4 + floatArray[i++] = matrix.M22; + floatArray[i++] = matrix.M32; + floatArray[i++] = matrix.M42; + floatArray[i++] = matrix.M13; // 8 + floatArray[i++] = matrix.M23; + floatArray[i++] = matrix.M33; + floatArray[i++] = matrix.M43; + floatArray[i++] = matrix.M14; // 12 + floatArray[i++] = matrix.M24; + floatArray[i++] = matrix.M34; + floatArray[i++] = matrix.M44; + } + } + + else if (type == android.opengl.GLES20.GL_FLOAT_MAT3) + { + floatArray = FloatArray(type, 3 * 3 * count); + for (j = 0; j < count; j++) + { + matrix = (Matrix) java.lang.reflect.Array.get(value, j); + floatArray[i++] = matrix.M11; // 0 + floatArray[i++] = matrix.M21; + floatArray[i++] = matrix.M31; + floatArray[i++] = matrix.M12; // 3 + floatArray[i++] = matrix.M22; + floatArray[i++] = matrix.M32; + floatArray[i++] = matrix.M13; // 6 + floatArray[i++] = matrix.M23; + floatArray[i++] = matrix.M33; + } + } + + else + throw new InvalidCastException(Name + " MAT TYPE " + type); + } + + public void SetValue(Matrix value) + { + // allocate an array of values without allocating each element + var array = (Matrix[]) java.lang.reflect.Array.newInstance( + (java.lang.Class) typeof(Matrix), 1); + java.lang.reflect.Array.set(array, 0, value); + SetValue(array); + } + + // + // Vector3 + // + + public Vector3 GetValueVector3() => GetValueVector3Array(1)[0]; + + public Vector3[] GetValueVector3Array(int count) + { + CheckCount(count); + var floatArray = FloatArray(android.opengl.GLES20.GL_FLOAT_VEC3, 3 * count); + + // allocate an array of values without allocating each element + var value = (Vector3[]) java.lang.reflect.Array.newInstance( + (java.lang.Class) typeof(Vector3), count); + Vector3 vector; + + int i = 0; + for (int j = 0; j < count; j++) + { + vector.X = floatArray[i++]; + vector.Y = floatArray[i++]; + vector.Z = floatArray[i++]; + java.lang.reflect.Array.set(value, j, vector); // vector is cloned + } + return value; + } + + public void SetValue(Vector3[] value) + { + int count = value.Length; + var floatArray = FloatArray(android.opengl.GLES20.GL_FLOAT_VEC3, 3 * count); + Vector3 vector; + int i = 0; + for (int j = 0; j < count; j++) + { + vector = (Vector3) java.lang.reflect.Array.get(value, j); + floatArray[i++] = vector.X; + floatArray[i++] = vector.Y; + floatArray[i++] = vector.Z; + } + } + + public void SetValue(Vector3 value) + { + // allocate an array of values without allocating each element + var array = (Vector3[]) java.lang.reflect.Array.newInstance( + (java.lang.Class) typeof(Vector3), 1); + java.lang.reflect.Array.set(array, 0, value); + SetValue(array); + } + + // + // Vector4 + // + + public Vector4 GetValueVector4() => GetValueVector4Array(1)[0]; + + public Vector4[] GetValueVector4Array(int count) + { + CheckCount(count); + var floatArray = FloatArray(android.opengl.GLES20.GL_FLOAT_VEC4, 4 * count); + + // allocate an array of values without allocating each element + var value = (Vector4[]) java.lang.reflect.Array.newInstance( + (java.lang.Class) typeof(Vector4), count); + Vector4 vector; + + int i = 0; + for (int j = 0; j < count; j++) + { + vector.X = floatArray[i++]; + vector.Y = floatArray[i++]; + vector.Z = floatArray[i++]; + vector.W = floatArray[i++]; + java.lang.reflect.Array.set(value, j, vector); // vector is cloned + } + return value; + } + + public void SetValue(Vector4[] value) + { + int count = value.Length; + var floatArray = FloatArray(android.opengl.GLES20.GL_FLOAT_VEC4, 4 * count); + Vector4 vector; + int i = 0; + for (int j = 0; j < count; j++) + { + vector = (Vector4) java.lang.reflect.Array.get(value, j); + floatArray[i++] = vector.X; + floatArray[i++] = vector.Y; + floatArray[i++] = vector.Z; + floatArray[i++] = vector.W; + } + } + + public void SetValue(Vector4 value) + { + // allocate an array of values without allocating each element + var array = (Vector4[]) java.lang.reflect.Array.newInstance( + (java.lang.Class) typeof(Vector4), 1); + java.lang.reflect.Array.set(array, 0, value); + SetValue(array); + } + + // + // texture + // + + public Texture2D GetValueTexture2D() => (Texture2D) storage; + + public Texture3D GetValueTexture3D() => (Texture3D) storage; + + public TextureCube GetValueTextureCube() => (TextureCube) storage; + + public void SetValue(Texture value) + { + if ( type == android.opengl.GLES20.GL_SAMPLER_2D + || type == android.opengl.GLES30.GL_SAMPLER_3D + || type == android.opengl.GLES20.GL_SAMPLER_CUBE) + { + storage = value; + } + } + + // + // string + // + + public void SetValue(string value) => throw new NotImplementedException(Name); + + // + // Array access + // + + int[] IntArray(int checkType, int checkCount) + { + if (type != checkType) + throw new InvalidCastException(); + var intArray = (int[]) storage; + if (intArray.Length < checkCount) + throw new InvalidCastException(); + return intArray; + } + + float[] FloatArray(int checkType, int checkCount) + { + if (type != checkType) + throw new InvalidCastException(Name); + var floatArray = (float[]) storage; + if (floatArray.Length < checkCount) + throw new InvalidCastException(Name); + return floatArray; + } + + void CheckCount(int count) + { + if (count <= 0) + throw new ArgumentOutOfRangeException(Name); + } + + // + // Apply + // + + public bool Apply(int uni, GraphicsDevice graphicsDevice) + { + if (storage is float[] floatArray) + { + if (kind == 'Y') + return ApplyMultiplierY(floatArray, uni, graphicsDevice); + else + return Apply(uni, floatArray); + } + + if (storage is int[] intArray) + return Apply(uni, intArray); + + if (kind == 'S') + { + if (storage != null) + { + // note that the uniform sampler (i.e. the texture + // unit selector) always remains set to default value + graphicsDevice.Textures[0] = (Texture) storage; + } + return true; + } + + return false; + } + + private bool Apply(int uni, float[] floatArray) + { + var floatArrayCopy = (float[]) storageCopy; + if (! java.util.Arrays.@equals(floatArray, floatArrayCopy)) + { + int num = floatArray.Length; + for (int i = 0; i < num; i++) + floatArrayCopy[i] = floatArray[i]; + + if (type == GLES20.GL_FLOAT) + GLES20.glUniform1fv(uni, num, floatArray, 0); + else if (type == GLES20.GL_FLOAT_VEC2) + GLES20.glUniform2fv(uni, num / 2, floatArray, 0); + else if (type == GLES20.GL_FLOAT_VEC3) + GLES20.glUniform3fv(uni, num / 3, floatArray, 0); + else if (type == GLES20.GL_FLOAT_VEC4) + GLES20.glUniform4fv(uni, num / 4, floatArray, 0); + else if (type == GLES20.GL_FLOAT_MAT4) + GLES20.glUniformMatrix4fv(uni, num / 16, true, floatArray, 0); + else if (type == GLES20.GL_FLOAT_MAT3) + GLES20.glUniformMatrix3fv(uni, num / 9, true, floatArray, 0); + else + return false; + } + return true; + } + + private bool Apply(int uni, int[] intArray) + { + var intArrayCopy = (int[]) storageCopy; + if (! java.util.Arrays.@equals(intArray, intArrayCopy)) + { + int num = intArray.Length; + for (int i = 0; i < num; i++) + intArrayCopy[i] = intArray[i]; + + if ( type == android.opengl.GLES20.GL_INT + || type == android.opengl.GLES20.GL_BOOL + || type == android.opengl.GLES20.GL_SAMPLER_2D) + { + GLES20.glUniform1iv(uni, num, intArray, 0); + } + else + return false; + } + return true; + } + + private bool ApplyMultiplierY(float[] floatArray, int uni, GraphicsDevice graphicsDevice) + { + // when rendering to texture, we have to flip vertically + var floatValue = FNA3D.IsRenderToTexture(graphicsDevice) ? -1f : 1f; + if (floatValue != floatArray[0]) + { + GLES20.glUniform1f(uni, floatValue); + floatArray[0] = floatValue; + } + return true; + } + + // + // data + // + + object storage; + object storageCopy; + int type; + char kind; + + public IntPtr values; + public string Name { get; private set; } + } + +} diff --git a/BNA/src/Empty.cs b/BNA/src/Empty.cs new file mode 100644 index 0000000..88d8b40 --- /dev/null +++ b/BNA/src/Empty.cs @@ -0,0 +1,27 @@ + +namespace Microsoft.Xna.Framework +{ + + public class LaunchParameters + { + } + + // this forwards audio events + + public static class FrameworkDispatcher + { + + public static void Update() + { + } + + } + + public static class FNAInternalExtensions + { + public static bool TryGetBuffer(System.IO.MemoryStream stream, ref byte[] buffer) + { + return false; + } + } +} diff --git a/BNA/src/FNA3D.cs b/BNA/src/FNA3D.cs new file mode 100644 index 0000000..20ba433 --- /dev/null +++ b/BNA/src/FNA3D.cs @@ -0,0 +1,577 @@ + +using System; +using android.opengl; +#pragma warning disable 0436 + +namespace Microsoft.Xna.Framework.Graphics +{ + + public static partial class FNA3D + { + + // + // FNA3D_SetViewport + // + + public static void FNA3D_SetViewport(IntPtr device, ref FNA3D_Viewport viewport) + { + var renderer = Renderer.Get(device); + var state = (State) renderer.UserData; + var v = viewport; + + if ( state.AdjustViewport && (! state.RenderToTexture) + && v.x == 0 && v.y == 0 && v.w > 0 && v.h > 0 + && v.w == state.BackBufferWidth && v.h == state.BackBufferHeight) + { + var (s_w, s_h) = (renderer.SurfaceWidth, renderer.SurfaceHeight); + if (v.w >= v.h) + { + // adjust from virtual landscape + v.h = (int) ((v.h * s_w) / (float) v.w); + v.w = s_w; + v.y = (s_h - v.h) / 2; + } + else + { + // adjust from virtual portrait + v.w = (int) ((v.w * s_w) / (float) v.h); + v.h = s_h; + v.x = (s_w - v.w) / 2; + } + } + + Renderer.Get(device).Send( () => + { + GLES20.glViewport(v.x, v.y, v.w, v.h); + GLES20.glDepthRangef(v.minDepth, v.maxDepth); + }); + } + + // + // FNA3D_SetScissorRect + // + + public static void FNA3D_SetScissorRect(IntPtr device, ref Rectangle scissor) + { + var s = scissor; + Renderer.Get(device).Send( () => + { + GLES20.glScissor(s.X, s.Y, s.Width, s.Height); + }); + } + + // + // FNA3D_Clear + // + + public static void FNA3D_Clear(IntPtr device, ClearOptions options, ref Vector4 color, + float depth, int stencil) + { + var clearColor = color; + var renderer = Renderer.Get(device); + renderer.Send( () => + { + var state = (State) renderer.UserData; + var WriteMask = state.WriteMask; + + if (state.ScissorTest) + { + // disable scissor before clear + GLES20.glDisable(GLES20.GL_SCISSOR_TEST); + } + + bool restoreColorMask = false; + bool restoreDepthMask = false; + bool restoreStencilMask = false; + + int mask = 0; + if ((options & ClearOptions.Target) != 0) + { + mask |= GLES20.GL_COLOR_BUFFER_BIT; + + if (clearColor != state.ClearColor) + { + state.ClearColor = clearColor; + GLES20.glClearColor( + clearColor.X, clearColor.Y, clearColor.Z, clearColor.W); + } + + if ((WriteMask & (RED_MASK | GREEN_MASK | BLUE_MASK | ALPHA_MASK)) + != (RED_MASK | GREEN_MASK | BLUE_MASK | ALPHA_MASK)) + { + // reset color masks before clear + GLES20.glColorMask(true, true, true, true); + restoreColorMask = true; + } + } + + if ((options & ClearOptions.DepthBuffer) != 0) + { + mask |= GLES20.GL_DEPTH_BUFFER_BIT; + + if (depth != state.ClearDepth) + { + state.ClearDepth = depth; + GLES20.glClearDepthf(depth); + } + + if ((state.WriteMask & DEPTH_MASK) == 0) + { + // reset depth mask before clear + GLES20.glDepthMask(true); + restoreDepthMask = true; + } + } + + if ((options & ClearOptions.Stencil) != 0) + { + mask |= GLES20.GL_STENCIL_BUFFER_BIT; + + if (stencil != state.ClearStencil) + { + state.ClearStencil = stencil; + GLES20.glClearStencil(stencil); + } + + if ((WriteMask & STENCIL_MASK) == 0) + { + // reset stencil mask before clear + GLES20.glStencilMask(-1); + restoreStencilMask = true; + } + } + + GLES20.glClear(mask); + + if (restoreStencilMask) + GLES20.glStencilMask(WriteMask & STENCIL_MASK); + + if (restoreDepthMask) + GLES20.glDepthMask(false); + + if (restoreColorMask) + { + GLES20.glColorMask((WriteMask & RED_MASK) != 0 ? true : false, + (WriteMask & GREEN_MASK) != 0 ? true : false, + (WriteMask & BLUE_MASK) != 0 ? true : false, + (WriteMask & ALPHA_MASK) != 0 ? true : false); + } + + if (state.ScissorTest) + { + // restore scissor after clear + GLES20.glEnable(GLES20.GL_SCISSOR_TEST); + } + }); + } + + // + // FNA3D_SwapBuffers + // + + public static void FNA3D_SwapBuffers(IntPtr device, + IntPtr sourceRectangle, + IntPtr destinationRectangle, + IntPtr overrideWindowHandle) + { + if ( (long) sourceRectangle != 0 + || (long) destinationRectangle != 0 + || (long) overrideWindowHandle != 0) + { + throw new PlatformNotSupportedException(); + } + var renderer = Renderer.Get(device); + renderer.Present(); + } + + // + // FNA3D_SetBlendState + // + + public static void FNA3D_SetBlendState(IntPtr device, ref FNA3D_BlendState blendState) + { + var input = blendState; + var renderer = Renderer.Get(device); + Renderer.Get(device).Send( () => + { + var state = (State) renderer.UserData; + + if ( input.colorSourceBlend != Blend.One + || input.colorDestinationBlend != Blend.Zero + || input.alphaSourceBlend != Blend.One + || input.alphaDestinationBlend != Blend.Zero) + { + // + // blend state + // + + if (! state.BlendEnable) + { + state.BlendEnable = true; + GLES20.glEnable(GLES20.GL_BLEND); + } + + // + // XNA blend factor / GL blend color + // + + if (input.blendFactor != state.BlendColor) + { + state.BlendColor = input.blendFactor; + + GLES20.glBlendColor(state.BlendColor.R / 255f, state.BlendColor.G / 255f, + state.BlendColor.B / 255f, state.BlendColor.A / 255f); + } + + // + // XNA blend mode / GL blend function + // + + if ( input.colorSourceBlend != state.BlendSrcColor + || input.colorDestinationBlend != state.BlendDstColor + || input.alphaSourceBlend != state.BlendSrcAlpha + || input.alphaDestinationBlend != state.BlendDstAlpha) + { + state.BlendSrcColor = input.colorSourceBlend; + state.BlendDstColor = input.colorDestinationBlend; + state.BlendSrcAlpha = input.alphaSourceBlend; + state.BlendDstAlpha = input.alphaDestinationBlend; + + GLES20.glBlendFuncSeparate( + BlendModeToBlendFunc[(int) state.BlendSrcColor], + BlendModeToBlendFunc[(int) state.BlendDstColor], + BlendModeToBlendFunc[(int) state.BlendSrcAlpha], + BlendModeToBlendFunc[(int) state.BlendDstAlpha]); + } + + // + // XNA blend function / GL blend equation + // + + if ( input.colorBlendFunction != state.BlendFuncColor + || input.alphaBlendFunction != state.BlendFuncAlpha) + { + state.BlendFuncColor = input.colorBlendFunction; + state.BlendFuncAlpha = input.alphaBlendFunction; + + GLES20.glBlendEquationSeparate( + BlendFunctionToBlendEquation[(int) state.BlendFuncColor], + BlendFunctionToBlendEquation[(int) state.BlendFuncAlpha]); + } + + // + // color write mask + // + + bool inputRed = ((input.colorWriteEnable & ColorWriteChannels.Red) != 0); + bool inputGreen = ((input.colorWriteEnable & ColorWriteChannels.Green) != 0); + bool inputBlue = ((input.colorWriteEnable & ColorWriteChannels.Blue) != 0); + bool inputAlpha = ((input.colorWriteEnable & ColorWriteChannels.Alpha) != 0); + var WriteMask = state.WriteMask; + + if ( inputRed != ((WriteMask & RED_MASK) != 0) + || inputGreen != ((WriteMask & GREEN_MASK) != 0) + || inputBlue != ((WriteMask & BLUE_MASK) != 0) + || inputAlpha != ((WriteMask & ALPHA_MASK) != 0)) + { + state.WriteMask = (inputRed ? RED_MASK : 0) + | (inputGreen ? GREEN_MASK : 0) + | (inputBlue ? BLUE_MASK : 0) + | (inputAlpha ? ALPHA_MASK : 0); + + GLES20.glColorMask(inputRed, inputGreen, inputBlue, inputAlpha); + } + } + else + { + state.BlendEnable = false; + GLES20.glDisable(GLES20.GL_BLEND); + } + }); + } + + static int[] BlendModeToBlendFunc = new int[] + { + GLES20.GL_ONE, // Blend.One + GLES20.GL_ZERO, // Blend.Zero + GLES20.GL_SRC_COLOR, // Blend.SourceColor + GLES20.GL_ONE_MINUS_SRC_COLOR, // Blend.InverseSourceColor + GLES20.GL_SRC_ALPHA, // Blend.SourceAlpha + GLES20.GL_ONE_MINUS_SRC_ALPHA, // Blend.InverseSourceAlpha + GLES20.GL_DST_COLOR, // Blend.DestinationColor + GLES20.GL_ONE_MINUS_DST_COLOR, // Blend.InverseDestinationColor + GLES20.GL_DST_ALPHA, // Blend.DestinationAlpha + GLES20.GL_ONE_MINUS_DST_ALPHA, // Blend.InverseDestinationAlpha + GLES20.GL_CONSTANT_COLOR, // Blend.BlendFactor + GLES20.GL_ONE_MINUS_CONSTANT_COLOR, // Blend.InverseBlendFactor + GLES20.GL_SRC_ALPHA_SATURATE // Blend.SourceAlphaSaturation + }; + + static int[] BlendFunctionToBlendEquation = new int[] + { + GLES20.GL_FUNC_ADD, // BlendFunction.Add + GLES20.GL_FUNC_SUBTRACT, // BlendFunction.Subtract + GLES20.GL_FUNC_REVERSE_SUBTRACT, // BlendFunction.ReverseSubtract + GLES30.GL_MAX, // BlendFunction.Max + GLES30.GL_MIN // BlendFunction.Min + }; + + // + // FNA3D_SetDepthStencilState + // + + public static void FNA3D_SetDepthStencilState(IntPtr device, + ref FNA3D_DepthStencilState depthStencilState) + { + // AndroidActivity.LogI(">>>>> SET DEPTH AND STENCIL STATE"); + } + + // + // FNA3D_ApplyRasterizerState + // + + public static void FNA3D_ApplyRasterizerState(IntPtr device, + ref FNA3D_RasterizerState rasterizerState) + { + var input = rasterizerState; + var renderer = Renderer.Get(device); + Renderer.Get(device).Send( () => + { + var state = (State) renderer.UserData; + + var inputScissorTest = (input.scissorTestEnable != 0); + if (inputScissorTest != state.ScissorTest) + { + state.ScissorTest = inputScissorTest; + if (inputScissorTest) + GLES20.glEnable(GLES20.GL_SCISSOR_TEST); + else + GLES20.glDisable(GLES20.GL_SCISSOR_TEST); + } + + int inputCullMode; + if (state.RenderToTexture) + { + // select culling mode when rendering to texture, where we + // flip vertically; see also EffectParameter::ApplyMultiplierY + inputCullMode = + (input.cullMode == CullMode.CullCounterClockwiseFace) ? GLES20.GL_BACK + : (input.cullMode == CullMode.CullClockwiseFace) ? GLES20.GL_FRONT : 0; + } + else + { + // select culling mode when rendering directly to the screen + inputCullMode = + (input.cullMode == CullMode.CullCounterClockwiseFace) ? GLES20.GL_FRONT + : (input.cullMode == CullMode.CullClockwiseFace) ? GLES20.GL_BACK : 0; + } + if (inputCullMode != state.CullMode) + { + state.CullMode = inputCullMode; + if (inputCullMode == 0) + GLES20.glDisable(GLES20.GL_CULL_FACE); + else + { + GLES20.glEnable(GLES20.GL_CULL_FACE); + GLES20.glCullFace(inputCullMode); + } + } + + // fillMode (glPolygonMode) is not supported on GL ES, + // so we also ignore depthBias (glPolygonOffset) + }); + } + + public static int FNA3D_GetMaxMultiSampleCount(IntPtr device, SurfaceFormat format, + int preferredMultiSampleCount) + { + return 0; + + #if false + for (int i = 0; i < 21; i++) + GLES30.glGetInternalformativ(GLES20.GL_RENDERBUFFER, + SurfaceFormatToTextureInternalFormat[i], + GLES20.GL_SAMPLES, 1, count, 0); + if (GLES20.glGetError() == 0 && count[0] < preferredMultiSampleCount) + preferredMultiSampleCount = count[0]; + #endif + } + + // + // FNA3D_DrawIndexedPrimitives + // + + public static void FNA3D_DrawIndexedPrimitives(IntPtr device, PrimitiveType primitiveType, + int baseVertex, int minVertexIndex, + int numVertices, int startIndex, + int primitiveCount, IntPtr indices, + IndexElementSize indexElementSize) + { + int elementSize, elementType; + if (indexElementSize == IndexElementSize.SixteenBits) + { + elementSize = 2; + elementType = GLES20.GL_UNSIGNED_SHORT; + } + else if (indexElementSize == IndexElementSize.ThirtyTwoBits) + { + elementSize = 4; + elementType = GLES20.GL_UNSIGNED_INT; + } + else + throw new ArgumentException("invalid IndexElementSize"); + + int drawMode = PrimitiveTypeToDrawMode[(int) primitiveType]; + int maxVertexIndex = minVertexIndex + numVertices - 1; + int indexOffset = startIndex * elementSize; + primitiveCount = PrimitiveCount(primitiveType, primitiveCount); + + Renderer.Get(device).Send( () => + { + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, (int) indices); + GLES30.glDrawRangeElements(drawMode, minVertexIndex, maxVertexIndex, + primitiveCount, elementType, indexOffset); + }); + } + + static int[] PrimitiveTypeToDrawMode = new int[] + { + GLES20.GL_TRIANGLES, // PrimitiveType.TriangleList + GLES20.GL_TRIANGLE_STRIP, // PrimitiveType.TriangleStrip + GLES20.GL_LINES, // PrimitiveType.LineList + GLES20.GL_LINE_STRIP, // PrimitiveType.LineStrip + }; + + static int PrimitiveCount(PrimitiveType primitiveType, int primitiveCount) + { + return primitiveType switch + { + PrimitiveType.TriangleList => primitiveCount * 3, + PrimitiveType.TriangleStrip => primitiveCount + 2, + PrimitiveType.LineList => primitiveCount * 2, + PrimitiveType.LineStrip => primitiveCount + 1, + _ => throw new ArgumentException("invalid PrimitiveType"), + }; + } + + // + // FNA3D_DrawPrimitives + // + + public static void FNA3D_DrawPrimitives(IntPtr device, PrimitiveType primitiveType, + int vertexStart, int primitiveCount) + { + int drawMode = PrimitiveTypeToDrawMode[(int) primitiveType]; + primitiveCount = PrimitiveCount(primitiveType, primitiveCount); + + Renderer.Get(device).Send( () => + { + GLES20.glDrawArrays(drawMode, vertexStart, primitiveCount); + }); + } + + // + // FNA3D_BlendState + // + + public struct FNA3D_BlendState + { + public Blend colorSourceBlend; + public Blend colorDestinationBlend; + public BlendFunction colorBlendFunction; + public Blend alphaSourceBlend; + public Blend alphaDestinationBlend; + public BlendFunction alphaBlendFunction; + public ColorWriteChannels colorWriteEnable; + public ColorWriteChannels colorWriteEnable1; + public ColorWriteChannels colorWriteEnable2; + public ColorWriteChannels colorWriteEnable3; + public Color blendFactor; + public int multiSampleMask; + } + + // + // FNA3D_DepthStencilState + // + + public struct FNA3D_DepthStencilState + { + public byte depthBufferEnable; + public byte depthBufferWriteEnable; + public CompareFunction depthBufferFunction; + public byte stencilEnable; + public int stencilMask; + public int stencilWriteMask; + public byte twoSidedStencilMode; + public StencilOperation stencilFail; + public StencilOperation stencilDepthBufferFail; + public StencilOperation stencilPass; + public CompareFunction stencilFunction; + public StencilOperation ccwStencilFail; + public StencilOperation ccwStencilDepthBufferFail; + public StencilOperation ccwStencilPass; + public CompareFunction ccwStencilFunction; + public int referenceStencil; + } + + // + // FNA3D_RasterizerState + // + + public struct FNA3D_RasterizerState + { + public FillMode fillMode; + public CullMode cullMode; + public float depthBias; + public float slopeScaleDepthBias; + public byte scissorTestEnable; + public byte multiSampleAntiAlias; + } + + // + // FNA3D_Viewport + // + + public struct FNA3D_Viewport + { + public int x; + public int y; + public int w; + public int h; + public float minDepth; + public float maxDepth; + } + + // + // State + // + + private partial class State + { + public Vector4 ClearColor; + public float ClearDepth; + public int ClearStencil; + public int WriteMask = -1; + public int CullMode; + public bool ScissorTest; + + public bool BlendEnable; + public Color BlendColor; + + public Blend BlendSrcColor; + public Blend BlendDstColor = Blend.Zero; + public Blend BlendSrcAlpha; + public Blend BlendDstAlpha = Blend.Zero; + public BlendFunction BlendFuncColor; + public BlendFunction BlendFuncAlpha; + } + + private const int DEPTH_MASK = 0x40000000; + private const int ALPHA_MASK = 0x20000000; + private const int BLUE_MASK = 0x04000000; + private const int GREEN_MASK = 0x02000000; + private const int RED_MASK = 0x01000000; + private const int STENCIL_MASK = 0x00FFFFFF; + } + +} diff --git a/BNA/src/FNA3D_Buf.cs b/BNA/src/FNA3D_Buf.cs new file mode 100644 index 0000000..428e03f --- /dev/null +++ b/BNA/src/FNA3D_Buf.cs @@ -0,0 +1,498 @@ + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using android.opengl; +#pragma warning disable 0436 + +namespace Microsoft.Xna.Framework.Graphics +{ + + public static partial class FNA3D + { + + // + // FNA3D_SupportsNoOverwrite + // + + public static byte FNA3D_SupportsNoOverwrite(IntPtr device) + { + // prevent flag SetDataOptions.NoOverwrite in Set*BufferData calls + return 0; + } + + + + // + // Create Buffers + // + + private static int CreateBuffer(Renderer renderer, int target, byte dynamic, int size) + { + int bufferId = 0; + renderer.Send( () => + { + int[] id = new int[1]; + GLES20.glGenBuffers(1, id, 0); + if (id[0] != 0) + { + bufferId = id[0]; + int usage = (dynamic != 0 ? GLES20.GL_STREAM_DRAW + : GLES20.GL_STATIC_DRAW); + GLES20.glBindBuffer(target, bufferId); + GLES20.glBufferData(target, size, null, usage); + + var state = (State) renderer.UserData; + state.BufferSizeUsage[bufferId] = new int[] { size, usage }; + } + }); + return bufferId; + } + + public static IntPtr FNA3D_GenVertexBuffer(IntPtr device, byte dynamic, + BufferUsage usage, int sizeInBytes) + { + return (IntPtr) CreateBuffer(Renderer.Get(device), + GLES20.GL_ARRAY_BUFFER, + dynamic, sizeInBytes); + } + + public static IntPtr FNA3D_GenIndexBuffer(IntPtr device, byte dynamic, + BufferUsage usage, int sizeInBytes) + { + return (IntPtr) CreateBuffer(Renderer.Get(device), + GLES20.GL_ELEMENT_ARRAY_BUFFER, + dynamic, sizeInBytes); + } + + + + // + // Delete Buffers + // + + public static void FNA3D_AddDisposeVertexBuffer(IntPtr device, IntPtr buffer) + { + var renderer = Renderer.Get(device); + renderer.Send( () => + { + GLES20.glDeleteBuffers(1, new int[] { (int) buffer }, 0); + + var state = (State) renderer.UserData; + state.BufferSizeUsage.Remove((int) buffer); + }); + } + + public static void FNA3D_AddDisposeIndexBuffer(IntPtr device, IntPtr buffer) + { + FNA3D_AddDisposeVertexBuffer(device, buffer); + } + + + + // + // Set Buffer Data + // + + private static void SetBufferData(Renderer renderer, int target, int bufferId, + int bufferOffset, bool discard, + IntPtr dataPointer, int dataLength) + { + var state = (State) renderer.UserData; + var dataBuffer = BufferSerializer.Convert( + dataPointer, dataLength, state, bufferId); + + renderer.Send( () => + { + GLES20.glBindBuffer(target, bufferId); + + if (discard) + { + var sizeUsage = state.BufferSizeUsage[bufferId]; + GLES20.glBufferData(target, sizeUsage[0], null, sizeUsage[1]); + } + + GLES20.glBufferSubData(target, bufferOffset, dataLength, dataBuffer); + }); + } + + public static void FNA3D_SetVertexBufferData(IntPtr device, IntPtr buffer, + int offsetInBytes, IntPtr data, + int elementCount, int elementSizeInBytes, + int vertexStride, SetDataOptions options) + { + if (elementSizeInBytes != vertexStride) + throw new System.ArgumentException("elementSizeInBytes != vertexStride"); + + SetBufferData(Renderer.Get(device), GLES20.GL_ARRAY_BUFFER, + (int) buffer, offsetInBytes, + (options == SetDataOptions.Discard), + data, elementCount * elementSizeInBytes); + } + + public static void FNA3D_SetIndexBufferData(IntPtr device, IntPtr buffer, + int offsetInBytes, IntPtr data, + int dataLength, SetDataOptions options) + { + SetBufferData(Renderer.Get(device), GLES20.GL_ELEMENT_ARRAY_BUFFER, + (int) buffer, offsetInBytes, + (options == SetDataOptions.Discard), + data, dataLength); + } + + + + // + // Set Buffer Attributes + // + + public static unsafe void FNA3D_ApplyVertexBufferBindings(IntPtr device, + FNA3D_VertexBufferBinding* bindings, + int numBindings, byte bindingsUpdated, + int baseVertex) + { + var bindingsCopy = new FNA3D_VertexBufferBinding[numBindings]; + for (int i = 0; i < numBindings; i++) + bindingsCopy[i] = bindings[i]; + + Renderer.Get(device).Send( () => + { + int nextAttribIndex = 0; + foreach (var binding in bindingsCopy) + { + if (binding.instanceFrequency != 0) + throw new ArgumentException("InstanceFrequnecy != 0"); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, (int) binding.vertexBuffer); + + var vertexDecl = binding.vertexDeclaration; + var elements = (VertexElement[]) System.Runtime.InteropServices.GCHandle + .FromIntPtr(vertexDecl.elements).Target; + for (int j = 0; j < vertexDecl.elementCount; j++) + { + if (elements[j].UsageIndex != 0) + throw new ArgumentException("UsageIndex != 0"); + var fmt = elements[j].VertexElementFormat; + + int size = VertexElementToBindingSize[(int) fmt]; + int type = VertexElementToBindingType[(int) fmt]; + bool norm = ( elements[j].VertexElementUsage == + VertexElementUsage.Color + || fmt == VertexElementFormat.NormalizedShort2 + || fmt == VertexElementFormat.NormalizedShort4); + + int stride = vertexDecl.vertexStride; + int offset = (binding.vertexOffset + baseVertex) * stride + + elements[j].Offset; + + GLES20.glVertexAttribPointer(nextAttribIndex, size, type, norm, + stride, offset); + + GLES20.glEnableVertexAttribArray(nextAttribIndex++); + } + } + }); + } + + // + // FNA3D_VertexBufferBinding + // + + public struct FNA3D_VertexBufferBinding + { + public IntPtr vertexBuffer; + public FNA3D_VertexDeclaration vertexDeclaration; + public int vertexOffset; + public int instanceFrequency; + } + + // + // FNA3D_VertexDeclaration + // + + public struct FNA3D_VertexDeclaration + { + public int vertexStride; + public int elementCount; + public IntPtr elements; + } + + // + // VertexElementToBindingSize + // + + static int[] VertexElementToBindingSize = new int[] + { + 1, // VertexElementFormat.Single + 2, // VertexElementFormat.Vector2 + 3, // VertexElementFormat.Vector3 + 4, // VertexElementFormat.Vector4 + 4, // VertexElementFormat.Color + 4, // VertexElementFormat.Byte4 + 2, // VertexElementFormat.Short2 + 4, // VertexElementFormat.Short4 + 2, // VertexElementFormat.NormalizedShort2 + 4, // VertexElementFormat.NormalizedShort4 + 2, // VertexElementFormat.HalfVector2 + 4 // VertexElementFormat.HalfVector4 + }; + + // + // VertexElementToBindingType + // + + static int[] VertexElementToBindingType = new int[] + { + GLES20.GL_FLOAT, // VertexElementFormat.Single + GLES20.GL_FLOAT, // VertexElementFormat.Vector2 + GLES20.GL_FLOAT, // VertexElementFormat.Vector3 + GLES20.GL_FLOAT, // VertexElementFormat.Vector4 + GLES20.GL_UNSIGNED_BYTE, // VertexElementFormat.Color + GLES20.GL_UNSIGNED_BYTE, // VertexElementFormat.Byte4 + GLES20.GL_SHORT, // VertexElementFormat.Short2 + GLES20.GL_SHORT, // VertexElementFormat.Short4 + GLES20.GL_SHORT, // VertexElementFormat.NormalizedShort2 + GLES20.GL_SHORT, // VertexElementFormat.NormalizedShort4 + GLES30.GL_HALF_FLOAT, // VertexElementFormat.HalfVector2 + GLES30.GL_HALF_FLOAT // VertexElementFormat.HalfVector4 + }; + + + + // + // BufferSerializer + // + + private static class BufferSerializer + { + + public static java.nio.Buffer Convert(IntPtr data, int length, + State state, int bufferId) + { + java.nio.Buffer oldBuffer, newBuffer; + lock (state.BufferCache) + { + state.BufferCache.TryGetValue(bufferId, out oldBuffer); + } + + // FNA IndexBuffer uses GCHandle::Alloc and GCHandle::AddrOfPinnedObject. + // 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); + + if (newBuffer != oldBuffer) + { + lock (state.BufferCache) + { + state.BufferCache[bufferId] = newBuffer; + } + } + + return newBuffer; + } + + + public static java.nio.Buffer Convert(object data, int offset, int length, + java.nio.Buffer buffer) + { + if (data is short[]) + { + return FromShort((short[]) data, offset, length); + } + + var byteBuffer = (buffer != null && buffer.limit() >= length) + ? (java.nio.ByteBuffer) buffer + : java.nio.ByteBuffer.allocateDirect(length) + .order(java.nio.ByteOrder.nativeOrder()); + + if (data is SpriteBatch.VertexPositionColorTexture4[]) + { + FromVertexPositionColorTexture4( + (SpriteBatch.VertexPositionColorTexture4[]) data, + offset, length, byteBuffer); + } + + else if (data is VertexPositionColor[]) + { + FromVertexPositionColor( + (VertexPositionColor[]) data, offset, length, byteBuffer); + } + + else if (data is VertexPositionColorTexture[]) + { + FromVertexPositionColorTexture( + (VertexPositionColorTexture[]) data, offset, length, byteBuffer); + } + + else if (data is VertexPositionNormalTexture[]) + { + FromVertexPositionNormalTexture( + (VertexPositionNormalTexture[]) data, offset, length, byteBuffer); + } + + else if (data is VertexPositionTexture[]) + { + FromVertexPositionTexture( + (VertexPositionTexture[]) data, offset, length, byteBuffer); + } + + else + { + /*IVertexType iVertexType, + => FromVertexDeclaration(iVertexType.VertexDeclaration, + data, offset, length),*/ + + throw new ArgumentException($"unsupported buffer type '{data.GetType()}'"); + }; + + return byteBuffer.position(0); + } + + private static void ValidateOffsetAndLength(int offset, int length, int divisor) + { + if ((offset % divisor) != 0 || (length % divisor) != 0) + throw new ArgumentException( + $"length and offset of buffer should be divisible by {divisor}"); + } + + private static java.nio.Buffer FromShort(short[] array, int offset, int length) + { + ValidateOffsetAndLength(offset, length, 2); + return java.nio.ShortBuffer.wrap(array, + offset / sizeof(short), + length / sizeof(short)); + } + + private static void FromVertexPositionColorTexture4( + SpriteBatch.VertexPositionColorTexture4[] array, + int offset, int length, java.nio.ByteBuffer buffer) + { + ValidateOffsetAndLength(offset, length, 96); + + int index = offset / 96; + int count = length / 96; + for (; count-- > 0; index++) + { + PutVector3(buffer, ref array[index].Position0); + PutColor(buffer, ref array[index].Color0); + PutVector2(buffer, ref array[index].TextureCoordinate0); + + PutVector3(buffer, ref array[index].Position1); + PutColor(buffer, ref array[index].Color1); + PutVector2(buffer, ref array[index].TextureCoordinate1); + + PutVector3(buffer, ref array[index].Position2); + PutColor(buffer, ref array[index].Color2); + PutVector2(buffer, ref array[index].TextureCoordinate2); + + PutVector3(buffer, ref array[index].Position3); + PutColor(buffer, ref array[index].Color3); + PutVector2(buffer, ref array[index].TextureCoordinate3); + } + } + + private static void FromVertexPositionColor( + VertexPositionColor[] array, + int offset, int length, java.nio.ByteBuffer buffer) + { + ValidateOffsetAndLength(offset, length, 16); + + int index = offset / 16; + int count = length / 16; + for (; count-- > 0; index++) + { + PutVector3(buffer, ref array[index].Position); + PutColor(buffer, ref array[index].Color); + } + } + + private static void FromVertexPositionColorTexture( + VertexPositionColorTexture[] array, + int offset, int length, java.nio.ByteBuffer buffer) + { + ValidateOffsetAndLength(offset, length, 24); + + int index = offset / 24; + int count = length / 24; + for (; count-- > 0; index++) + { + PutVector3(buffer, ref array[index].Position); + PutColor(buffer, ref array[index].Color); + PutVector2(buffer, ref array[index].TextureCoordinate); + } + } + + private static void FromVertexPositionNormalTexture( + VertexPositionNormalTexture[] array, + int offset, int length, java.nio.ByteBuffer buffer) + { + ValidateOffsetAndLength(offset, length, 32); + + int index = offset / 32; + int count = length / 32; + for (; count-- > 0; index++) + { + PutVector3(buffer, ref array[index].Position); + PutVector3(buffer, ref array[index].Normal); + PutVector2(buffer, ref array[index].TextureCoordinate); + } + } + + private static void FromVertexPositionTexture( + VertexPositionTexture[] array, + int offset, int length, java.nio.ByteBuffer buffer) + { + ValidateOffsetAndLength(offset, length, 20); + + int index = offset / 20; + int count = length / 20; + for (; count-- > 0; index++) + { + PutVector3(buffer, ref array[index].Position); + PutVector2(buffer, ref array[index].TextureCoordinate); + } + } + + /*private static java.nio.Buffer FromVertexDeclaration( + VertexDeclaration vertexDeclaration, + object data, int offset, int length) { } */ + + private static void PutVector3(java.nio.ByteBuffer buffer, ref Vector3 vector3) + { + buffer.putFloat(vector3.X); + buffer.putFloat(vector3.Y); + buffer.putFloat(vector3.Z); + } + + private static void PutVector2(java.nio.ByteBuffer buffer, ref Vector2 vector2) + { + buffer.putFloat(vector2.X); + buffer.putFloat(vector2.Y); + } + + private static void PutColor(java.nio.ByteBuffer buffer, ref Color color) + { + buffer.put((sbyte) color.R); + buffer.put((sbyte) color.G); + buffer.put((sbyte) color.B); + buffer.put((sbyte) color.A); + } + + } + + + + // + // State + // + + private partial class State + { + public Dictionary BufferSizeUsage = new Dictionary(); + public Dictionary BufferCache = new Dictionary(); + } + + } + +} diff --git a/BNA/src/FNA3D_Dev.cs b/BNA/src/FNA3D_Dev.cs new file mode 100644 index 0000000..9cabcf1 --- /dev/null +++ b/BNA/src/FNA3D_Dev.cs @@ -0,0 +1,140 @@ + +using System; +using android.opengl; +#pragma warning disable 0436 + +namespace Microsoft.Xna.Framework.Graphics +{ + + public static partial class FNA3D + { + + // + // FNA3D_CreateDevice + // + + public static IntPtr FNA3D_CreateDevice( + ref FNA3D_PresentationParameters presentationParameters, + byte debugMode) + { + int depthSize, stencilSize = 0; + switch (presentationParameters.depthStencilFormat) + { + case DepthFormat.None: depthSize = 0; break; + case DepthFormat.Depth16: depthSize = 16; break; + case DepthFormat.Depth24: depthSize = 24; break; + case DepthFormat.Depth24Stencil8: depthSize = 24; stencilSize = 8; break; + default: throw new ArgumentException("depthStencilFormat"); + } + + var device = Renderer.Create(GameRunner.Singleton.Activity, + GameRunner.Singleton.OnSurfaceChanged, + 8, 8, 8, 0, depthSize, stencilSize); + 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) + };*/ + } + + // + // FNA3D_ResetBackbuffer + // + + public static void FNA3D_ResetBackbuffer(IntPtr device, + ref FNA3D_PresentationParameters presentationParameters) + { + var renderer = Renderer.Get(device); + + var state = (State) renderer.UserData; + if (state == null) + { + state = new State(); + renderer.UserData = state; + } + + state.BackBufferWidth = presentationParameters.backBufferWidth; + state.BackBufferHeight = presentationParameters.backBufferHeight; + state.AdjustViewport = // see also FNA3D_SetViewport + (presentationParameters.displayOrientation == DisplayOrientation.Default); + + presentationParameters.backBufferFormat = SurfaceFormat.Color; + presentationParameters.isFullScreen = 1; + } + + // + // FNA3D_DestroyDevice + // + + public static void FNA3D_DestroyDevice(IntPtr device) + { + Renderer.Get(device).Release(); + } + + // + // FNA3D_GetMaxTextureSlots + // + + public static void FNA3D_GetMaxTextureSlots(IntPtr device, + out int textures, out int vertexTextures) + { + // XNA GraphicsDevice Limits from FNA3D/src/FNA3D_Driver.h + const int MAX_TEXTURE_SAMPLERS = 16; + const int MAX_VERTEXTEXTURE_SAMPLERS = 4; + + var renderer = Renderer.Get(device); + int numSamplers = renderer.TextureUnits; + // number of texture slots + textures = Math.Min(numSamplers, MAX_TEXTURE_SAMPLERS); + // number of vertex texture slots + vertexTextures = Math.Min(Math.Max(numSamplers - MAX_TEXTURE_SAMPLERS, 0), + MAX_VERTEXTEXTURE_SAMPLERS); + } + + // + // FNA3D_GetBackbufferDepthFormat + // + + public static DepthFormat FNA3D_GetBackbufferDepthFormat(IntPtr device) + { + return Renderer.Get(device).SurfaceDepthFormat; + } + + // + // FNA3D_PresentationParameters + // + + public struct FNA3D_PresentationParameters + { + public int backBufferWidth; + public int backBufferHeight; + public SurfaceFormat backBufferFormat; + public int multiSampleCount; + public IntPtr deviceWindowHandle; + public byte isFullScreen; + public DepthFormat depthStencilFormat; + public PresentInterval presentationInterval; + public DisplayOrientation displayOrientation; + public RenderTargetUsage renderTargetUsage; + } + + // + // State + // + + private partial class State + { + public int BackBufferWidth; + public int BackBufferHeight; + public bool AdjustViewport; + } + + } + +} diff --git a/BNA/src/FNA3D_Rt.cs b/BNA/src/FNA3D_Rt.cs new file mode 100644 index 0000000..959cc58 --- /dev/null +++ b/BNA/src/FNA3D_Rt.cs @@ -0,0 +1,247 @@ + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using android.opengl; +#pragma warning disable 0436 + +namespace Microsoft.Xna.Framework.Graphics +{ + + public static partial class FNA3D + { + + // + // FNA3D_SetRenderTargets + // + + public static unsafe void FNA3D_SetRenderTargets(IntPtr device, + FNA3D_RenderTargetBinding* renderTargets, + int numRenderTargets, + IntPtr depthStencilBuffer, + DepthFormat depthFormat, + byte preserveContents) + { + var renderTargetsCopy = new FNA3D_RenderTargetBinding[numRenderTargets]; + for (int i = 0; i < numRenderTargets; i++) + renderTargetsCopy[i] = renderTargets[i]; + + var renderer = Renderer.Get(device); + renderer.Send( () => + { + var state = (State) renderer.UserData; + if (state.TargetFramebuffer == 0) + { + var id = new int[1]; + GLES20.glGenFramebuffers(1, id, 0); + if ((state.TargetFramebuffer = id[0]) == 0) + return; + } + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, state.TargetFramebuffer); + + int attachmentIndex = GLES20.GL_COLOR_ATTACHMENT0; + foreach (var renderTarget in renderTargetsCopy) + { + if (renderTarget.colorBuffer != IntPtr.Zero) + { + // a color buffer is only created if a non-zero result + // from FNA3D_GetMaxMultiSampleCount, which we never do + throw new PlatformNotSupportedException(); + /*GLES20.glFramebufferRenderbuffer( + GLES20.GL_FRAMEBUFFER, attachmentIndex, + GLES20.GL_RENDERBUFFER, (int) renderTarget.colorBuffer);*/ + } + else + { + int attachmentType = GLES20.GL_TEXTURE_2D; + if (renderTarget.type != /* FNA3D_RENDERTARGET_TYPE_2D */ 0) + { + attachmentType = GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_X + + renderTarget.data2; + } + GLES20.glFramebufferTexture2D( + GLES20.GL_FRAMEBUFFER, attachmentIndex, + attachmentType, (int) renderTarget.texture, 0); + } + attachmentIndex++; + } + + int lastAttachmentPlusOne = state.ActiveAttachments; + state.ActiveAttachments = attachmentIndex; + while (attachmentIndex < lastAttachmentPlusOne) + { + GLES20.glFramebufferRenderbuffer( + GLES20.GL_FRAMEBUFFER, attachmentIndex++, + GLES20.GL_RENDERBUFFER, 0); + } + + GLES20.glFramebufferRenderbuffer( + GLES20.GL_FRAMEBUFFER, GLES30.GL_DEPTH_STENCIL_ATTACHMENT, + GLES20.GL_RENDERBUFFER, (int) depthStencilBuffer); + + state.RenderToTexture = true; + }); + } + + // + // FNA3D_SetRenderTargets + // + + public static void FNA3D_SetRenderTargets(IntPtr device, + IntPtr renderTargets, + int numRenderTargets, + IntPtr depthStencilBuffer, + DepthFormat depthFormat, + byte preserveContents) + { + if (renderTargets != IntPtr.Zero || numRenderTargets != 0) + throw new PlatformNotSupportedException(); + + var renderer = Renderer.Get(device); + renderer.Send( () => + { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); + + var state = (State) renderer.UserData; + state.RenderToTexture = false; + }); + } + + // + // FNA3D_ResolveTarget + // + + public static void FNA3D_ResolveTarget(IntPtr device, + ref FNA3D_RenderTargetBinding renderTarget) + { + if (renderTarget.multiSampleCount > 0) + { + // no support for multisampling; see FNA3D_SetRenderTargets + throw new PlatformNotSupportedException(); + } + + if (renderTarget.levelCount > 1) + { + int attachmentType = GLES20.GL_TEXTURE_2D; + if (renderTarget.type != /* FNA3D_RENDERTARGET_TYPE_2D */ 0) + { + attachmentType = GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_X + + renderTarget.data2; + } + int texture = (int) renderTarget.texture; + + var renderer = Renderer.Get(device); + renderer.Send( () => + { + int textureUnit = GLES20.GL_TEXTURE0 + renderer.TextureUnits - 1; + GLES20.glActiveTexture(textureUnit); + + GLES20.glBindTexture(attachmentType, texture); + GLES20.glGenerateMipmap(attachmentType); + GLES20.glBindTexture(attachmentType, 0); + }); + } + } + + // + // FNA3D_GenDepthStencilRenderbuffer + // + + public static IntPtr FNA3D_GenDepthStencilRenderbuffer(IntPtr device, + int width, int height, + DepthFormat format, + int multiSampleCount) + { + if (multiSampleCount > 0) + { + // no support for multisampling; see FNA3D_SetRenderTargets + throw new PlatformNotSupportedException(); + } + + int bufferId = 0; + + var renderer = Renderer.Get(device); + renderer.Send( () => + { + var state = (State) renderer.UserData; + + var id = new int[1]; + GLES20.glGenRenderbuffers(1, id, 0); + if (id[0] != 0) + { + GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, id[0]); + GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, + DepthFormatToDepthStorage[(int) format], + width, height); + + bufferId = id[0]; + } + }); + + return (IntPtr) bufferId; + } + + // + // Delete Render Target + // + + public static void FNA3D_AddDisposeRenderbuffer(IntPtr device, IntPtr renderbuffer) + { + var renderer = Renderer.Get(device); + renderer.Send( () => + { + GLES20.glDeleteRenderbuffers(1, new int[] { (int) renderbuffer }, 0); + }); + } + + // + // IsRenderToTexture + // + + public static bool IsRenderToTexture(GraphicsDevice graphicsDevice) + { + // should be called in the renderer thread context + return ((State) Renderer.Get(graphicsDevice.GLDevice).UserData).RenderToTexture; + } + + // + // DepthFormatToDepthStorage + // + + static int[] DepthFormatToDepthStorage = new int[] + { + GLES20.GL_ZERO, // invalid + GLES20.GL_DEPTH_COMPONENT16, // DepthFormat.Depth16 + GLES30.GL_DEPTH_COMPONENT24, // DepthFormat.Depth24 + GLES30.GL_DEPTH24_STENCIL8, // DepthFormat.Depth24Stencil8 + }; + + // + // FNA3D_RenderTargetBinding + // + + public struct FNA3D_RenderTargetBinding + { + public byte type; // 0 for RenderTarget2D, 1 for RenderTargetCube + public int data1; // 2D width or cube size + public int data2; // 2D height or cube face + public int levelCount; + public int multiSampleCount; + public IntPtr texture; + public IntPtr colorBuffer; + } + + // + // State + // + + private partial class State + { + public bool RenderToTexture; + public int TargetFramebuffer; + public int ActiveAttachments; + } + + } +} + diff --git a/BNA/src/FNA3D_Tex.cs b/BNA/src/FNA3D_Tex.cs new file mode 100644 index 0000000..4db1ca1 --- /dev/null +++ b/BNA/src/FNA3D_Tex.cs @@ -0,0 +1,577 @@ + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using android.opengl; +#pragma warning disable 0436 + +namespace Microsoft.Xna.Framework.Graphics +{ + + public static partial class FNA3D + { + + // + // FNA3D_SupportsDXT1 + // + + public static byte FNA3D_SupportsDXT1(IntPtr device) + { + var fmts = Renderer.Get(device).TextureFormats; + bool f = -1 != Array.IndexOf(fmts, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT); + return (byte) (f ? 1 : 0); + } + + // + // FNA3D_SupportsS3TC + // + + public static byte FNA3D_SupportsS3TC(IntPtr device) + { + var fmts = Renderer.Get(device).TextureFormats; + bool f = -1 != Array.IndexOf(fmts, GL_COMPRESSED_RGBA_S3TC_DXT3_EXT) + && -1 != Array.IndexOf(fmts, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT); + return (byte) (f ? 1 : 0); + } + + // + // Create Textures + // + + private static int CreateTexture(Renderer renderer, int textureKind, + SurfaceFormat format, int levelCount) + { + int[] id = new int[1]; + GLES20.glGenTextures(1, id, 0); + if (id[0] != 0) + { + GLES20.glBindTexture(textureKind, id[0]); + + var state = (State) renderer.UserData; + state.TextureConfigs[id[0]] = + new int[] { textureKind, (int) format, levelCount }; + + } + return id[0]; + } + + public static IntPtr FNA3D_CreateTexture2D(IntPtr device, SurfaceFormat format, + int width, int height, int levelCount, + byte isRenderTarget) + { + var renderer = Renderer.Get(device); + if ( width <= 0 || width > renderer.TextureSize + || height <= 0 || height > renderer.TextureSize) + { + throw new ArgumentException($"bad texture size {width} x {height}"); + } + + int textureFormat = SurfaceFormatToTextureFormat[(int) format]; + int internalFormat = SurfaceFormatToTextureInternalFormat[(int) format]; + int dataType, dataSize; + + if (textureFormat == GLES20.GL_COMPRESSED_TEXTURE_FORMATS) + { + dataType = textureFormat; + dataSize = SurfaceFormatToTextureDataSize[(int) format]; + } + else + { + dataType = SurfaceFormatToTextureDataType[(int) format]; + dataSize = 0; + } + + if ( textureFormat == GLES20.GL_ZERO + || internalFormat == GLES20.GL_ZERO + || dataType == GLES20.GL_ZERO) + { + throw new PlatformNotSupportedException( + $"unsupported texture format {format}"); + } + + int textureId = 0; + renderer.Send( () => + { + int id = CreateTexture(renderer, GLES20.GL_TEXTURE_2D, format, levelCount); + if (id != 0) + { + textureId = id; + + for (int level = 0; level < levelCount; level++) + { + if (textureFormat == GLES20.GL_COMPRESSED_TEXTURE_FORMATS) + { + int levelSize = dataSize * + ((width + 3) / 4) * ((height + 3) / 4); + + GLES20.glCompressedTexImage2D(GLES20.GL_TEXTURE_2D, + level, internalFormat, + width, height, /* border */ 0, + levelSize, null); + } + else + { + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, + level, internalFormat, + width, height, /* border */ 0, + textureFormat, dataType, null); + } + + width >>= 1; + height >>= 1; + if (width <= 0) + width = 1; + if (height <= 0) + height = 1; + } + } + }); + return (IntPtr) textureId; + } + + + + // + // Delete Textures + // + + public static void FNA3D_AddDisposeTexture(IntPtr device, IntPtr texture) + { + var renderer = Renderer.Get(device); + renderer.Send( () => + { + GLES20.glDeleteTextures(1, new int[] { (int) texture }, 0); + + var state = (State) renderer.UserData; + state.TextureConfigs.Remove((int) texture); + }); + } + + + + // + // Set Texture Data + // + + private static void SetTextureData(Renderer renderer, int textureId, + int x, int y, int w, int h, int level, + object dataObject, int dataOffset, int dataLength) + { + java.nio.Buffer buffer = dataObject switch + { + sbyte[] byteArray => + java.nio.ByteBuffer.wrap(byteArray, dataOffset, dataLength), + + int[] intArray => + java.nio.IntBuffer.wrap(intArray, dataOffset / 4, dataLength / 4), + + _ => throw new ArgumentException(dataObject?.GetType().ToString()), + }; + + renderer.Send( () => + { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); + + var state = (State) renderer.UserData; + var config = state.TextureConfigs[textureId]; + int format = config[1]; + + int textureFormat = SurfaceFormatToTextureFormat[format]; + if (textureFormat == GLES20.GL_COMPRESSED_TEXTURE_FORMATS) + { + int internalFormat = SurfaceFormatToTextureInternalFormat[format]; + + GLES20.glCompressedTexSubImage2D(GLES20.GL_TEXTURE_2D, + level, x, y, w, h, internalFormat, dataLength, buffer); + } + else + { + int dataType = SurfaceFormatToTextureDataType[format]; + int dataSize = SurfaceFormatToTextureDataSize[format]; + + if (dataSize != 4) + GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, dataSize); + + GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, level, x, y, w, h, + textureFormat, dataType, buffer); + + if (dataSize != 4) + GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 4); + + } + }); + } + + public static void FNA3D_SetTextureData2D(IntPtr device, IntPtr texture, + int x, int y, int w, int h, int level, + IntPtr data, int dataLength) + { + // FNA Texture2D uses GCHandle::Alloc and GCHandle::AddrOfPinnedObject. + // we use GCHandle::FromIntPtr to convert that address to an object reference. + // see also: system.runtime.interopservices.GCHandle struct in baselib. + int dataOffset = (int) data; + var dataObject = System.Runtime.InteropServices.GCHandle.FromIntPtr(data).Target; + SetTextureData(Renderer.Get(device), (int) texture, + x, y, w, h, level, dataObject, dataOffset, dataLength); + } + + + + // + // ReadImageStream + // + + public static IntPtr ReadImageStream(System.IO.Stream stream, + out int width, out int height, out int len, + int forceWidth, int forceHeight, bool zoom) + { + var bitmap = LoadBitmap(stream); + + int[] pixels; + if (forceWidth == -1 || forceHeight == -1) + { + pixels = GetPixels(bitmap, out width, out height, out len); + } + else + { + pixels = CropAndScale(bitmap, forceWidth, forceHeight, zoom, + out width, out height, out len); + } + + // keep a strong reference until FNA3D_Image_Free is called + ImagePixels.set(pixels); + + return System.Runtime.InteropServices.GCHandle.Alloc( + pixels, System.Runtime.InteropServices.GCHandleType.Pinned) + .AddrOfPinnedObject(); + + + android.graphics.Bitmap LoadBitmap(System.IO.Stream stream) + { + if (stream is Microsoft.Xna.Framework.TitleContainer.TitleStream titleStream) + { + var bitmap = android.graphics.BitmapFactory + .decodeStream(titleStream.JavaStream); + if ( bitmap == null + || bitmap.getConfig() != android.graphics.Bitmap.Config.ARGB_8888) + { + string reason = (bitmap == null) ? "unspecified error" + : $"unsupported config '{bitmap.getConfig()}'"; + throw new System.BadImageFormatException( + $"Load failed for bitmap image '{titleStream.Name}': {reason}"); + } + + return bitmap; + } + + throw new ArgumentException(stream?.GetType()?.ToString()); + } + + + int[] CropAndScale(android.graphics.Bitmap bitmap, + int newWidth, int newHeight, bool zoom, + out int width, out int height, out int len) + { + int oldWidth = bitmap.getWidth(); + int oldHeight = bitmap.getHeight(); + bool scaleWidth = zoom ? (oldWidth < oldHeight) : (oldWidth > oldHeight); + float scaleFactor = scaleWidth ? ((float) newWidth / (float) oldWidth) + : ((float) newHeight / (float) oldHeight); + if (zoom) + { + int x, y, w, h; + if (scaleWidth) + { + x = 0; + y = (int) (oldHeight / 2 - (newHeight / scaleFactor) / 2); + w = oldWidth; + h = (int) (newHeight / scaleFactor); + } + else + { + x = (int) (oldWidth / 2 - (newWidth / scaleFactor) / 2); + y = 0; + w = (int) (newWidth / scaleFactor); + h = oldHeight; + } + bitmap = android.graphics.Bitmap.createBitmap(bitmap, x, y, w, h); + } + else + { + newWidth = (int) (oldWidth * scaleFactor); + newHeight = (int) (oldHeight * scaleFactor); + } + + return GetPixels(android.graphics.Bitmap.createScaledBitmap( + bitmap, newWidth, newHeight, false), + out width, out height, out len); + } + + + int[] GetPixels(android.graphics.Bitmap bitmap, + out int width, out int height, out int len) + { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + var pixels = new int[w * h]; + bitmap.copyPixelsToBuffer(java.nio.IntBuffer.wrap(pixels)); + width = w; + height = h; + len = w * h * 4; + return pixels; + } + } + + // + // FNA3D_Image_Free + // + + public static void FNA3D_Image_Free(IntPtr mem) + { + // FNA calls this method after uploading the data returned by + // ReadImageStream, so we can safely discard the reference + ImagePixels.set(null); + } + + // + // FNA3D_VerifySampler + // + + public static void FNA3D_VerifySampler(IntPtr device, int index, IntPtr texture, + ref FNA3D_SamplerState sampler) + { + var samplerCopy = sampler; + var renderer = Renderer.Get(device); + + renderer.Send( () => + { + var state = (State) renderer.UserData; + var config = state.TextureConfigs[(int) texture]; + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + index); + GLES20.glBindTexture(config[0], (int) texture); + + GLES20.glTexParameteri(config[0], GLES30.GL_TEXTURE_MAX_LEVEL, + config[2] - 1); + GLES20.glTexParameteri(config[0], GLES30.GL_TEXTURE_BASE_LEVEL, + samplerCopy.maxMipLevel); + + GLES20.glTexParameteri(config[0], GLES20.GL_TEXTURE_WRAP_S, + TextureWrapMode[(int) samplerCopy.addressU]); + GLES20.glTexParameteri(config[0], GLES20.GL_TEXTURE_WRAP_T, + TextureWrapMode[(int) samplerCopy.addressV]); + if (config[0] == GLES30.GL_TEXTURE_3D) + { + GLES20.glTexParameteri(config[0], GLES30.GL_TEXTURE_WRAP_R, + TextureWrapMode[(int) samplerCopy.addressW]); + } + + int magIndex = (int) samplerCopy.filter * 3; + int minIndex = magIndex + (config[2] <= 1 ? 1 : 2); + + GLES20.glTexParameteri(config[0], GLES20.GL_TEXTURE_MAG_FILTER, + TextureFilterMode[magIndex]); + GLES20.glTexParameteri(config[0], GLES20.GL_TEXTURE_MIN_FILTER, + TextureFilterMode[minIndex]); + + }); + } + + + + // + // SurfaceFormatToTextureFormat + // + + static int[] SurfaceFormatToTextureFormat = new int[] + { + GLES20.GL_RGBA, // SurfaceFormat.Color + GLES20.GL_RGB, // SurfaceFormat.Bgr565 + GLES20.GL_ZERO, // was GL_BGRA // SurfaceFormat.Bgra5551 + GLES20.GL_ZERO, // was GL_BGRA // SurfaceFormat.Bgra4444 + GLES20.GL_COMPRESSED_TEXTURE_FORMATS, // SurfaceFormat.Dxt1 + GLES20.GL_COMPRESSED_TEXTURE_FORMATS, // SurfaceFormat.Dxt3 + GLES20.GL_COMPRESSED_TEXTURE_FORMATS, // SurfaceFormat.Dxt5 + GLES30.GL_RG, // SurfaceFormat.NormalizedByte2 + GLES20.GL_RGBA, // SurfaceFormat.NormalizedByte4 + GLES20.GL_RGBA, // SurfaceFormat.Rgba1010102 + GLES30.GL_RG, // SurfaceFormat.Rg32 + GLES20.GL_RGBA, // SurfaceFormat.Rgba64 + GLES20.GL_ALPHA, // SurfaceFormat.Alpha8 + GLES30.GL_RED, // SurfaceFormat.Single + GLES30.GL_RG, // SurfaceFormat.Vector2 + GLES20.GL_RGBA, // SurfaceFormat.Vector4 + GLES30.GL_RED, // SurfaceFormat.HalfSingle + GLES30.GL_RG, // SurfaceFormat.HalfVector2 + GLES20.GL_RGBA, // SurfaceFormat.HalfVector4 + GLES20.GL_RGBA, // SurfaceFormat.HdrBlendable + GLES20.GL_ZERO, // was GL_BGRA // SurfaceFormat.ColorBgraEXT + }; + + // + // SurfaceFormatToTextureInternalFormat + // + + static int[] SurfaceFormatToTextureInternalFormat = new int[] + { + GLES30.GL_RGBA8, // SurfaceFormat.Color + GLES30.GL_RGB8, // SurfaceFormat.Bgr565 + GLES20.GL_RGB5_A1, // SurfaceFormat.Bgra5551 + GLES20.GL_RGBA4, // SurfaceFormat.Bgra4444 + GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, // SurfaceFormat.Dxt1 + GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, // SurfaceFormat.Dxt3 + GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, // SurfaceFormat.Dxt5 + GLES30.GL_RG8, // SurfaceFormat.NormalizedByte2 + GLES30.GL_RGBA8, // SurfaceFormat.NormalizedByte4 + GLES30.GL_RGB10_A2, // was ..._A2_EXT // SurfaceFormat.Rgba1010102 + GLES20.GL_ZERO, // was GL_RG16 // SurfaceFormat.Rg32 + GLES20.GL_ZERO, // was GL_RGBA16, // SurfaceFormat.Rgba64 + GLES20.GL_ALPHA, // SurfaceFormat.Alpha8 + GLES30.GL_R32F, // SurfaceFormat.Single + GLES30.GL_RG32F, // SurfaceFormat.Vector2 + GLES30.GL_RGBA32F, // SurfaceFormat.Vector4 + GLES30.GL_R16F, // SurfaceFormat.HalfSingle + GLES30.GL_RG16F, // SurfaceFormat.HalfVector2 + GLES30.GL_RGBA16F, // SurfaceFormat.HalfVector4 + GLES30.GL_RGBA16F, // SurfaceFormat.HdrBlendable + GLES30.GL_RGBA8, // SurfaceFormat.ColorBgraEXT + }; + + // + // SurfaceFormatToTextureDataType + // + + static int[] SurfaceFormatToTextureDataType = new int[] + { + GLES20.GL_UNSIGNED_BYTE, // SurfaceFormat.Color + GLES20.GL_UNSIGNED_SHORT_5_6_5, // SurfaceFormat.Bgr565 + GLES20.GL_ZERO, // was ..._5_5_5_1_REV // SurfaceFormat.Bgra5551 + GLES20.GL_ZERO, // was ..._4_4_4_4_REV // SurfaceFormat.Bgra4444 + GLES20.GL_ZERO, // not applicable // SurfaceFormat.Dxt1 + GLES20.GL_ZERO, // not applicable // SurfaceFormat.Dxt3 + GLES20.GL_ZERO, // not applicable // SurfaceFormat.Dxt5 + GLES20.GL_BYTE, // SurfaceFormat.NormalizedByte2 + GLES20.GL_BYTE, // SurfaceFormat.NormalizedByte4 + GLES30.GL_UNSIGNED_INT_2_10_10_10_REV, // SurfaceFormat.Rgba1010102 + GLES20.GL_UNSIGNED_SHORT, // SurfaceFormat.Rg32 + GLES20.GL_UNSIGNED_SHORT, // SurfaceFormat.Rgba64 + GLES20.GL_UNSIGNED_BYTE, // SurfaceFormat.Alpha8 + GLES20.GL_FLOAT, // SurfaceFormat.Single + GLES20.GL_FLOAT, // SurfaceFormat.Vector2 + GLES20.GL_FLOAT, // SurfaceFormat.Vector4 + GLES30.GL_HALF_FLOAT, // SurfaceFormat.HalfSingle + GLES30.GL_HALF_FLOAT, // SurfaceFormat.HalfVector2 + GLES30.GL_HALF_FLOAT, // SurfaceFormat.HalfVector4 + GLES30.GL_HALF_FLOAT, // SurfaceFormat.HdrBlendable + GLES20.GL_UNSIGNED_BYTE // SurfaceFormat.ColorBgraEXT + }; + + // from ubiquitous extension EXT_texture_compression_s3tc + const int GL_COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1; + const int GL_COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2; + const int GL_COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3; + + // + // SurfaceFormatToTextureDataSize + // + + static int[] SurfaceFormatToTextureDataSize = new int[] + { + 4, // SurfaceFormat.Color + 2, // SurfaceFormat.Bgr565 + 2, // SurfaceFormat.Bgra5551 + 2, // SurfaceFormat.Bgra4444 + 8, // SurfaceFormat.Dxt1 + 16, // SurfaceFormat.Dxt3 + 16, // SurfaceFormat.Dxt5 + 2, // SurfaceFormat.NormalizedByte2 + 4, // SurfaceFormat.NormalizedByte4 + 4, // SurfaceFormat.Rgba1010102 + 4, // SurfaceFormat.Rg32 + 8, // SurfaceFormat.Rgba64 + 1, // SurfaceFormat.Alpha8 + 4, // SurfaceFormat.Single + 8, // SurfaceFormat.Vector2 + 16, // SurfaceFormat.Vector4 + 2, // SurfaceFormat.HalfSingle + 4, // SurfaceFormat.HalfVector2 + 8, // SurfaceFormat.HalfVector4 + 8, // SurfaceFormat.HdrBlendable + 4, // SurfaceFormat.ColorBgraEXT + }; + + // + // TextureWrapMode + // + + static int[] TextureWrapMode = new int[] + { + GLES20.GL_REPEAT, // TextureAddressMode.Wrap + GLES20.GL_CLAMP_TO_EDGE, // TextureAddressMode.Clamp + GLES20.GL_MIRRORED_REPEAT // TextureAddressMode.Mirror + }; + + // + // TextureFilterMode + // + + static int[] TextureFilterMode = new int[] + { + // TextureFilter.Linear: mag filter, min filter, mipmap filter + GLES20.GL_LINEAR, GLES20.GL_LINEAR, GLES20.GL_LINEAR_MIPMAP_LINEAR, + // TextureFilter.Point + GLES20.GL_NEAREST, GLES20.GL_NEAREST, GLES20.GL_NEAREST_MIPMAP_NEAREST, + // TextureFilter.Anisotropic + GLES20.GL_LINEAR, GLES20.GL_LINEAR, GLES20.GL_LINEAR_MIPMAP_LINEAR, + // TextureFilter.LinearMipPoint + GLES20.GL_LINEAR, GLES20.GL_LINEAR, GLES20.GL_LINEAR_MIPMAP_NEAREST, + // TextureFilter.PointMipLinear + GLES20.GL_NEAREST, GLES20.GL_NEAREST, GLES20.GL_NEAREST_MIPMAP_LINEAR, + // TextureFilter.MinLinearMagPointMipLinear + GLES20.GL_NEAREST, GLES20.GL_LINEAR, GLES20.GL_LINEAR_MIPMAP_LINEAR, + // TextureFilter.MinLinearMagPointMipPoint + GLES20.GL_NEAREST, GLES20.GL_LINEAR, GLES20.GL_LINEAR_MIPMAP_NEAREST, + // TextureFilter.MinPointMagLinearMipLinear + GLES20.GL_LINEAR, GLES20.GL_NEAREST, GLES20.GL_NEAREST_MIPMAP_LINEAR, + // TextureFilter.MinPointMagLinearMipPoint + GLES20.GL_LINEAR, GLES20.GL_NEAREST, GLES20.GL_NEAREST_MIPMAP_NEAREST, + }; + + // + // FNA3D_SamplerState + // + + public struct FNA3D_SamplerState + { + public TextureFilter filter; + public TextureAddressMode addressU; + public TextureAddressMode addressV; + public TextureAddressMode addressW; + public float mipMapLevelOfDetailBias; + public int maxAnisotropy; + public int maxMipLevel; + } + + + + // + // data + // + + private static readonly java.lang.ThreadLocal ImagePixels = new java.lang.ThreadLocal(); + + // + // state + // + + private partial class State + { + // texture config array: + // #0 - target type + // #1 - SurfaceFormat + // #2 - levels count + public Dictionary TextureConfigs = new Dictionary(); + } + + } +} diff --git a/BNA/src/FNAPlatform.cs b/BNA/src/FNAPlatform.cs new file mode 100644 index 0000000..6abd671 --- /dev/null +++ b/BNA/src/FNAPlatform.cs @@ -0,0 +1,172 @@ + +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using Microsoft.Xna.Framework.Input.Touch; +#pragma warning disable 0436 + +namespace Microsoft.Xna.Framework +{ + + public static class FNAPlatform + { + // + // CreateWindow + // + + private static GameWindow CreateWindowImpl() => GameRunner.Singleton; + + public delegate GameWindow CreateWindowFunc(); + public static readonly CreateWindowFunc CreateWindow = CreateWindowImpl; + + // + // DisposeWindow + // + + private static void DisposeWindowImpl(GameWindow window) { } + + public delegate void DisposeWindowFunc(GameWindow window); + public static readonly DisposeWindowFunc DisposeWindow = DisposeWindowImpl; + + // + // SupportsOrientationChanges + // + + private static bool SupportsOrientationChangesImpl() => true; + + public delegate bool SupportsOrientationChangesFunc(); + public static readonly SupportsOrientationChangesFunc SupportsOrientationChanges = SupportsOrientationChangesImpl; + + // + // GetGraphicsAdapters + // + + private static GraphicsAdapter[] GetGraphicsAdaptersImpl() + { + var bounds = GameRunner.Singleton.ClientBounds; + var modesList = new List(); + var theMode = new DisplayMode(bounds.Width, bounds.Height, SurfaceFormat.Color); + modesList.Add(theMode); + var modesCollection = new DisplayModeCollection(modesList); + var name = "Android Surface"; + var theAdapter = new GraphicsAdapter(modesCollection, name, name); + return new GraphicsAdapter[] { (GraphicsAdapter) (object) theAdapter }; + } + + public delegate GraphicsAdapter[] GetGraphicsAdaptersFunc(); + public static readonly GetGraphicsAdaptersFunc GetGraphicsAdapters = GetGraphicsAdaptersImpl; + + // + // RegisterGame + // + + public static GraphicsAdapter RegisterGameImpl(Game game) => GraphicsAdapter.Adapters[0]; + + public delegate GraphicsAdapter RegisterGameFunc(Game game); + public static readonly RegisterGameFunc RegisterGame = RegisterGameImpl; + + // + // GetTouchCapabilities + // + + public static TouchPanelCapabilities GetTouchCapabilitiesImpl() + => (TouchPanelCapabilities) (object) new TouchPanelCapabilities(true, 4); + + public delegate TouchPanelCapabilities GetTouchCapabilitiesFunc(); + public static readonly GetTouchCapabilitiesFunc GetTouchCapabilities = GetTouchCapabilitiesImpl; + + // + // NeedsPlatformMainLoop + // + + public static bool NeedsPlatformMainLoopImpl() => true; + + public delegate bool NeedsPlatformMainLoopFunc(); + public static readonly NeedsPlatformMainLoopFunc NeedsPlatformMainLoop = NeedsPlatformMainLoopImpl; + + // + // + // + + public static void RunPlatformMainLoopImpl(Game game) + => GameRunner.Singleton.MainLoop(game); + + public delegate void RunPlatformMainLoopFunc(Game game); + public static readonly RunPlatformMainLoopFunc RunPlatformMainLoop = RunPlatformMainLoopImpl; + + // + // UnregisterGame + // + + public static void UnregisterGameImpl(Game game) { } + + public delegate void UnregisterGameFunc(Game game); + public static readonly UnregisterGameFunc UnregisterGame = UnregisterGameImpl; + + // + // OnIsMouseVisibleChanged + // + + public static void OnIsMouseVisibleChangedImpl(bool visible) { } + + public delegate void OnIsMouseVisibleChangedFunc(bool visible); + public static readonly OnIsMouseVisibleChangedFunc OnIsMouseVisibleChanged = OnIsMouseVisibleChangedImpl; + + // + // GetNumTouchFingers + // + + public static int GetNumTouchFingersImpl() => Mouse.NumTouchFingers.get(); + + public delegate int GetNumTouchFingersFunc(); + public static readonly GetNumTouchFingersFunc GetNumTouchFingers = GetNumTouchFingersImpl; + + // + // GetStorageRoot + // + + public static string GetStorageRootImpl() + { + var s = GameRunner.Singleton.Activity.getFilesDir(); + return s.getAbsolutePath(); + } + + public delegate string GetStorageRootFunc(); + public static readonly GetStorageRootFunc GetStorageRoot = GetStorageRootImpl; + + // + // GetDriveInfo + // + + public static System.IO.DriveInfo GetDriveInfoImpl(string storageRoot) => null; + + public delegate System.IO.DriveInfo GetDriveInfoFunc(string storageRoot); + public static readonly GetDriveInfoFunc GetDriveInfo = GetDriveInfoImpl; + + // + // GetMouseState + // + + /*public static void GetMouseStateImpl(IntPtr window, out int x, out int y, out ButtonState left, out ButtonState middle, out ButtonState right, out ButtonState x1, out ButtonState x2) + { + x = 0; + y = 0; + left = ButtonState.Released; + middle = ButtonState.Released; + right = ButtonState.Released; + x1 = ButtonState.Released; + x2 = ButtonState.Released; + } + + public delegate void GetMouseStateFunc(IntPtr window, out int x, out int y, out ButtonState left, out ButtonState middle, out ButtonState right, out ButtonState x1, out ButtonState x2); + public static readonly GetMouseStateFunc GetMouseState = GetMouseStateImpl;*/ + + // + // TextInputCharacters + // + + public static readonly char[] TextInputCharacters = new char[0]; + } + +} diff --git a/BNA/src/GameRunner.cs b/BNA/src/GameRunner.cs new file mode 100644 index 0000000..5a78b48 --- /dev/null +++ b/BNA/src/GameRunner.cs @@ -0,0 +1,413 @@ + +using System; +using Microsoft.Xna.Framework.Graphics; +using GL10 = javax.microedition.khronos.opengles.GL10; +using EGLConfig = javax.microedition.khronos.egl.EGLConfig; +#pragma warning disable 0436 + +namespace Microsoft.Xna.Framework +{ + + public class GameRunner : GameWindow, IServiceProvider, java.lang.Runnable + + { + + 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; + private java.util.concurrent.atomic.AtomicInteger shouldPause; + private java.util.concurrent.atomic.AtomicInteger shouldResume; + private java.util.concurrent.atomic.AtomicInteger shouldExit; + private java.util.concurrent.atomic.AtomicInteger shouldEvents; + private android.os.ConditionVariable waitForPause; + private android.os.ConditionVariable waitForResume; + + private static readonly java.lang.ThreadLocal selfTls = + new java.lang.ThreadLocal(); + + private const int CONFIG_EVENT = 1; + private const int TOUCH_EVENT = 2; + + // + // constructor + // + + public GameRunner(Activity activity) + { + this.activity = activity; + + UpdateConfiguration(false); + + inModal = new java.util.concurrent.atomic.AtomicInteger(); + shouldPause = new java.util.concurrent.atomic.AtomicInteger(); + shouldResume = new java.util.concurrent.atomic.AtomicInteger(); + shouldExit = new java.util.concurrent.atomic.AtomicInteger(); + shouldEvents = new java.util.concurrent.atomic.AtomicInteger(); + waitForPause = new android.os.ConditionVariable(); + waitForResume = new android.os.ConditionVariable(); + } + + // + // Singleton and Activity properties + // + + public static GameRunner Singleton + => (GameRunner) selfTls.get() + ?? throw new System.InvalidOperationException("not main thread"); + + public android.app.Activity Activity => activity; + + // + // InModal + // + + public bool InModal + { + get => inModal.get() != 0 ? true : false; + set => inModal.set(value ? 1 : 0); + } + + // + // Thread run() method + // + + [java.attr.RetainName] + public void run() + { + selfTls.set(this); + + RunMainMethod(); + + shouldExit.set(1); + waitForPause.open(); + + activity.FinishAndRestart(recreateActivity); + } + + // + // RunMainMethod + // + + private void RunMainMethod() + { + try + { + CallMainMethod(GetMainClass()); + } + catch (Exception e) + { + GameRunner.Log("========================================"); + GameRunner.Log(e.ToString()); + GameRunner.Log("========================================"); + System.Windows.Forms.MessageBox.Show(e.ToString()); + } + + + Type GetMainClass() + { + 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); + } + + if (clsType == null) + { + throw new Exception($"main class '{clsName}' not found"); + } + + return clsType; + } + + + void CallMainMethod(Type mainClass) + { + var method = mainClass.GetMethod("Main"); + if (method.IsStatic) + { + method.Invoke(null, new object[method.GetParameters().Length]); + } + else + { + throw new Exception($"missing or invalid method 'Main' in type '{mainClass}'"); + } + } + } + + // + // MainLoop + // + + public void MainLoop(Game game) + { + int pauseCount = 0; + + while (game.RunApplication) + { + + // + // pause game if required + // + + if (shouldPause.get() != pauseCount) + { + pauseCount = shouldPause.incrementAndGet(); + + // FNA.Game calls game.OnDeactivated() + game.IsActive = false; + + if (shouldExit.get() != 0) + break; + + PauseGame(false); + + shouldResume.incrementAndGet(); + waitForPause.open(); + waitForResume.block(); + waitForResume.close(); + + if (! ResumeGame(false)) + break; + + // on resume from pause, reset input state + Microsoft.Xna.Framework.Input.Mouse.WindowHandle = + Microsoft.Xna.Framework.Input.Mouse.WindowHandle; + + // FNA.Game calls game.OnActivated() + game.IsActive = true; + } + + // + // handle various events as indicated + // + + int eventBits = shouldEvents.get(); + if (eventBits != 0) + { + while (! shouldEvents.compareAndSet(eventBits, 0)) + eventBits = shouldEvents.get(); + + if ((eventBits & CONFIG_EVENT) != 0) + UpdateConfiguration(true); + + if ((eventBits & TOUCH_EVENT) != 0) + { + Microsoft.Xna.Framework.Input.Mouse + .HandleEvents(clientWidth, clientHeight); + } + } + + // + // run one game frame + // + + game.Tick(); + } + + InModal = true; + game.RunApplication = false; + } + + // + // PauseGame + // + + public void PauseGame(bool enterModal) + { + Renderer.Pause(activity); + if (enterModal) + InModal = true; + } + + // + // ResumeGame + // + + public bool ResumeGame(bool leaveModal) + { + if (leaveModal) + InModal = false; + + if (shouldExit.get() != 0) + return false; + + if (! Renderer.CanResume(activity)) + { + // restart because we lost the GL context and state + recreateActivity = true; + + // in case we are called from MessageBox, make sure + // the main loop sees that we need to exit. + shouldExit.set(1); + shouldPause.incrementAndGet(); + + return false; + } + + return true; + } + + // + // Callbacks from Android activity UI thread: + // onPause, onResume, onDestroy, onTouchEvent + // + + public void ActivityPause() + { + if (! InModal) + { + shouldPause.incrementAndGet(); + waitForPause.block(); + if (shouldExit.get() == 0) + waitForPause.close(); + } + } + + // + // ActivityResume + // + + public void ActivityResume() + { + if (shouldResume.compareAndSet(1, 0)) + waitForResume.open(); + } + + // + // ActivityDestroy + // + + public void ActivityDestroy() + { + shouldExit.set(1); + ActivityResume(); + ActivityPause(); + } + + // + // ActivityTouch + // + + public void ActivityTouch(android.view.MotionEvent motionEvent) + { + Microsoft.Xna.Framework.Input.Mouse.QueueEvent(motionEvent); + for (;;) + { + int v = shouldEvents.get(); + if (shouldEvents.compareAndSet(v, v | TOUCH_EVENT)) + break; + } + } + + // + // OnSurfaceChanged + // + + public void OnSurfaceChanged() + { + for (;;) + { + int v = shouldEvents.get(); + if (shouldEvents.compareAndSet(v, v | CONFIG_EVENT)) + break; + } + } + + // + // UpdateConfiguration + // + + void UpdateConfiguration(bool withCallback) + { + var metrics = new android.util.DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getRealMetrics(metrics); + + clientWidth = metrics.widthPixels; + clientHeight = metrics.heightPixels; + clientBounds = new Rectangle(0, 0, clientWidth, clientHeight); + + if (dict == null) + dict = new System.Collections.Hashtable(); + + // int dpi - pixels per inch + dict["dpi"] = (int) ((metrics.xdpi + metrics.ydpi) * 0.5f); + + if (withCallback) + { + OnClientSizeChanged(); + OnOrientationChanged(); + } + } + + // + // GetService + // + + public object GetService(Type type) + { + if (object.ReferenceEquals(type, typeof(System.Collections.IDictionary))) + return dict.Clone(); + return null; + } + + // + // GameWindow interface + // + + public override Rectangle ClientBounds => clientBounds; + + public override string ScreenDeviceName => "Android"; + + public override bool AllowUserResizing { get => false; set { } } + + public override void SetSupportedOrientations(DisplayOrientation orientations) + => CurrentOrientation = orientations; + + public override DisplayOrientation CurrentOrientation + { + get => (clientWidth < clientHeight) ? DisplayOrientation.Portrait + : DisplayOrientation.LandscapeLeft; + set + { + bool portrait = 0 != (value & DisplayOrientation.Portrait); + bool landscape = 0 != (value & ( DisplayOrientation.LandscapeLeft + | DisplayOrientation.LandscapeRight)); + int r; + if (portrait && (! landscape)) + r = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT; + else if (landscape && (! portrait)) + r = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE; + else + r = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER; + activity.setRequestedOrientation(r); + } + } + + public static void Log(string s) => Microsoft.Xna.Framework.Activity.Log(s); + + // + // not implemented + // + + public override IntPtr Handle => IntPtr.Zero; + + public override void SetTitle(string title) { } + + public override void BeginScreenDeviceChange(bool willBeFullScreen) { } + + public override void EndScreenDeviceChange(string screenDeviceName, + int clientWidth, int clientHeight) { } + + + } + +} diff --git a/BNA/src/Import.cs b/BNA/src/Import.cs new file mode 100644 index 0000000..64aac7e --- /dev/null +++ b/BNA/src/Import.cs @@ -0,0 +1,228 @@ + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Microsoft.Xna.Framework.Graphics; +#pragma warning disable 0436 + +namespace Microsoft.Xna.Framework +{ + + // + // GameWindow + // + + [java.attr.Discard] // discard in output + public abstract class GameWindow + { + public abstract IntPtr Handle { get; } + public abstract bool AllowUserResizing { get; set; } + public abstract Rectangle ClientBounds { get; } + public abstract string ScreenDeviceName { get; } + public abstract void SetSupportedOrientations(DisplayOrientation orientations); + public abstract DisplayOrientation CurrentOrientation { get; set; } + public abstract void BeginScreenDeviceChange(bool willBeFullScreen); + public abstract void EndScreenDeviceChange(string screenDeviceName, int clientWidth, int clientHeight); + public abstract void SetTitle(string title); + protected void OnActivated() { } + protected void OnDeactivated() { } + protected void OnPaint() { } + protected void OnScreenDeviceNameChanged() { } + protected void OnClientSizeChanged() { } + protected void OnOrientationChanged() { } + public static readonly int DefaultClientWidth = 800; + public static readonly int DefaultClientHeight = 600; + } + + // + // Game + // + + [java.attr.Discard] // discard in output + public abstract class Game + { + public bool IsActive { get; set; } + public bool RunApplication; + public abstract void Tick(); + } +} + + + +namespace Microsoft.Xna.Framework.Graphics +{ + + // + // GraphicsDevice + // + + [java.attr.Discard] // discard in output + public class GraphicsDevice + { + public readonly IntPtr GLDevice; + public TextureCollection Textures { get; } + } + + // + // DisplayMode + // + + [java.attr.Discard] // discard in output + public class DisplayMode + { + public DisplayMode(int width, int height, SurfaceFormat format) { } + } + + // + // DisplayModeCollection + // + + [java.attr.Discard] // discard in output + public class DisplayModeCollection + { + public DisplayModeCollection(List setmodes) { } + } + + // + // GraphicsAdapter + // + + [java.attr.Discard] // discard in output + public sealed class GraphicsAdapter + { + public GraphicsAdapter(DisplayModeCollection modes, string name, string description) { } + public static ReadOnlyCollection Adapters => null; + } + + // + // GraphicsResource + // + + [java.attr.Discard] // discard in output + public abstract class GraphicsResource + { + public GraphicsDevice GraphicsDevice { get; set; } + protected virtual void Dispose(bool disposing) { } + public virtual bool IsDisposed => false; + } + + // + // SpriteBatch + // + + [java.attr.Discard] // discard in output + public class SpriteBatch + { + public struct VertexPositionColorTexture4 + { + public Vector3 Position0; + public Color Color0; + public Vector2 TextureCoordinate0; + + public Vector3 Position1; + public Color Color1; + public Vector2 TextureCoordinate1; + + public Vector3 Position2; + public Color Color2; + public Vector2 TextureCoordinate2; + + public Vector3 Position3; + public Color Color3; + public Vector2 TextureCoordinate3; + } + } + + // + // EffectParameterCollection + // + + [java.attr.Discard] // discard in output + public class EffectParameterCollection + { + public EffectParameterCollection(List value) { } + public EffectParameter this[int index] => null; + public int Count { get; } + } + + // + // EffectPass + // + + [java.attr.Discard] // discard in output + public sealed class EffectPass + { + public EffectPass(string name, EffectAnnotationCollection annotations, + Effect parent, IntPtr technique, uint passIndex) { } + } + + // + // EffectPassCollection + // + + [java.attr.Discard] // discard in output + public class EffectPassCollection + { + public EffectPassCollection(List value) { } + } + + // + // EffectTechnique + // + + [java.attr.Discard] // discard in output + public sealed class EffectTechnique + { + public EffectTechnique(string name, IntPtr pointer, EffectPassCollection passes, + EffectAnnotationCollection annotations) { } + public string Name { get; } + } + + // + // EffectTechniqueCollection + // + + [java.attr.Discard] // discard in output + public class EffectTechniqueCollection + { + public EffectTechniqueCollection(List value) { } + } + +} + + + +namespace Microsoft.Xna.Framework.Input +{ + [java.attr.Discard] // discard in output + public struct MouseState + { + public int X { get; set; } + public int Y { get; set; } + public ButtonState LeftButton { get; set; } + } +} + + + +namespace Microsoft.Xna.Framework.Input.Touch +{ + + // + // TouchPanelCapabilities + // + + [java.attr.Discard] // discard in output + public struct TouchPanelCapabilities + { + public TouchPanelCapabilities(bool isConnected, int maximumTouchCount) { } + } + + [java.attr.Discard] // discard in output + public class TouchPanel + { + public static void INTERNAL_onTouchEvent(int fingerId, TouchLocationState state, + float x, float y, float dx, float dy) { } + } + +} diff --git a/BNA/src/MessageBox.cs b/BNA/src/MessageBox.cs new file mode 100644 index 0000000..fe167ac --- /dev/null +++ b/BNA/src/MessageBox.cs @@ -0,0 +1,73 @@ + +using System; +using Microsoft.Xna.Framework; + +namespace System.Windows.Forms +{ + + public enum DialogResult + { + None, OK, Cancel, Abort, Retry, Ignore, Yes, No + } + + public class MessageBox + { + + public static volatile bool Showing; + public static volatile bool Disable; + + public static DialogResult Show(string text) + { + if (! Disable) + { + var gameRunner = GameRunner.Singleton; + var activity = gameRunner.Activity; + if ( gameRunner.InModal || activity == null + || android.os.Looper.getMainLooper().getThread() + == java.lang.Thread.currentThread()) + { + GameRunner.Log(text); + } + else + { + gameRunner.PauseGame(true); + + var waitObj = new android.os.ConditionVariable(); + Show(activity, text, (_) => waitObj.open()); + waitObj.block(); + + gameRunner.ResumeGame(true); + + } + } + return DialogResult.OK; + } + + static void Show(android.app.Activity activity, string text, + System.Action onClick) + { + activity.runOnUiThread(((java.lang.Runnable.Delegate) ( () => { + + var dlg = new android.app.AlertDialog.Builder(activity); + dlg.setPositiveButton((java.lang.CharSequence) (object) "Close", + ((android.content.DialogInterface.OnClickListener.Delegate) + ((dialog, which) => + { Showing = false; onClick(DialogResult.Yes); } + )).AsInterface()); + dlg.setOnDismissListener( + ((android.content.DialogInterface.OnDismissListener.Delegate) + ((dialog) => + { Showing = false; onClick(DialogResult.Cancel); } + )).AsInterface()); + dlg.create(); + dlg.setMessage((java.lang.CharSequence) (object) text); + + Showing = true; + dlg.show(); + + })).AsInterface()); + } + + } + +} diff --git a/BNA/src/Mouse.cs b/BNA/src/Mouse.cs new file mode 100644 index 0000000..6537e6b --- /dev/null +++ b/BNA/src/Mouse.cs @@ -0,0 +1,188 @@ + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Input.Touch; +#pragma warning disable 0436 + +namespace Microsoft.Xna.Framework.Input +{ + + public static class Mouse + { + + private static MouseState state; + + private static Queue motionEvents = new Queue(); + + private static List<(int id, float x, float y)> fingerXYs = + new List<(int id, float x, float y)>(); + + public static java.util.concurrent.atomic.AtomicInteger NumTouchFingers = + new java.util.concurrent.atomic.AtomicInteger(); + + + + public static IntPtr WindowHandle + { + get => IntPtr.Zero; + set + { + state = default(MouseState); + lock (motionEvents) + { + motionEvents.Clear(); + } + fingerXYs.Clear(); + NumTouchFingers.set(0); + } + } + public static int INTERNAL_BackBufferWidth; + public static int INTERNAL_BackBufferHeight; + + + + public static void QueueEvent(android.view.MotionEvent motionEvent) + { + lock (motionEvents) + { + motionEvents.Enqueue( + android.view.MotionEvent.obtainNoHistory(motionEvent)); + } + } + + public static void HandleEvents(int clientWidth, int clientHeight) + { + for (;;) + { + android.view.MotionEvent motionEvent; + lock (motionEvents) + { + if (motionEvents.Count == 0) + motionEvent = null; + else + { + motionEvent = + (android.view.MotionEvent) motionEvents.Dequeue(); + } + } + + if (motionEvent == null) + return; + + HandleOneEvent(motionEvent, clientWidth, clientHeight); + } + } + + + + private static void HandleOneEvent(android.view.MotionEvent motionEvent, + int clientWidth, int clientHeight) + { + int action = motionEvent.getActionMasked(); + + if ( action == android.view.MotionEvent.ACTION_DOWN + || action == android.view.MotionEvent.ACTION_MOVE + || action == android.view.MotionEvent.ACTION_POINTER_DOWN) + { + state.LeftButton = ButtonState.Pressed; + state.X = (int) Clamp(motionEvent.getX(), + clientWidth, INTERNAL_BackBufferWidth); + state.Y = (int) Clamp(motionEvent.getY(), + clientHeight, INTERNAL_BackBufferHeight); + + var which = (action == android.view.MotionEvent.ACTION_DOWN) + ? TouchLocationState.Pressed + : TouchLocationState.Moved; + + SendTouchEvents(motionEvent, which, clientWidth, clientHeight); + } + + else if ( action == android.view.MotionEvent.ACTION_UP + || action == android.view.MotionEvent.ACTION_POINTER_UP + || action == android.view.MotionEvent.ACTION_CANCEL) + { + state.LeftButton = ButtonState.Released; + + SendTouchEvents(motionEvent, TouchLocationState.Released, + clientWidth, clientHeight); + } + } + + private static void SendTouchEvents(android.view.MotionEvent motionEvent, + TouchLocationState whichTouchEvent, + int clientWidth, int clientHeight) + { + int pointerCount = motionEvent.getPointerCount(); + var eventArray = + new (int id, float x, float y, float dx, float dy)[pointerCount]; + + for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) + { + int id = motionEvent.getPointerId(pointerIndex); + var x = Clamp(motionEvent.getX(pointerIndex), clientWidth, 1); + var y = Clamp(motionEvent.getY(pointerIndex), clientHeight, 1); + float dx, dy; + + int fingerCount = fingerXYs.Count; + int fingerIndex = 0; + while (fingerIndex < fingerCount) + { + if (fingerXYs[fingerIndex].id == id) + break; + fingerIndex++; + } + + if (fingerIndex == fingerCount) + { + dx = dy = 0f; + if (whichTouchEvent != TouchLocationState.Released) + { + fingerXYs.Add((id: id, x: x, y: y)); + } + } + else + { + dx = x - fingerXYs[fingerIndex].x; + dy = y - fingerXYs[fingerIndex].y; + + if (whichTouchEvent != TouchLocationState.Released) + { + fingerXYs[fingerIndex] = (id: id, x: x, y: y); + } + else + { + fingerXYs.RemoveAt(fingerIndex); + } + } + + eventArray[pointerIndex] = (id: id, x: x, y: y, dx: dx, dy: dy); + } + + NumTouchFingers.set(fingerXYs.Count); + + for (int eventIndex = 0; eventIndex < pointerCount; eventIndex++) + { + var e = eventArray[eventIndex]; + TouchPanel.INTERNAL_onTouchEvent(e.id, whichTouchEvent, e.x, e.y, e.dx, e.dy); + } + } + + private static float Clamp(float v, int clientSize, int backbufferSize) + { + if (v < 0) + v = 0; + if (v > clientSize) + v = clientSize; + return v * backbufferSize / clientSize; + } + + + + public static MouseState GetState() => state; + + public static void SetPosition(int x, int y) { } + + } + +} diff --git a/BNA/src/Renderer.cs b/BNA/src/Renderer.cs new file mode 100644 index 0000000..e0083f3 --- /dev/null +++ b/BNA/src/Renderer.cs @@ -0,0 +1,337 @@ + +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using android.opengl; +using GL10 = javax.microedition.khronos.opengles.GL10; +using EGLConfig = javax.microedition.khronos.egl.EGLConfig; + +namespace Microsoft.Xna.Framework.Graphics +{ + + public class Renderer : android.opengl.GLSurfaceView.Renderer + { + + // + // renderer data + // + + private android.opengl.GLSurfaceView surface; + private android.os.ConditionVariable waitObject; + private java.util.concurrent.atomic.AtomicInteger paused; + private Action actionOnChanged; + + public object UserData; + + // + // surface configuration + // + + public int SurfaceWidth, SurfaceHeight; + public DepthFormat SurfaceDepthFormat; + public int TextureUnits; + public int TextureSize; + public int[] TextureFormats; + + // + // constructor + // + + private Renderer(android.app.Activity activity, Action onChanged, + int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) + { + waitObject = new android.os.ConditionVariable(); + paused = new java.util.concurrent.atomic.AtomicInteger(); + actionOnChanged = onChanged; + + activity.runOnUiThread(((java.lang.Runnable.Delegate) (() => + { + surface = new android.opengl.GLSurfaceView(activity); + surface.setEGLContextClientVersion(3); // OpenGL ES 3.0 + surface.setEGLConfigChooser(redSize, greenSize, blueSize, + alphaSize, depthSize, stencilSize); + surface.setPreserveEGLContextOnPause(true); + surface.setRenderer(this); + surface.setRenderMode(android.opengl.GLSurfaceView.RENDERMODE_WHEN_DIRTY); + activity.setContentView(surface); + + })).AsInterface()); + + // wait for one onDrawFrame callback, which tells us that + // GLSurfaceView finished initializing the GL context + if (! waitObject.block(8000)) + throw new NoSuitableGraphicsDeviceException("cannot create GLSurfaceView"); + + var clientBounds = GameRunner.Singleton.ClientBounds; + if (SurfaceWidth != clientBounds.Width || SurfaceHeight != clientBounds.Height) + { + // while not common, it is possible for the screen to rotate, + // between the time the Window/GameRunner is created, and the + // time the renderer is created. we want to identify this. + if (actionOnChanged != null) + actionOnChanged(); + } + } + + // + // Send + // + + public void Send(Action action) + { + Exception exc = null; + if (paused.get() == 0) + { + var cond = new android.os.ConditionVariable(); + surface.queueEvent(((java.lang.Runnable.Delegate) (() => + { + var error = GLES20.glGetError(); + if (error == GLES20.GL_NO_ERROR) + { + try + { + action(); + } + catch (Exception exc2) + { + exc = exc2; + } + error = GLES20.glGetError(); + } + if (error != GLES20.GL_NO_ERROR) + exc = new Exception($"GL Error {error}"); + cond.open(); + })).AsInterface()); + cond.block(); + } + if (exc != null) + { + throw new AggregateException(exc.Message, exc); + } + } + + // + // Present + // + + public void Present() + { + waitObject.close(); + surface.requestRender(); + waitObject.block(); + } + + // + // Renderer interface + // + + [java.attr.RetainName] + public void onSurfaceCreated(GL10 unused, EGLConfig config) + { + // if onSurfaceCreated is called while resuming from pause, + // it means the GL context was lost + paused.compareAndSet(1, -1); + } + + [java.attr.RetainName] + public void onSurfaceChanged(GL10 unused, int width, int height) + { + bool changed = (SurfaceWidth != width || SurfaceHeight != height) + && (SurfaceWidth != 0 || SurfaceHeight != 0); + + SurfaceWidth = width; + SurfaceHeight = height; + InitConfig(); + + if (changed && actionOnChanged != null) + actionOnChanged(); + } + + [java.attr.RetainName] + public void onDrawFrame(GL10 unused) => waitObject.open(); + + // + // InitConfig + // + + private void InitConfig() + { + var data = new int[5]; + GLES20.glGetIntegerv(GLES20.GL_DEPTH_BITS, data, 0); + GLES20.glGetIntegerv(GLES20.GL_STENCIL_BITS, data, 1); + GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_IMAGE_UNITS, data, 2); + GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, data, 3); + GLES20.glGetIntegerv(GLES20.GL_NUM_COMPRESSED_TEXTURE_FORMATS, data, 4); + + if (data[0] /* DEPTH_BITS */ >= 24) + { + SurfaceDepthFormat = (data[1] /* STENCIL_BITS */ >= 8) + ? DepthFormat.Depth24Stencil8 + : DepthFormat.Depth24; + } + else if (data[0] /* DEPTH_BITS */ >= 16) + SurfaceDepthFormat = DepthFormat.Depth16; + else + SurfaceDepthFormat = DepthFormat.None; + + TextureUnits = data[2]; // GL_MAX_TEXTURE_IMAGE_UNITS + TextureSize = data[3]; // GL_MAX_TEXTURE_SIZE + + TextureFormats = new int[data[4]]; // GL_NUM_COMPRESSED_TEXTURE_FORMATS + GLES20.glGetIntegerv(GLES20.GL_COMPRESSED_TEXTURE_FORMATS, TextureFormats, 0); + } + + // + // Create + // + + public static IntPtr Create(android.app.Activity activity, Action onChanged, + int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) + { + for (;;) + { + lock (RendererObjects) + { + var deviceId = java.lang.System.nanoTime(); + + foreach (var oldRendererObject in RendererObjects) + { + if (oldRendererObject.deviceId == deviceId) + { + deviceId = 0; + break; + } + } + + if (deviceId == 0) + { + java.lang.Thread.sleep(1); + continue; + } + + RendererObjects.Insert(0, new RendererObject() + { + deviceId = deviceId, + renderer = new Renderer(activity, onChanged, + redSize, greenSize, blueSize, + alphaSize, depthSize, stencilSize), + activity = new java.lang.@ref.WeakReference(activity), + }); + + return (IntPtr) deviceId; + } + } + } + + // + // GetRenderer + // + + public static Renderer Get(IntPtr deviceId) + { + var longDeviceId = (long) deviceId; + lock (RendererObjects) + { + foreach (var renderer in RendererObjects) + { + if (renderer.deviceId == longDeviceId) + return renderer.renderer; + } + } + throw new ArgumentException("invalid device ID"); + } + + // + // Release + // + + public void Release() + { + lock (RendererObjects) + { + for (int i = RendererObjects.Count; i-- > 0; ) + { + if (RendererObjects[i].renderer == this) + { + RendererObjects[i].renderer.surface = null; + RendererObjects[i].renderer = null; + RendererObjects.RemoveAt(i); + } + } + } + } + + // + // GetRenderersForActivity + // + + private static List GetRenderersForActivity(android.app.Activity activity) + { + var list = new List(); + lock (RendererObjects) + { + foreach (var renderer in RendererObjects) + { + if (renderer.activity.get() == activity) + list.Add(renderer.renderer); + } + } + return list; + } + + // + // Pause + // + + public static void Pause(android.app.Activity activity) + { + foreach (var renderer in GetRenderersForActivity(activity)) + { + renderer.surface.onPause(); + renderer.paused.set(1); + } + } + + // + // CanResume + // + + public static bool CanResume(android.app.Activity activity) + { + foreach (var renderer in GetRenderersForActivity(activity)) + { + if (renderer.paused.get() != 0) + { + renderer.waitObject.close(); + renderer.surface.onResume(); + renderer.waitObject.block(); + + if (! renderer.paused.compareAndSet(1, 0)) + { + // cannot resume because we lost the GL context, + // see also PauseRenderers and onSurfaceCreated + return false; + } + } + } + return true; + } + + // + // data + // + + private static List RendererObjects = new List(); + + private class RendererObject + { + public long deviceId; + public Renderer renderer; + public java.lang.@ref.WeakReference activity; + } + + } + +} diff --git a/BNA/src/Resources.cs b/BNA/src/Resources.cs new file mode 100644 index 0000000..22902e4 --- /dev/null +++ b/BNA/src/Resources.cs @@ -0,0 +1,188 @@ + +using System; + +namespace Microsoft.Xna.Framework.Graphics +{ + public class Resources + { + + // + // SpriteEffect + // + + public static byte[] SpriteEffect => System.Text.Encoding.ASCII.GetBytes(@" + +#technique SpriteBatch + +--- vertex --- + +layout (location = 0) in vec3 pos; +layout (location = 1) in vec4 color; +layout (location = 2) in vec2 uv; +out vec4 f_color; +out vec2 f_uv; +uniform mat4 MatrixTransform; +uniform float MultiplierY; + +void main() +{ + gl_Position = MatrixTransform * vec4(pos, 1.0); + gl_Position.y *= MultiplierY; + f_color = color; + f_uv = uv; +} + +--- fragment --- + +in vec4 f_color; +in vec2 f_uv; +out vec4 o_color; +uniform sampler2D image; + +void main() +{ + o_color = texture(image, f_uv) * f_color; +} + +--- end --- +"); + + // + // BasicEffect + // + + public static byte[] BasicEffect => System.Text.Encoding.ASCII.GetBytes(@" + +#technique BasicEffect + +--- vertex --- + +layout (location = 0) in vec3 pos; +layout (location = 1) in vec3 norm; +layout (location = 2) in vec2 uv; + +uniform int ShaderIndex; +uniform float MultiplierY; + +uniform mat4 WorldViewProj; +uniform mat4 World; +uniform mat3 WorldInverseTranspose; + +uniform vec4 DiffuseColor; +uniform vec4 FogVector; + +out vec3 f_Position; +out vec3 f_Normal; +out vec2 f_TexCoord; +out float f_FogFactor; +out vec2 f_uv; + +void main() +{ + vec4 pos4 = vec4(pos, 1.0); + + f_FogFactor = clamp(dot(pos4, FogVector), 0.0, 1.0); + + f_Position = vec3(World * pos4); + f_Normal = normalize(WorldInverseTranspose * norm); + + gl_Position = WorldViewProj * pos4; + gl_Position.y *= MultiplierY; + f_TexCoord = uv; +} + +--- fragment --- + +in vec3 f_Position; +in vec3 f_Normal; +in vec2 f_TexCoord; +in float f_FogFactor; + +uniform vec3 DirLight0Direction; +uniform vec3 DirLight0DiffuseColor; +uniform vec3 DirLight0SpecularColor; + +uniform vec3 DirLight1Direction; +uniform vec3 DirLight1DiffuseColor; +uniform vec3 DirLight1SpecularColor; + +uniform vec3 DirLight2Direction; +uniform vec3 DirLight2DiffuseColor; +uniform vec3 DirLight2SpecularColor; + +uniform vec4 DiffuseColor; +uniform vec3 EmissiveColor; +uniform vec3 SpecularColor; +uniform float SpecularPower; +uniform vec3 FogColor; + +uniform vec3 EyePosition; + +uniform sampler2D Texture; +uniform int ShaderIndex; + +out vec4 o_color; + +void ComputeOneLight(vec3 eyeVector, vec3 worldNormal, out vec3 o_diffuse, out vec3 o_specular) +{ + float dotL = dot(worldNormal, -DirLight0Direction); + float dotH = dot(worldNormal, normalize(eyeVector - DirLight0Direction)); + + float zeroL = step(0.0, dotL); + + float diffuse = zeroL * dotL; + float specular = pow(max(dotH, 0.0) * zeroL, SpecularPower); + + o_diffuse = (DirLight0DiffuseColor * diffuse) * DiffuseColor.rgb + EmissiveColor; + o_specular = (DirLight0SpecularColor * specular) * SpecularColor; +} + +void ComputeThreeLights(vec3 eyeVector, vec3 worldNormal, out vec3 o_diffuse, out vec3 o_specular) +{ + mat3 lightDirections = mat3(DirLight0Direction, DirLight1Direction, DirLight2Direction); + mat3 lightDiffuse = mat3(DirLight0DiffuseColor, DirLight1DiffuseColor, DirLight2DiffuseColor); + mat3 lightSpecular = mat3(DirLight0SpecularColor, DirLight1SpecularColor, DirLight2SpecularColor); + mat3 halfVectors = mat3(normalize(eyeVector - DirLight0Direction), + normalize(eyeVector - DirLight1Direction), + normalize(eyeVector - DirLight2Direction)); + + vec3 dotL = worldNormal * -lightDirections; + vec3 dotH = worldNormal * halfVectors; + + vec3 zeroL = step(vec3(0.0), dotL); + + vec3 diffuse = zeroL * dotL; + vec3 specular = pow(max(dotH, vec3(0.0)) * zeroL, vec3(SpecularPower)); + + o_diffuse = (lightDiffuse * diffuse) * DiffuseColor.rgb + EmissiveColor; + o_specular = (lightSpecular * specular) * SpecularColor; +} + +void main() +{ + vec3 eyeVector = normalize(EyePosition - f_Position); + vec3 worldNormal = normalize(f_Normal); + vec3 diffuse, specular; + if ((ShaderIndex & 24) == 24) + ComputeThreeLights(eyeVector, worldNormal, diffuse, specular); + else if ((ShaderIndex & 24) != 0) + ComputeOneLight(eyeVector, worldNormal, diffuse, specular); + else + { + diffuse = DiffuseColor.rgb; + specular = vec3(0.0); + } + + vec4 color = mix(vec4(1.0), texture(Texture, f_TexCoord), bvec4(ShaderIndex & 4)); + color.rgb *= diffuse; + color.rgb += specular * color.a; + color.rgb = mix(color.rgb, FogColor * color.a, f_FogFactor); + color.a *= DiffuseColor.a; + o_color = color; +} + +--- end --- +"); + + } +} diff --git a/BNA/src/TitleContainer.cs b/BNA/src/TitleContainer.cs new file mode 100644 index 0000000..20e438e --- /dev/null +++ b/BNA/src/TitleContainer.cs @@ -0,0 +1,62 @@ + +using System; +using System.IO; +using Microsoft.Xna.Framework.Graphics; +#pragma warning disable 0436 + +namespace Microsoft.Xna.Framework +{ + + internal static class TitleContainer + { + + public static Stream OpenStream(string name) + { + var stream = GameRunner.Singleton.Activity + .getAssets().open(name.Replace('\\', '/')); + if (stream == null) + throw new System.IO.FileNotFoundException(name); + return new TitleStream(stream, name); + } + + public class TitleStream : Stream + { + public java.io.InputStream JavaStream; + public string Name; + + public TitleStream(java.io.InputStream javaStream, string name) + { + JavaStream = javaStream; + Name = name; + } + + public override bool CanRead => true; + public override bool CanWrite => false; + public override bool CanSeek => false; + + public override int Read(byte[] buffer, int offset, int count) + => JavaStream.read((sbyte[]) (object) buffer, offset, count); + + // + // unused methods and properties + // + + public override long Length => throw new System.PlatformNotSupportedException(); + public override long Position + { + get => throw new System.PlatformNotSupportedException(); + set => throw new System.PlatformNotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + => throw new System.PlatformNotSupportedException(); + public override long Seek(long offset, System.IO.SeekOrigin origin) + => throw new System.PlatformNotSupportedException(); + public override void SetLength(long value) + => throw new System.PlatformNotSupportedException(); + public override void Flush() => throw new System.PlatformNotSupportedException(); + } + + } + +} diff --git a/Demo1/AndroidManifest.xml b/Demo1/AndroidManifest.xml new file mode 100644 index 0000000..a1b15ab --- /dev/null +++ b/Demo1/AndroidManifest.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo1/Demo1.sln b/Demo1/Demo1.sln new file mode 100644 index 0000000..05fff15 --- /dev/null +++ b/Demo1/Demo1.sln @@ -0,0 +1,47 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30503.244 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo1", "Demo1\Demo1.csproj", "{223C1770-AB2F-4278-B8E3-349580A51644}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo1Content", "Demo1Content\Demo1Content.contentproj", "{E50B6FE1-C91C-4226-BA4B-75DEFDEF4CA3}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Demo1FSharp", "Demo1FSharp\Demo1FSharp.fsproj", "{E3649229-B2DD-4CE8-AF10-7814CD306EA5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {223C1770-AB2F-4278-B8E3-349580A51644}.Debug|Any CPU.ActiveCfg = Debug|x86 + {223C1770-AB2F-4278-B8E3-349580A51644}.Debug|Any CPU.Build.0 = Debug|x86 + {223C1770-AB2F-4278-B8E3-349580A51644}.Debug|x86.ActiveCfg = Debug|x86 + {223C1770-AB2F-4278-B8E3-349580A51644}.Debug|x86.Build.0 = Debug|x86 + {223C1770-AB2F-4278-B8E3-349580A51644}.Release|Any CPU.ActiveCfg = Release|x86 + {223C1770-AB2F-4278-B8E3-349580A51644}.Release|Any CPU.Build.0 = Release|x86 + {223C1770-AB2F-4278-B8E3-349580A51644}.Release|x86.ActiveCfg = Release|x86 + {223C1770-AB2F-4278-B8E3-349580A51644}.Release|x86.Build.0 = Release|x86 + {E50B6FE1-C91C-4226-BA4B-75DEFDEF4CA3}.Debug|Any CPU.ActiveCfg = Debug|x86 + {E50B6FE1-C91C-4226-BA4B-75DEFDEF4CA3}.Debug|x86.ActiveCfg = Debug|x86 + {E50B6FE1-C91C-4226-BA4B-75DEFDEF4CA3}.Release|Any CPU.ActiveCfg = Release|x86 + {E50B6FE1-C91C-4226-BA4B-75DEFDEF4CA3}.Release|x86.ActiveCfg = Release|x86 + {E3649229-B2DD-4CE8-AF10-7814CD306EA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3649229-B2DD-4CE8-AF10-7814CD306EA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3649229-B2DD-4CE8-AF10-7814CD306EA5}.Debug|x86.ActiveCfg = Debug|x86 + {E3649229-B2DD-4CE8-AF10-7814CD306EA5}.Debug|x86.Build.0 = Debug|x86 + {E3649229-B2DD-4CE8-AF10-7814CD306EA5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3649229-B2DD-4CE8-AF10-7814CD306EA5}.Release|Any CPU.Build.0 = Release|Any CPU + {E3649229-B2DD-4CE8-AF10-7814CD306EA5}.Release|x86.ActiveCfg = Release|x86 + {E3649229-B2DD-4CE8-AF10-7814CD306EA5}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4E3915BB-BD1B-4790-A50A-63DE2AEB9DAE} + EndGlobalSection +EndGlobal diff --git a/Demo1/Demo1/Config.cs b/Demo1/Demo1/Config.cs new file mode 100644 index 0000000..9a31ece --- /dev/null +++ b/Demo1/Demo1/Config.cs @@ -0,0 +1,74 @@ + +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Demo1 +{ + public static class Config + { + + public static int ClientWidth; + public static int ClientHeight; + public static int PixelsPerInch; + + public static void InitGraphics(Game game) + { + // by default, if the field DisplayOrientation is left as default, + // BNA will fit the game 'window' to the Android screen size. + // this may or not be appropriate. it may be preferrable to use + // a PreparingDeviceSettings hook, as shown below, to explicitly + // set the size and orientation. + + new GraphicsDeviceManager(game).PreparingDeviceSettings += ((sender, args) => + { + var pp = args.GraphicsDeviceInformation.PresentationParameters; + pp.BackBufferWidth = game.Window.ClientBounds.Width; + pp.BackBufferHeight = game.Window.ClientBounds.Height; + pp.DisplayOrientation = game.Window.CurrentOrientation; + pp.RenderTargetUsage = RenderTargetUsage.PreserveContents; + }); + } + + public static void InitWindow(GameWindow window) + { + // request notification as the 'window' changes orientation. + // if AndroidManifest.xml specifies a locked orientation, + // this may not be needed. + window.ClientSizeChanged += WindowResized; + + // initialize the window configuration + WindowResized(window, null); + } + + private static void WindowResized(object sender, EventArgs eventArgs) + { + if (sender is GameWindow window) + { + ClientWidth = window.ClientBounds.Width; + ClientHeight = window.ClientBounds.Height; + + // the BNA GameWindow object (the sender parameter) provides + // an IDictionary object through an IServiceProvider interface. + // the dictionary can be used to query information that is not + // otherwise accessible via XNA interfaces. at this time, only + // the screen DPI (dots per inch) value is provided. + + PixelsPerInch = 144; + if (((object) window) is IServiceProvider windowServiceProvider) + { + var windowDict = (System.Collections.IDictionary) + windowServiceProvider.GetService( + typeof(System.Collections.IDictionary)); + if (windowDict != null) + { + PixelsPerInch = (int) windowDict["dpi"]; + } + } + } + Console.WriteLine($">>> WINDOW CONFIG {ClientWidth} x {ClientHeight} @ {PixelsPerInch} ppi"); + } + + } + +} diff --git a/Demo1/Demo1/CubeDemo.cs b/Demo1/Demo1/CubeDemo.cs new file mode 100644 index 0000000..951d80a --- /dev/null +++ b/Demo1/Demo1/CubeDemo.cs @@ -0,0 +1,184 @@ + + +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input.Touch; + +namespace Demo1 +{ + + public class CubeDemo : DrawableGameComponent + { + +#if CUSTOM_VERTEX_BUFFER + // custom vertex buffers are not supported at this time + private VertexPositionNormalTextureColor[] cube; +#else + private VertexPositionNormalTexture[] cube; +#endif + private BasicEffect theEffect; + private float angle; + private int shaderCycler; + + + public CubeDemo(Game game) : base(game) + { + var face = new Vector3[] + { + //TopLeft-BottomLeft-TopRight + new Vector3(-1f, 1f, 0f), new Vector3(-1f, -1f, 0f), new Vector3( 1f, 1f, 0f), + //BottomLeft-BottomRight-TopRight + new Vector3(-1f, -1f, 0f), new Vector3( 1f, -1f, 0f), new Vector3( 1f, 1f, 0f), + }; + + var faceNormals = new Vector3[] + { + Vector3.UnitZ, -Vector3.UnitZ, //Front & Back faces + Vector3.UnitX, -Vector3.UnitX, //Left & Right faces + Vector3.UnitY, -Vector3.UnitY, //Top & Bottom faces + }; + + var ang90 = (float) Math.PI / 2f; + var faceRotations = new Matrix[] + { + Matrix.CreateRotationY(2f * ang90), + Matrix.CreateRotationY(0f), + Matrix.CreateRotationY(-ang90), + Matrix.CreateRotationY(ang90), + Matrix.CreateRotationX(ang90), + Matrix.CreateRotationX(-ang90) + }; + +#if CUSTOM_VERTEX_BUFFER + cube = new VertexPositionNormalTextureColor[36]; + for (int x = 0; x < cube.Length; x++) + { + var i = x % 6; + var j = x / 6; + cube[x] = new VertexPositionNormalTextureColor( + Vector3.Transform(face[i], faceRotations[j]) + faceNormals[j], + faceNormals[j], Vector2.Zero, Color.Red); + } +#else + var uvCoords = new Vector2[] { + Vector2.Zero, Vector2.UnitY, Vector2.UnitX, Vector2.UnitY, Vector2.One, Vector2.UnitX }; + cube = new VertexPositionNormalTexture[36]; + for (int x = 0; x < cube.Length; x++) + { + var i = x % 6; + var j = x / 6; + cube[x] = new VertexPositionNormalTexture( + Vector3.Transform(face[i], faceRotations[j]) + faceNormals[j], + faceNormals[j], uvCoords[i] * 2f); + } +#endif + } + + + public override void Initialize() + { + theEffect = new BasicEffect(Game.GraphicsDevice) + { +#if CUSTOM_VERTEX_BUFFER + VertexColorEnabled = true, +#endif + AmbientLightColor = new Vector3(0f, 0.2f, 0f), + LightingEnabled = true, + TextureEnabled = true, + PreferPerPixelLighting = true, + FogStart = -10f, FogEnd = 20f, + View = Matrix.CreateTranslation(0f, 0f, -10f), + }; + + theEffect.Texture = Game.Content.Load("4x4"); + + theEffect.DirectionalLight0.Enabled = true; + theEffect.DirectionalLight0.DiffuseColor = new Vector3(1f, 1f, 0f); + theEffect.DirectionalLight0.Direction = Vector3.Down; + + theEffect.DirectionalLight1.Enabled = true; + theEffect.DirectionalLight1.DiffuseColor = new Vector3(0f, 1f, 0f); + theEffect.DirectionalLight1.SpecularColor = new Vector3(0f, 0f, 1f); + theEffect.DirectionalLight1.Direction = Vector3.Right; + + theEffect.DirectionalLight2.Enabled = true; + theEffect.DirectionalLight2.DiffuseColor = new Vector3(1f, 0f, 0f); + theEffect.DirectionalLight2.SpecularColor = new Vector3(0f, 0f, 1f); + theEffect.DirectionalLight2.Direction = Vector3.Left; + + shaderCycler = Storage.GetInt("CubeDemo_ShaderCycler", 1); + UpdateEffect(); + + angle = Storage.GetFloat("CubeDemo_Angle", 0f); + + base.Initialize(); + } + + + + public override void Update(GameTime gameTime) + { + angle += 0.005f; + if (angle > 2f * (float) Math.PI) + angle = 0f; + var R = Matrix.CreateRotationY(angle) * Matrix.CreateRotationX(0.4f); + var T = Matrix.CreateTranslation(0f, 0f, 5f); + theEffect.World = R * T; + + if (Touch.LastGesture.GestureType == Microsoft.Xna.Framework.Input.Touch.GestureType.FreeDrag) + { + angle += Touch.LastGesture.Delta.X * 0.01f; + Touch.LastGesture = default(GestureSample); + } + + Storage.Set("CubeDemo_Angle", angle); + base.Update(gameTime); + } + + + public override void Draw(GameTime gameTime) + { + theEffect.Projection = Matrix.CreatePerspectiveFieldOfView( + (float)Math.PI / 4.0f, + (float)Config.ClientWidth / (float)Config.ClientHeight, + 1f, 10f); + + GraphicsDevice.SamplerStates[0] = ((shaderCycler & 16) == 0) ? SamplerState.PointWrap + : SamplerState.LinearWrap; + + Game.GraphicsDevice.RasterizerState = new RasterizerState(); + foreach (var pass in theEffect.CurrentTechnique.Passes) + { + pass.Apply(); + Game.GraphicsDevice.DrawUserPrimitives( + PrimitiveType.TriangleList, cube, 0, 12); + } + + var shaderIndex = theEffect.Parameters["ShaderIndex"].GetValueInt32(); + var rect = ((Game1) Game).DrawText( + $"SHADER INDEX {shaderIndex}\nTAP HERE TO CYCLE", 0.2f, 0.4f, 1.5f, 0.7f); + + if (Touch.Clicked(rect)) + { + Storage.Set("CubeDemo_ShaderCycler", ++shaderCycler); + UpdateEffect(); + } + + base.Draw(gameTime); + } + + + void UpdateEffect() + { + theEffect.FogEnabled = ((shaderCycler & 1) != 0) ? false : true; + theEffect.TextureEnabled = ((shaderCycler & 2) != 0) ? false : true; + theEffect.PreferPerPixelLighting = ((shaderCycler & 4) != 0) ? false : true; + theEffect.DirectionalLight1.Enabled = ((shaderCycler & 8) != 0) ? false : true; + theEffect.DirectionalLight2.Enabled = ((shaderCycler & 8) != 0) ? false : true; + theEffect.LightingEnabled = ((shaderCycler & 16) != 0) ? false : true; + } + + } + +} diff --git a/Demo1/Demo1/Demo1.csproj b/Demo1/Demo1/Demo1.csproj new file mode 100644 index 0000000..2c947b0 --- /dev/null +++ b/Demo1/Demo1/Demo1.csproj @@ -0,0 +1,110 @@ + + + {223C1770-AB2F-4278-B8E3-349580A51644} + {6D335F3A-9D43-41b4-9D22-F6F17C4BE596};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Debug + x86 + WinExe + Properties + Demo1 + Demo1 + v4.6.1 + + + v4.0 + Windows + HiDef + ed304386-2da8-41d9-bfb0-3d6469f2ace6 + Game + Game.ico + GameThumbnail.png + + + true + full + false + ..\..\.obj\Demo1\Debug\ + DEBUG;TRACE;WINDOWS + prompt + 4 + true + false + x86 + false + 8.0 + MinimumRecommendedRules.ruleset + + + pdbonly + true + ..\..\.obj\Demo1\Release\ + TRACE;WINDOWS + prompt + 4 + true + false + x86 + true + 8.0 + + + $(OutputPath)\Intermediate + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + Demo1Content + Content + + + {e3649229-b2dd-4ce8-af10-7814cd306ea5} + Demo1FSharp + + + + + + + + + + + + \ No newline at end of file diff --git a/Demo1/Demo1/Font.cs b/Demo1/Demo1/Font.cs new file mode 100644 index 0000000..604ff1d --- /dev/null +++ b/Demo1/Demo1/Font.cs @@ -0,0 +1,85 @@ + +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Demo1 +{ + + public class Font + { + + private SpriteBatch spriteBatch; + private SpriteFont spriteFont; + + + public Font(Game game, SpriteBatch spriteBatch, string fontName) + { + spriteFont = game.Content.Load(fontName); + this.spriteBatch = spriteBatch; + } + + + public Rectangle Measure(Vector4 pos, Vector4 size, string text) + { + var mm = spriteFont.MeasureString(text); + + var wh = new Vector2(size.X, size.Y) * Config.PixelsPerInch; + var xy = new Vector2(pos.X, pos.Y) * Config.PixelsPerInch; + xy.X += pos.Z * Config.ClientWidth - size.Z * wh.X; + xy.Y += pos.W * Config.ClientHeight - size.W * wh.Y; + + return new Rectangle((int) xy.X, (int) xy.Y, (int) wh.X, (int) wh.Y); + } + + + + public Rectangle Measure(Vector2 pos, Vector2 size, string text) + { + var pos4 = new Vector4(pos.X, pos.Y, + pos.X >= 0f ? 0f : 1f, + pos.Y >= 0f ? 0f : 1f); + var size4 = new Vector4(size.X, size.Y, 0f, 0f); + return Measure(pos4, size4, text); + } + + + public void Draw(Rectangle rect, Color color, string text) + { + var mm = spriteFont.MeasureString(text); + + var xy = new Vector2(rect.Left, rect.Top); + var wh = new Vector2(rect.Width, rect.Height); + var scl = wh / mm; + + spriteBatch.DrawString(spriteFont, text, xy, color, 0f, Vector2.Zero, scl, + SpriteEffects.None, 0f); + } + + + public void Draw(Vector4 pos, Vector4 size, Color color, string text) + { + var mm = spriteFont.MeasureString(text); + + var wh = new Vector2(size.X, size.Y) * Config.PixelsPerInch; + var xy = new Vector2(pos.X, pos.Y) * Config.PixelsPerInch; + xy.X += pos.Z * Config.ClientWidth- size.Z * wh.X; + xy.Y += pos.W * Config.ClientHeight - size.W * wh.Y; + + var scl = wh / mm; + + spriteBatch.DrawString(spriteFont, text, xy, color, 0f, Vector2.Zero, scl, + SpriteEffects.None, 0f); + } + + public void Draw(Vector2 pos, Vector2 size, Color color, string text) + { + var pos4 = new Vector4(pos.X, pos.Y, + pos.X >= 0f ? 0f : 1f, + pos.Y >= 0f ? 0f : 1f); + var size4 = new Vector4(size.X, size.Y, 0f, 0f); + Draw(pos4, size4, color, text); + } + } + +} diff --git a/Demo1/Demo1/Game.ico b/Demo1/Demo1/Game.ico new file mode 100644 index 0000000..8cff41e Binary files /dev/null and b/Demo1/Demo1/Game.ico differ diff --git a/Demo1/Demo1/Game1.cs b/Demo1/Demo1/Game1.cs new file mode 100644 index 0000000..260bf0e --- /dev/null +++ b/Demo1/Demo1/Game1.cs @@ -0,0 +1,248 @@ + +using System; +using System.IO; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Demo1 +{ + + public class Game1 : Microsoft.Xna.Framework.Game + { + + private DrawableGameComponent pageComponent; + public Texture2D white; + private SpriteBatch spriteBatch; + private Font myFont; + private int pageNumber, pageNumberOld; + private bool paused; + private bool anyDrawText; + + private float framesPerSecond = 60f; + private float countSeconds; + private int countFrames; + + + public Game1() + { + Content.RootDirectory = "Content"; + + // set up callbacks for window creation and resizing + Config.InitGraphics(this); + } + + + protected override void Initialize() + { + base.Initialize(); + Config.InitWindow(Window); + Storage.Init(); + IsMouseVisible = true; + pageNumber = Storage.GetInt("Game_PageNumber", 1); + Components.Add(new Touch(this)); + } + + protected override void LoadContent() + { + spriteBatch = new SpriteBatch(GraphicsDevice); + myFont = new Font(this, spriteBatch, "MyFont"); + white = Content.Load("white"); + + // Android does not have universal support for DXT compression, + // and DXT compression generally creates larger files than PNG. + // thus it may be preferrable to avoid DXT compression altogether + // and load the unprocessed PNG as below: + // + // var stream = TitleContainer.OpenStream( + // Content.RootDirectory + "/image.png"); + // Texture2D.FromStream(GraphicsDevice, stream); + // + // to disable processing, open Properties on the image in the + // Content project, set "Build Action: None", and + // "Copy to Output Directory: Copy if newer". + } + + + protected override void UnloadContent() + { + } + + + protected override void Update(GameTime gameTime) + { + // when resuming the Android activity after it was paused, Update() + // may be invoked multiple times to "catch up" within a time span + // of up to half a second, and then Draw() will be called, and then + // normal processing is resumed. + // if this "catch up" is not desireable, a simple workaround is to + // set a "paused" flag in OnDeactivated(), and clear it in Draw(). + + if (paused) + return; + + // basic scene management for the purpose of this demo: + // after either arrow at the top of the screen is clicked, and + // the current page number has changed, create the new 'page'. + + if (pageNumber != pageNumberOld) + { + const int LAST_PAGE = 4; + if (pageNumber <= 0) + pageNumber = LAST_PAGE; + else if (pageNumber > LAST_PAGE) + pageNumber = 1; + + pageNumberOld = pageNumber; + Storage.Set("Game_PageNumber", pageNumber); + + if (pageComponent != null) + Components.Remove(pageComponent); + + pageComponent = pageNumber switch + { + 1 => new SpriteDemo(this), + 2 => new CubeDemo(this), + 3 => new RenderDemo(this), + // the F# example is in project Demo1FSharp + 4 => new Demo1FSharp.StencilDemo(this), + _ => throw new InvalidOperationException(), + }; + Components.Add(pageComponent); + } + + base.Update(gameTime); + } + + + protected override void Draw(GameTime gameTime) + { + // BNA does not clear the screen at the start of the frame. + // since we are alpha blending, we need to make sure we clear. + + GraphicsDevice.Clear(Color.CornflowerBlue); + spriteBatch.Begin(); + + // display frames per second at the bottom of the screen. + + countSeconds += (float) gameTime.ElapsedGameTime.TotalSeconds; + if (countSeconds > 1f) + { + framesPerSecond = countFrames / countSeconds; + countSeconds -= 1f; + countFrames = 0; + } + else + countFrames++; + + var fps = framesPerSecond; + myFont.Draw(new Vector2(-0.7f, -0.2f), new Vector2(0.6f, 0.2f), Color.Yellow, $"FPS {fps:N2}"); + + // display the logo at the top of the screen + + var ttl = $"BNA Demo (pg. {pageNumber})"; + var rect = myFont.Measure(new Vector4(0f, 0f, 0.5f, 0f), new Vector4(1.25f, 0.3f, 0.5f, 0f), ttl); + spriteBatch.Draw(white, new Rectangle(0, 0, Window.ClientBounds.Width, (int) (rect.Height * 0.9f)), Color.Yellow); + myFont.Draw(rect, Color.Green, ttl); + + // display the arrows at the top of the screen, and check for + // for taps. the Touch class uses the XNA Mouse class, which is + // simulated from the touch screen on Android. see Touch class. + + var btn = "<<<"; + rect = myFont.Measure(new Vector2(0f, 0.05f), new Vector2(0.5f, 0.2f), btn); + myFont.Draw(rect, Color.Black, btn); + if (Touch.Clicked(rect)) + pageNumber--; + + btn = ">>>"; + rect = myFont.Measure(new Vector2(-0.5f, 0.05f), new Vector2(0.5f, 0.2f), btn); + myFont.Draw(rect, Color.Black, btn); + if (Touch.Clicked(rect)) + pageNumber++; + + spriteBatch.End(); + base.Draw(gameTime); + + // reset the "paused" flag. see comment at top of Update() + paused = false; + } + + + // + // utility methods for the 'page' components + // + + + public Rectangle DrawText(string txt, float x, float y, float w, float h) + { + if (! anyDrawText) + { + spriteBatch.Begin(); + anyDrawText = true; + } + + var rect = myFont.Measure(new Vector2(x, y), new Vector2(w, h), txt); + myFont.Draw(rect, Color.Black, txt); + rect.Offset(3, 3); + myFont.Draw(rect, Color.White, txt); + return rect; + } + + + public void DrawSprite(Texture2D sprite, Rectangle rect) + { + if (! anyDrawText) + { + spriteBatch.Begin(); + anyDrawText = true; + } + + spriteBatch.Draw(sprite, rect, Color.White); + } + + + + public void DrawFlushBatch() + { + if (anyDrawText) + { + spriteBatch.End(); + anyDrawText = false; + } + } + + + + protected override void EndDraw() + { + DrawFlushBatch(); + base.EndDraw(); + } + + + protected override void OnActivated(object sender, EventArgs args) + { + } + + + protected override void OnDeactivated(object sender, EventArgs args) + { + // set the "paused" flag. see comment at top of Update() + paused = true; + + // it is important to save the state when Android is pausing, + // and restore the state on start up, because we never know + // when we might get destroyed, but we know we will always + // get the OnDeactivated callback before destruction. + // see also: Storage::Init() + Storage.Sync(); + } + + + protected override void OnExiting(object sender, EventArgs args) + { + // Storage.Clear(); + } + + } +} diff --git a/Demo1/Demo1/GameThumbnail.png b/Demo1/Demo1/GameThumbnail.png new file mode 100644 index 0000000..462311a Binary files /dev/null and b/Demo1/Demo1/GameThumbnail.png differ diff --git a/Demo1/Demo1/Program.cs b/Demo1/Demo1/Program.cs new file mode 100644 index 0000000..604a681 --- /dev/null +++ b/Demo1/Demo1/Program.cs @@ -0,0 +1,31 @@ +using Microsoft.Xna.Framework; +using System; +using System.Security.Principal; + +namespace com.spaceflint.bluebonnet.xnademo1 +{ +#if WINDOWS || XBOX + static class Program + { + /// + /// The main entry point for the application. + /// + static void Main() + { + using (var game = new Demo1.Game1()) + { + try + { + game.Run(); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + System.Windows.Forms.MessageBox.Show(e.ToString()); + } + } + } + } +#endif +} + diff --git a/Demo1/Demo1/Properties/AssemblyInfo.cs b/Demo1/Demo1/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..380d207 --- /dev/null +++ b/Demo1/Demo1/Properties/AssemblyInfo.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Demo1")] +[assembly: AssemblyProduct("Demo1")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. Only Windows +// assemblies support COM. +[assembly: ComVisible(false)] + +// On Windows, the following GUID is for the ID of the typelib if this +// project is exposed to COM. On other platforms, it unique identifies the +// title storage container when deploying this assembly to the device. +[assembly: Guid("223c1770-ab2f-4278-b8e3-349580a51644")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] \ No newline at end of file diff --git a/Demo1/Demo1/RenderDemo.cs b/Demo1/Demo1/RenderDemo.cs new file mode 100644 index 0000000..eac63e1 --- /dev/null +++ b/Demo1/Demo1/RenderDemo.cs @@ -0,0 +1,102 @@ + +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; + +namespace Demo1 +{ + public class RenderDemo : DrawableGameComponent + { + + private bool renderToTexture; + private int renderTargetWidth, renderTargetHeight; + private RenderTarget2D renderTarget; + private Rectangle clickRectangle; + + public RenderDemo(Game game) : base(game) + { + } + + + public override void Initialize() + { + renderToTexture = Storage.GetInt("RenderDemo_RenderToTexture", 0) != 0; + base.Initialize(); + } + + + public override void Draw(GameTime gameTime) + { + // multiple draw calls, particular for drawing fonts, are slow. + // this example shows rendering a lot of text to one texture, + // and drawing just a single texture. + + if (renderToTexture && renderTargetWidth == Config.ClientWidth + && renderTargetHeight == Config.ClientHeight) + { + ((Game1)Game).DrawSprite(renderTarget, + new Rectangle(0, 0, renderTargetWidth, renderTargetHeight)); + } + else + { + if (renderToTexture) + { + ((Game1)Game).DrawFlushBatch(); + renderTargetWidth = Config.ClientWidth; + renderTargetHeight = Config.ClientHeight; + if (renderTarget != null) + renderTarget.Dispose(); + renderTarget = new RenderTarget2D(GraphicsDevice, + renderTargetWidth, renderTargetHeight, + true, SurfaceFormat.Color, DepthFormat.Depth24Stencil8); + GraphicsDevice.SetRenderTarget(renderTarget); + GraphicsDevice.Clear(Color.Transparent); + } + + ReallyDraw(); + + if (renderToTexture) + { + ((Game1)Game).DrawFlushBatch(); + GraphicsDevice.SetRenderTarget(null); + ((Game1)Game).DrawSprite(renderTarget, + new Rectangle(0, 0, renderTargetWidth, renderTargetHeight)); + } + } + + if (Touch.Clicked(clickRectangle)) + { + renderToTexture = ! renderToTexture; + renderTargetWidth = -1; + renderTargetHeight = -1; + Storage.Set("RenderDemo_RenderToTexture", renderToTexture ? 1 : 0); + } + } + + + private void ReallyDraw() + { + float widthInInches = Config.ClientWidth / (float)Config.PixelsPerInch; + float heightInInches = Config.ClientHeight / (float)Config.PixelsPerInch - 1f; + + for (int y = 0; y < 20; y++) + { + for (int x = 0; x < 20; x++) + { + string s = char.ConvertFromUtf32((int)'A' + (x + y) % 26); + float sx = 0.05f + widthInInches * 0.05f * x; + float sy = 0.7f + heightInInches * 0.05f * y; + ((Game1)Game).DrawText(s, sx, sy, 0.1f, 0.1f); + } + } + + string what = renderToTexture ? "DISABLE" : "ENABLE"; + clickRectangle = ((Game1)Game).DrawText( + $" TAP TO {what} RENDER TEXTURE ", 0f, 0.35f, widthInInches, 0.2f); + + } + + } + +} diff --git a/Demo1/Demo1/SpriteDemo.cs b/Demo1/Demo1/SpriteDemo.cs new file mode 100644 index 0000000..768d24f --- /dev/null +++ b/Demo1/Demo1/SpriteDemo.cs @@ -0,0 +1,60 @@ + +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; + +namespace Demo1 +{ + public class SpriteDemo : DrawableGameComponent + { + + private Texture2D ball; + private SpriteBatch spriteBatch; + private int x, y; + private int dx, dy; + + + public SpriteDemo(Game game) : base(game) + { + spriteBatch = new SpriteBatch(game.GraphicsDevice); + } + + + public override void Initialize() + { + ball = Game.Content.Load("circle"); + + x = Storage.GetInt("SpriteDemo_X", Config.ClientWidth / 2); + y = Storage.GetInt("SpriteDemo_Y", Config.ClientHeight / 2); + dx = Storage.GetInt("SpriteDemo_DX", 1); + dy = Storage.GetInt("SpriteDemo_DY", 1); + } + + + public override void Update(GameTime gameTime) + { + if (x < 0 || x + Config.PixelsPerInch > Config.ClientWidth) + dx = -dx; + if (y < 0 || y + Config.PixelsPerInch > Config.ClientHeight) + dy = -dy; + x += dx * 2; + y += dy * 2; + + Storage.Set("SpriteDemo_X", x); + Storage.Set("SpriteDemo_Y", y); + Storage.Set("SpriteDemo_DX", dx); + Storage.Set("SpriteDemo_DY", dy); + } + + + public override void Draw(GameTime gameTime) + { + spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend); + spriteBatch.Draw(ball, new Rectangle(x, y, Config.PixelsPerInch, Config.PixelsPerInch), Color.Red); + spriteBatch.End(); + } + + } + +} diff --git a/Demo1/Demo1/Storage.cs b/Demo1/Demo1/Storage.cs new file mode 100644 index 0000000..0f74a5b --- /dev/null +++ b/Demo1/Demo1/Storage.cs @@ -0,0 +1,155 @@ + +using System; +using System.IO; +using System.Text; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Storage; + +namespace Demo1 +{ + public static class Storage + { + + private const int VERSION_1 = 0x10101; + private static readonly int INTEGER = BitConverter.ToInt32(Encoding.ASCII.GetBytes("int "), 0); + private static readonly int FLOAT = BitConverter.ToInt32(Encoding.ASCII.GetBytes("flt "), 0); + private static readonly int STRING = BitConverter.ToInt32(Encoding.ASCII.GetBytes("str "), 0); + + private static Stream file; + private static Dictionary dict; + + public static void Init() + { + dict = new Dictionary(); + + // on Android, StorageDevice and StorageContainer are "thin" + // objects that do little more than translate the relative path + // specified in StorageContainer::OpenFile, to a full path in + // the app folder, which is then passed to System.IO.File.Open. + + // the basic persistence model here is: game components update + // a dictionary with current values. the dictionary is written + // to a file on pause (Game::OnDeactivated calls Storage::Sync), + // and read from the file on startup (Game::Initialize calls + // Storage::Init). + + try + { + var result = StorageDevice.BeginShowSelector(null, null); + result.AsyncWaitHandle.WaitOne(); + var device = StorageDevice.EndShowSelector(result); + result.AsyncWaitHandle.Close(); + + result = device.BeginOpenContainer("Demo1", null, null); + result.AsyncWaitHandle.WaitOne(); + var container = device.EndOpenContainer(result); + result.AsyncWaitHandle.Close(); + + file = container.OpenFile("state-v1.bin", FileMode.OpenOrCreate); + if (file.Length > 4) + Read(file); + } + catch (Exception e) + { + Console.WriteLine("Exception while reading state: " + e); + dict.Clear(); + } + } + + public static void Sync() + { + file.Position = 0; + try + { + Write(file); + } + catch (Exception e) + { + Console.WriteLine("Exception while writing state: " + e); + file.SetLength(0); + } + file.Flush(); + } + + public static void Clear() + { + dict.Clear(); + Sync(); + } + + static void Read(Stream file) + { + using (var reader = new BinaryReader(file, Encoding.UTF8, true)) + { + var version = reader.ReadInt32(); + if (version == VERSION_1) + { + for (var count = reader.ReadInt32(); count > 0; count--) + { + var name = reader.ReadString(); + var type = reader.ReadInt32(); + var obj = (type == INTEGER) ? (object) reader.ReadInt32() + : (type == FLOAT) ? (object) reader.ReadSingle() + : (type == STRING) ? (object) reader.ReadString() + : null; + if (obj == null) + throw new NullReferenceException(); + dict[name] = obj; + } + } + } + } + + static void Write(Stream file) + { + using (var writer = new BinaryWriter(file, Encoding.UTF8, true)) + { + writer.Write(VERSION_1); + writer.Write(dict.Count); + foreach (var kvp in dict) + { + writer.Write(kvp.Key); + if (kvp.Value is int intValue) + { + writer.Write(INTEGER); + writer.Write(intValue); + } + else if (kvp.Value is float floatValue) + { + writer.Write(FLOAT); + writer.Write(floatValue); + } + else if (kvp.Value is string stringValue) + { + writer.Write(STRING); + writer.Write(stringValue); + } + } + } + } + + public static int GetInt(string name, int defValue = 0) + { + return dict.TryGetValue(name, out var v) + && (v is int intValue) ? intValue : defValue; + } + + public static float GetFloat(string name, float defValue = 0f) + { + return dict.TryGetValue(name, out var v) + && (v is float floatValue) ? floatValue : defValue; + } + + public static string GetString(string name, string defValue = "") + { + return dict.TryGetValue(name, out var v) + && (v is string stringValue) ? stringValue : defValue; + } + + public static void Set(string name, object value) + { + dict[name] = value; + } + + } +} diff --git a/Demo1/Demo1/Touch.cs b/Demo1/Demo1/Touch.cs new file mode 100644 index 0000000..175f8fe --- /dev/null +++ b/Demo1/Demo1/Touch.cs @@ -0,0 +1,80 @@ + +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using Microsoft.Xna.Framework.Input.Touch; + +namespace Demo1 +{ + public class Touch : DrawableGameComponent + { + + private int pressX, pressY; + private int releaseX, releaseY; + private static Touch instance; + public static GestureSample LastGesture; + + + public Touch(Game game) : base(game) + { + pressX = int.MinValue; + releaseX = int.MinValue; + instance = this; + + // TouchPanel is functional when running on Android + TouchPanel.EnabledGestures = GestureType.Tap | GestureType.FreeDrag; + } + + + protected override void Dispose(bool disposing) + { + instance = null; + base.Dispose(disposing); + } + + public override void Draw(GameTime gameTime) + { + // this code is in Draw because Update may be invoked multiple times + // per frame, which might cause the loss of the occasional click + + // on Android, the Mouse class tracks single-finger taps, so it can + // be used on both Windows and Android for simple input. for more + // advanced touch tracking, use TouchPanel and gestures. + + var state = Mouse.GetState(); + if (state.LeftButton == ButtonState.Pressed) + { + if (pressX == int.MinValue) + { + pressX = state.X; + pressY = state.Y; + } + } + else if (pressX != int.MinValue && releaseX == int.MinValue) + { + releaseX = state.X; + releaseY = state.Y; + } + else + { + pressX = int.MinValue; + releaseX = int.MinValue; + } + + if (TouchPanel.IsGestureAvailable) + { + LastGesture = TouchPanel.ReadGesture(); + Console.WriteLine($"Gesture {LastGesture.GestureType} at {LastGesture.Position}"); + } + } + + + public bool _Clicked(Rectangle rect) + => rect.Contains(pressX, pressY) && rect.Contains(releaseX, releaseY); + + public static bool Clicked(Rectangle rect) => instance._Clicked(rect); + + } + +} diff --git a/Demo1/Demo1/VertexPositionNormalTextureColor.cs b/Demo1/Demo1/VertexPositionNormalTextureColor.cs new file mode 100644 index 0000000..9c8bb37 --- /dev/null +++ b/Demo1/Demo1/VertexPositionNormalTextureColor.cs @@ -0,0 +1,118 @@ + +using System; +using System.Runtime.InteropServices; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Demo1 +{ + [Serializable] + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct VertexPositionNormalTextureColor : IVertexType + { + VertexDeclaration IVertexType.VertexDeclaration + { + get + { + return VertexDeclaration; + } + } + + public Vector3 Position; + public Vector3 Normal; + public Vector2 TextureCoordinate; + public Color Color; + + public static readonly VertexDeclaration VertexDeclaration; + + static VertexPositionNormalTextureColor() + { + VertexDeclaration = new VertexDeclaration( + new VertexElement[] + { + new VertexElement( + 0, + VertexElementFormat.Vector3, + VertexElementUsage.Position, + 0 + ), + new VertexElement( + 12, + VertexElementFormat.Vector3, + VertexElementUsage.Normal, + 0 + ), + new VertexElement( + 24, + VertexElementFormat.Vector2, + VertexElementUsage.TextureCoordinate, + 0 + ), + new VertexElement( + 32, + VertexElementFormat.Color, + VertexElementUsage.Color, + 0 + ), + } + ); + } + + public VertexPositionNormalTextureColor( + Vector3 position, + Vector3 normal, + Vector2 textureCoordinate, + Color color) + { + Position = position; + Normal = normal; + TextureCoordinate = textureCoordinate; + Color = color; + } + + public override int GetHashCode() + { + // TODO: Fix GetHashCode + return 0; + } + + public override string ToString() + { + return ( + "{{Position:" + Position.ToString() + + " Normal:" + Normal.ToString() + + " TextureCoordinate:" + TextureCoordinate.ToString() + + " Color:" + Color.ToString() + + "}}" + ); + } + + public static bool operator ==(VertexPositionNormalTextureColor left, VertexPositionNormalTextureColor right) + { + return ( (left.Position == right.Position) && + (left.Normal == right.Normal) && + (left.TextureCoordinate == right.TextureCoordinate) && + (left.Color == right.Color) + ); + } + + public static bool operator !=(VertexPositionNormalTextureColor left, VertexPositionNormalTextureColor right) + { + return !(left == right); + } + + public override bool Equals(object obj) + { + if (obj == null) + { + return false; + } + if (obj.GetType() != base.GetType()) + { + return false; + } + return (this == ((VertexPositionNormalTextureColor) obj)); + } + + } +} diff --git a/Demo1/Demo1/app.config b/Demo1/Demo1/app.config new file mode 100644 index 0000000..3dbff35 --- /dev/null +++ b/Demo1/Demo1/app.config @@ -0,0 +1,3 @@ + + + diff --git a/Demo1/Demo1Content/4x4.png b/Demo1/Demo1Content/4x4.png new file mode 100644 index 0000000..324c31c Binary files /dev/null and b/Demo1/Demo1Content/4x4.png differ diff --git a/Demo1/Demo1Content/Demo1Content.contentproj b/Demo1/Demo1Content/Demo1Content.contentproj new file mode 100644 index 0000000..cee89a8 --- /dev/null +++ b/Demo1/Demo1Content/Demo1Content.contentproj @@ -0,0 +1,75 @@ + + + {E50B6FE1-C91C-4226-BA4B-75DEFDEF4CA3} + {96E2B04D-8817-42c6-938A-82C39BA4D311};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Debug + x86 + Library + Properties + v4.0 + v4.0 + ..\..\.obj\Demo1\Content + $(OutputPath)\Intermediate + Content + true + + + x86 + + + x86 + + + Demo1Content + + + + + + + + + + + + circle + TextureImporter + TextureProcessor + + + 4x4 + TextureImporter + TextureProcessor + False + False + NoChange + + + white + TextureImporter + TextureProcessor + + + + + MyFont + FontDescriptionImporter + FontDescriptionProcessor + + + + + fsharp256 + TextureImporter + TextureProcessor + + + + + \ No newline at end of file diff --git a/Demo1/Demo1Content/MyFont.spritefont b/Demo1/Demo1Content/MyFont.spritefont new file mode 100644 index 0000000..4daa038 --- /dev/null +++ b/Demo1/Demo1Content/MyFont.spritefont @@ -0,0 +1,60 @@ + + + + + + + Consolas + + + 64 + + + 0 + + + true + + + + + + + + + + + + ~ + + + + diff --git a/Demo1/Demo1Content/circle.png b/Demo1/Demo1Content/circle.png new file mode 100644 index 0000000..c1d5ea2 Binary files /dev/null and b/Demo1/Demo1Content/circle.png differ diff --git a/Demo1/Demo1Content/fsharp256.png b/Demo1/Demo1Content/fsharp256.png new file mode 100644 index 0000000..f984490 Binary files /dev/null and b/Demo1/Demo1Content/fsharp256.png differ diff --git a/Demo1/Demo1Content/white.png b/Demo1/Demo1Content/white.png new file mode 100644 index 0000000..13c0d35 Binary files /dev/null and b/Demo1/Demo1Content/white.png differ diff --git a/Demo1/Demo1FSharp/Demo1FSharp.fsproj b/Demo1/Demo1FSharp/Demo1FSharp.fsproj new file mode 100644 index 0000000..a89d7e0 --- /dev/null +++ b/Demo1/Demo1FSharp/Demo1FSharp.fsproj @@ -0,0 +1,30 @@ + + + + Library + net461 + latest + ..\..\.obj\Demo1\$(Configuration)\ + $(OutputPath)\Intermediate + x86 + AnyCPU;x86 + + + + + + + + C:\Program Files (x86)\Microsoft XNA\XNA Game Studio\v4.0\References\Windows\x86\Microsoft.Xna.Framework.dll + + + + C:\Program Files (x86)\Microsoft XNA\XNA Game Studio\v4.0\References\Windows\x86\Microsoft.Xna.Framework.Game.dll + + + + C:\Program Files (x86)\Microsoft XNA\XNA Game Studio\v4.0\References\Windows\x86\Microsoft.Xna.Framework.Graphics.dll + + + + diff --git a/Demo1/Demo1FSharp/Directory.Build.props b/Demo1/Demo1FSharp/Directory.Build.props new file mode 100644 index 0000000..91a3da7 --- /dev/null +++ b/Demo1/Demo1FSharp/Directory.Build.props @@ -0,0 +1,5 @@ + + + ..\..\.obj\Demo1\packages + + diff --git a/Demo1/Demo1FSharp/Library1.fs b/Demo1/Demo1FSharp/Library1.fs new file mode 100644 index 0000000..30f1236 --- /dev/null +++ b/Demo1/Demo1FSharp/Library1.fs @@ -0,0 +1,37 @@ +namespace Demo1FSharp + +open System +open Microsoft.Xna.Framework +open Microsoft.Xna.Framework.Graphics + +type StencilDemo (game : Game) = + inherit DrawableGameComponent(game) + + let mutable spriteBatch = null + let mutable texture = null + + // using option here solely to force a dependency on FSharp.Core.dll + let mutable rectFunc : (Game -> Rectangle) option = None + let mutable colorFunc : (GameTime -> Color) option = None + + let logoRect (game : Game) = + let (sw, sh) = (game.Window.ClientBounds.Width, game.Window.ClientBounds.Height) + let (iw, ih) = ((int) ((single) sw * 0.75f), (int) ((single) sh * 0.75f)) + let (ix, iy) = ((sw - iw) / 2, (sh - ih) / 2) + Rectangle(ix, iy, iw, ih) + + let logoColor (gameTime : GameTime) = + Color(0.f, + (float32) (Math.Sin(gameTime.TotalGameTime.TotalMilliseconds * 0.001)), + (float32) (Math.Cos(gameTime.TotalGameTime.TotalMilliseconds * 0.001))) + + override Game.Initialize() = + spriteBatch <- new SpriteBatch (game.GraphicsDevice) + texture <- game.Content.Load("fsharp256") + rectFunc <- Some logoRect + colorFunc <- Some logoColor + + override Game.Draw gameTime = + spriteBatch.Begin () + spriteBatch.Draw (texture, rectFunc.Value game, colorFunc.Value gameTime) + spriteBatch.End () \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..67d27c6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 spaceflint7 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MakeAPK.project b/MakeAPK.project new file mode 100644 index 0000000..df42009 --- /dev/null +++ b/MakeAPK.project @@ -0,0 +1,104 @@ + + + + + + + OnOutputUpdated + + + + + + + %(ContentDir.Filename)%(ContentDir.Extension) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..00cf07f --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ + +# Bluebonnet BNA + +This is a port of [FNA](https://fna-xna.github.io/) for use with [Bluebonnet](https://github.com/spaceflint7/bluebonnet) to build games for Android using the [XNA 4.0](https://en.wikipedia.org/wiki/Microsoft_XNA) libraries. + +**Bluebonnet** is an Android-compatible implementation of the .NET platform on top of the Java Virtual Machine. **Bluebonnet BNA** makes it possible to compile XNA games written in C# or F# to Android Java without any dependencies on native code libraries. + +## Building + +- Download and build the `Bluebonnet` compiler and its runtime library, `Baselib.jar`. For instructions, see [Bluebonnet README](https://github.com/spaceflint7/bluebonnet/blob/master/README.md). + +- Download the [FNA](https://github.com/FNA-XNA/FNA/archive/master.zip) source code. Build by typing the following command in the FNA root directory: + + - `MSBuild FNA.csproj -p:Configuration=Release` + + - If the build is successful, the file `FNA.DLL` will be generated in the `bin/Release` sub-directory of the FNA root directory. + +- Download this `BNA` project and build it by typing the following command in the BNA root directory: + + - `MSBuild BNA -p:Configuration=Release -p:ANDROID_JAR=/path/to/Android.jar -p:BLUEBONNET_EXE=/path/to/Bluebonnet/executable -p:FNA_DLL=/path/to/FNA.DLL` + + - The `ANDROID_JAR` property specifies the full path to an `Android.jar` file from the Android SDK distribution. `BNA` requires Android SDK version 18 or later. + + - The `BLUEBONNET_EXE` property specifies the full path to the Bluebonnet compiler that you built in an earlier step. + + - The `FNA_DLL` property specifies the full path to the FNA.DLL that you built in an earlier step. As noted earlier, this path should be `(FNA_DIR)/bin/Release/FNA.dll`. + + - If the build is successful, the file `BNA.jar` will be generated in the `.obj` sub-directory of the repository root directory. + +## Building the Demo + +An example application `Demo1` is provided, which demonstrates some XNA functionality in C# and F#. It can be built using Visual Studio (solution file `Demo1.sln`), or from the command line: + +- Type `nuget restore Demo1` to restore packages using [nuget](https://www.nuget.org/downloads). + +- Type `msbuild Demo1 -p:Configuration=Release -p:Platform="x86"` + +- Test the program: `.obj\Demo1\Release\Demo1.exe`. (Note that this will create the directory `SavedGames\Demo1` in the `Documents` directory.) + +- To convert the built application to an Android APK, type the following command: (Note that this is a single-line command; line breaks were added for clarity.) + + - MSBuild MakeAPK.project
+ -p:INPUT_DLL=.obj\Demo1\Release\Demo1.exe
+ -p:INPUT_DLL_2=.obj\Demo1\Release\Demo1FSharp.dll
+ -p:INPUT_DLL_3=.obj\Demo1\Release\FSharp.Core.dll
+ -p:CONTENT_DIR=.obj\Demo1\Release\Content
+ -p:ICON_PNG=Demo1\Demo1\GameThumbnail.png
+ -p:ANDROID_MANIFEST=Demo1\AndroidManifest.xml
+ -p:KEYSTORE_FILE=.\my.keystore
+ -p:KEYSTORE_PWD=123456
+ -p:APK_OUTPUT=.obj\Demo1.apk
+ -p:APK_TEMP_DIR=.obj\Demo1\Release\TempApk
+ -p:EXTRA_JAR_1=.obj\BNA.jar
+ -p:EXTRA_JAR_2=\path\to\Bluebonnet\Baselib.jar
+ -p:BLUEBONNET_EXE=\path\to\Bluebonnet.exe
+ -p:ANDROID_JAR=\path\to\Android\platforms\android-XX\android.jar
+ -p:ANDROID_BUILD=\path\to\Android\build-tools\30.0.2
+ + - Make sure to specify the right paths for the Bluebonnet compiler (via the `BLUEBONNET_EXE` property), the Baselib support library (via the `EXTRA_JAR_2` property), the Android.jar file (via the `ANDROID_JAR` property) and the Android build-tools directory (via the `ANDROID_BUILD` property). + + - The parameters are detailed at the top of the [MakeAPK.project](MakeAPK.project) file. See also the comments in the [AndroidManifest.xml](Demo1/AndroidManifest.xml) file, and comments throughout the `Demo1` source files. + + - If the build is successful, the file `Demo1.apk` will be generated in the `.obj` sub-directory of the repository root directory. + +- The batch file `build_demo.bat` runs the steps discussed in this "Building the Demo" section. + +- Install the built APK to an Android device: + + - `\path\to\Android\platform-tools\adb install -r .obj\Demo1.apk` diff --git a/Solution.project b/Solution.project new file mode 100644 index 0000000..50669fa --- /dev/null +++ b/Solution.project @@ -0,0 +1,64 @@ + + + + + + + $(MSBuildThisFileDirectory) + $(MSBuildThisFileDirectory) + + Release + AnyCPU + + Properties + + 512 + true + v4.7.2 + + 8.0 + false + 4 + false + prompt + + + true + + $(MSBuildThisFileDirectory).obj\ + $(ObjDir)$(AssemblyName)\$(Configuration)\ + $(ObjDir)$(AssemblyName)\$(Configuration)\ + + + + + pdbonly + true + + + + true + full + false + DEBUG;TRACE + + + + 4.7 + portable + + + + + + + + + + + + diff --git a/build_demo.bat b/build_demo.bat new file mode 100644 index 0000000..cc555c4 --- /dev/null +++ b/build_demo.bat @@ -0,0 +1,54 @@ +@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 "%ANDROID_BUILD%" == "" ( + echo Missing environment variable ANDROID_BUILD. + echo It should specify the full path to a build-tools directory in 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 +) +if "%BLUEBONNET_LIB%" == "" ( + echo Missing environment variable BLUEBONNET_LIB. + echo It should specify the full path to the Bluebonnet Baselib.jar file. + goto :EOF +) + +echo ======================================== +echo Building BNA. Command: +echo MSBuild BNA -p:Configuration=Release +echo ======================================== +MSBuild BNA -p:Configuration=Release +pause + +echo ======================================== +echo Building Demo1. Command: +echo nuget restore Demo1 +echo msbuild Demo1 -p:Configuration=Release -p:Platform="x86" +echo ======================================== +nuget restore Demo1 +msbuild Demo1 -p:Configuration=Release -p:Platform="x86" +pause + +echo ======================================== +echo Converting Demo1 to APK. Command: +echo MSBuild MakeAPK.project -p:INPUT_DLL=.obj\Demo1\Release\Demo1.exe -p:INPUT_DLL_2=.obj\Demo1\Release\Demo1FSharp.dll -p:INPUT_DLL_3=.obj\Demo1\Release\FSharp.Core.dll -p:CONTENT_DIR=.obj\Demo1\Release\Content -p:ICON_PNG=Demo1\Demo1\GameThumbnail.png -p:ANDROID_MANIFEST=Demo1\AndroidManifest.xml -p:KEYSTORE_FILE=.\my.keystore -p:KEYSTORE_PWD=123456 -p:APK_OUTPUT=.obj\Demo1.apk -p:APK_TEMP_DIR=.obj\Demo1\Release\TempApk -p:EXTRA_JAR_1=.obj\BNA.jar -p:EXTRA_JAR_2=%BLUEBONNET_LIB% +echo ======================================== +MSBuild MakeAPK.project -p:INPUT_DLL=.obj\Demo1\Release\Demo1.exe -p:INPUT_DLL_2=.obj\Demo1\Release\Demo1FSharp.dll -p:INPUT_DLL_3=.obj\Demo1\Release\FSharp.Core.dll -p:CONTENT_DIR=.obj\Demo1\Release\Content -p:ICON_PNG=Demo1\Demo1\GameThumbnail.png -p:ANDROID_MANIFEST=Demo1\AndroidManifest.xml -p:KEYSTORE_FILE=.\my.keystore -p:KEYSTORE_PWD=123456 -p:APK_OUTPUT=.obj\Demo1.apk -p:APK_TEMP_DIR=.obj\Demo1\Release\TempApk -p:EXTRA_JAR_1=.obj\BNA.jar -p:EXTRA_JAR_2=%BLUEBONNET_LIB% + +echo ======================================== +echo All done +echo ======================================== + +:EOF diff --git a/my.keystore b/my.keystore new file mode 100644 index 0000000..6b0270d Binary files /dev/null and b/my.keystore differ