From de8f4a88e7c68a7fec1ea670f421faf7381ad02c Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Fri, 15 May 2026 15:41:42 +0200 Subject: [PATCH 1/6] ios backend --- build.gradle | 1 + .../environment/baker/IBLGLEnvBakerLight.java | 43 +- .../src/main/java/com/jme3/renderer/Caps.java | 5 + .../com/jme3/renderer/opengl/GLRenderer.java | 6 +- .../com/jme3/util/MaterialDebugAppState.java | 13 +- .../resources/Common/IBLSphH/IBLSphH.frag | 140 +- .../resources/Common/IBLSphH/IBLSphH.j3md | 6 +- .../jme3test/light/pbr/TestPBRSimple.java | 2 +- jme3-ios-examples/build.gradle | 385 +++ .../java/jme3test/ios/IosTestChooser.java | 428 +++ .../jme3test/ios/IosTestChooserLauncher.java | 205 ++ .../java/jme3test/ios/IosTestChooserTest.java | 176 ++ jme3-ios-native/build.gradle | 37 - jme3-ios-native/export.sh | 11 - .../jme3-ios-native.xcodeproj/project.pbxproj | 416 --- jme3-ios-native/src/Info.plist | 22 - jme3-ios-native/src/JmeAppHarness.java | 130 - jme3-ios-native/src/JmeAppHarness.m | 60 - jme3-ios-native/src/JmeIosGLES.m | 2392 ----------------- .../src/com_jme3_audio_ios_IosAL.c | 138 - .../src/com_jme3_audio_ios_IosAL.h | 173 -- .../src/com_jme3_audio_ios_IosALC.c | 178 -- .../src/com_jme3_audio_ios_IosALC.h | 77 - .../src/com_jme3_audio_ios_IosEFX.c | 79 - .../src/com_jme3_audio_ios_IosEFX.h | 101 - .../com_jme3_util_IosNativeBufferAllocator.c | 94 - .../com_jme3_util_IosNativeBufferAllocator.h | 29 - jme3-ios-native/src/jme-ios.m | 192 -- jme3-ios-native/src/jme3_ios_native.h | 18 - .../template/META-INF/robovm/ios/robovm.xml | 9 - jme3-ios/build.gradle | 24 + jme3-ios/src/main/java/com/jme3/asset/IOS.cfg | 10 - .../main/java/com/jme3/audio/ios/IosAL.java | 184 +- .../main/java/com/jme3/audio/ios/IosALC.java | 42 +- .../main/java/com/jme3/audio/ios/IosEFX.java | 48 +- .../com/jme3/input/ios/IosInputHandler.java | 136 +- .../java/com/jme3/input/ios/IosJoyInput.java | 345 +++ .../java/com/jme3/input/ios/IosSdlKeyMap.java | 347 +++ .../com/jme3/input/ios/IosTouchHandler.java | 62 +- .../com/jme3/renderer/ios/JmeIosGLES.java | 353 +-- .../com/jme3/system/ios/IGLESContext.java | 291 +- .../java/com/jme3/system/ios/IosHarness.java | 61 - ...IosImageLoader.java => JmeIosBackend.java} | 62 +- .../com/jme3/system/ios/JmeIosSystem.java | 26 +- .../com/jme3/system/ios/ObjcNativeObject.java | 130 - .../jme3/util/IosNativeBufferAllocator.java | 71 - .../util/LibJGLIOSNativeBufferAllocator.java | 27 + .../src/main/resources/com/jme3/asset/IOS.cfg | 7 - .../jme3/input/ios/IosInputHandlerTest.java | 220 ++ .../jme3/input/lwjgl/SdlJoystickInput.java | 25 + settings.gradle | 18 +- 51 files changed, 3104 insertions(+), 4951 deletions(-) create mode 100644 jme3-ios-examples/build.gradle create mode 100644 jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooser.java create mode 100644 jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooserLauncher.java create mode 100644 jme3-ios-examples/src/test/java/jme3test/ios/IosTestChooserTest.java delete mode 100644 jme3-ios-native/build.gradle delete mode 100755 jme3-ios-native/export.sh delete mode 100644 jme3-ios-native/jme3-ios-native.xcodeproj/project.pbxproj delete mode 100644 jme3-ios-native/src/Info.plist delete mode 100644 jme3-ios-native/src/JmeAppHarness.java delete mode 100644 jme3-ios-native/src/JmeAppHarness.m delete mode 100644 jme3-ios-native/src/JmeIosGLES.m delete mode 100644 jme3-ios-native/src/com_jme3_audio_ios_IosAL.c delete mode 100644 jme3-ios-native/src/com_jme3_audio_ios_IosAL.h delete mode 100644 jme3-ios-native/src/com_jme3_audio_ios_IosALC.c delete mode 100644 jme3-ios-native/src/com_jme3_audio_ios_IosALC.h delete mode 100644 jme3-ios-native/src/com_jme3_audio_ios_IosEFX.c delete mode 100644 jme3-ios-native/src/com_jme3_audio_ios_IosEFX.h delete mode 100644 jme3-ios-native/src/com_jme3_util_IosNativeBufferAllocator.c delete mode 100644 jme3-ios-native/src/com_jme3_util_IosNativeBufferAllocator.h delete mode 100644 jme3-ios-native/src/jme-ios.m delete mode 100644 jme3-ios-native/src/jme3_ios_native.h delete mode 100644 jme3-ios-native/template/META-INF/robovm/ios/robovm.xml delete mode 100644 jme3-ios/src/main/java/com/jme3/asset/IOS.cfg create mode 100644 jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java create mode 100644 jme3-ios/src/main/java/com/jme3/input/ios/IosSdlKeyMap.java delete mode 100644 jme3-ios/src/main/java/com/jme3/system/ios/IosHarness.java rename jme3-ios/src/main/java/com/jme3/system/ios/{IosImageLoader.java => JmeIosBackend.java} (55%) delete mode 100644 jme3-ios/src/main/java/com/jme3/system/ios/ObjcNativeObject.java delete mode 100644 jme3-ios/src/main/java/com/jme3/util/IosNativeBufferAllocator.java create mode 100644 jme3-ios/src/main/java/com/jme3/util/LibJGLIOSNativeBufferAllocator.java create mode 100644 jme3-ios/src/test/java/com/jme3/input/ios/IosInputHandlerTest.java diff --git a/build.gradle b/build.gradle index 3a1395207c..4f87de2333 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ apply from: file('version.gradle') allprojects { repositories { + mavenLocal() mavenCentral() google() } diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java index 54e0bac574..af468ba78a 100644 --- a/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java +++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java @@ -38,7 +38,6 @@ import com.jme3.material.Material; import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; -import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; import com.jme3.renderer.Caps; import com.jme3.renderer.RenderManager; @@ -98,7 +97,6 @@ public void bakeSphericalHarmonicsCoefficients() { Material mat = new Material(assetManager, "Common/IBLSphH/IBLSphH.j3md"); mat.setTexture("Texture", envMap); - mat.setVector2("Resolution", new Vector2f(envMap.getImage().getWidth(), envMap.getImage().getHeight())); screen.setMaterial(mat); float remapMaxValue = 0; @@ -117,38 +115,21 @@ public void bakeSphericalHarmonicsCoefficients() { mat.clearParam("RemapMaxValue"); } - Texture2D shCoefTx[] = { new Texture2D(NUM_SH_COEFFICIENT, 1, 1, format), new Texture2D(NUM_SH_COEFFICIENT, 1, 1, format) }; + Texture2D shCoefTx = new Texture2D(NUM_SH_COEFFICIENT, 1, 1, format); - FrameBuffer shbaker[] = { new FrameBuffer(NUM_SH_COEFFICIENT, 1, 1), new FrameBuffer(NUM_SH_COEFFICIENT, 1, 1) }; - shbaker[0].setSrgb(false); - shbaker[0].addColorTarget(FrameBufferTarget.newTarget(shCoefTx[0])); + FrameBuffer shbaker = new FrameBuffer(NUM_SH_COEFFICIENT, 1, 1); + shbaker.setSrgb(false); + shbaker.addColorTarget(FrameBufferTarget.newTarget(shCoefTx)); - shbaker[1].setSrgb(false); - shbaker[1].addColorTarget(FrameBufferTarget.newTarget(shCoefTx[1])); + screen.updateLogicalState(0); + screen.updateGeometricState(); - int renderOnT = -1; + renderManager.setCamera(updateAndGetInternalCamera(0, shbaker.getWidth(), shbaker.getHeight(), Vector3f.ZERO, 1, 1000), false); + renderManager.getRenderer().setFrameBuffer(shbaker); + renderManager.renderGeometry(screen); - for (int faceId = 0; faceId < 6; faceId++) { - if (renderOnT != -1) { - int s = renderOnT; - renderOnT = renderOnT == 0 ? 1 : 0; - mat.setTexture("ShCoef", shCoefTx[s]); - } else { - renderOnT = 0; - } - - mat.setInt("FaceId", faceId); - - screen.updateLogicalState(0); - screen.updateGeometricState(); - - renderManager.setCamera(updateAndGetInternalCamera(0, shbaker[renderOnT].getWidth(), shbaker[renderOnT].getHeight(), Vector3f.ZERO, 1, 1000), false); - renderManager.getRenderer().setFrameBuffer(shbaker[renderOnT]); - renderManager.renderGeometry(screen); - } - - ByteBuffer shCoefRaw = BufferUtils.createByteBuffer(NUM_SH_COEFFICIENT * 1 * (shbaker[renderOnT].getColorTarget().getFormat().getBitsPerPixel() / 8)); - renderManager.getRenderer().readFrameBufferWithFormat(shbaker[renderOnT], shCoefRaw, shbaker[renderOnT].getColorTarget().getFormat()); + ByteBuffer shCoefRaw = BufferUtils.createByteBuffer(NUM_SH_COEFFICIENT * 1 * (shbaker.getColorTarget().getFormat().getBitsPerPixel() / 8)); + renderManager.getRenderer().readFrameBufferWithFormat(shbaker, shCoefRaw, shbaker.getColorTarget().getFormat()); shCoefRaw.rewind(); Image img = new Image(format, NUM_SH_COEFFICIENT, 1, shCoefRaw, ColorSpace.Linear); @@ -164,7 +145,6 @@ public void bakeSphericalHarmonicsCoefficients() { else if (weightAccum != c.a) { LOG.warning("SH weight is not uniform, this may cause issues."); } - } if (remapMaxValue > 0) weightAccum /= remapMaxValue; @@ -176,6 +156,7 @@ else if (weightAccum != c.a) { } EnvMapUtils.prepareShCoefs(shCoef); img.dispose(); + shbaker.dispose(); } } diff --git a/jme3-core/src/main/java/com/jme3/renderer/Caps.java b/jme3-core/src/main/java/com/jme3/renderer/Caps.java index 9361addba9..ad813db94f 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Caps.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Caps.java @@ -427,6 +427,11 @@ public enum Caps { */ DepthTexture, + /** + * Supports hardware depth texture comparison for shadow maps. + */ + TextureShadowCompare, + /** * Supports 32-bit index buffers. */ diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index ae40e05d00..d52dd8eee0 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -453,6 +453,10 @@ private void loadCapabilitiesCommon() { caps.add(Caps.DepthTexture); } + if (gl2 != null || caps.contains(Caps.OpenGLES30)) { + caps.add(Caps.TextureShadowCompare); + } + if (caps.contains(Caps.OpenGL20) || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL) || hasExtension("GL_OES_depth24")) { caps.add(Caps.Depth24); @@ -2673,7 +2677,7 @@ && isMipmapGenerationSupported(image.getFormat(), } ShadowCompareMode texCompareMode = tex.getShadowCompareMode(); - if ( (gl2 != null || caps.contains(Caps.OpenGLES30)) && curState.shadowCompareMode != texCompareMode) { + if (caps.contains(Caps.TextureShadowCompare) && curState.shadowCompareMode != texCompareMode) { bindTextureAndUnit(target, image, unit); if (texCompareMode != ShadowCompareMode.Off) { gl.glTexParameteri(target, GL2.GL_TEXTURE_COMPARE_MODE, GL2.GL_COMPARE_REF_TO_TEXTURE); diff --git a/jme3-core/src/main/java/com/jme3/util/MaterialDebugAppState.java b/jme3-core/src/main/java/com/jme3/util/MaterialDebugAppState.java index 62d4c6c5de..7f0a81d94a 100644 --- a/jme3-core/src/main/java/com/jme3/util/MaterialDebugAppState.java +++ b/jme3-core/src/main/java/com/jme3/util/MaterialDebugAppState.java @@ -377,16 +377,23 @@ public void init() { file = new File(url.getFile()); fileLastM = file.lastModified(); - } catch (NoSuchFieldException - | SecurityException + } catch (NoSuchFieldException ex) { + Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.FINE, + "Material hot reload disabled for {0}; asset URL is not reflectively available.", + fileName); + } catch (SecurityException | IllegalArgumentException | IllegalAccessException ex) { - Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, null, ex); + Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.FINE, + "Material hot reload disabled for " + fileName, ex); } } } public boolean shouldFire() { + if (file == null || fileLastM == null) { + return false; + } if (file.lastModified() != fileLastM) { fileLastM = file.lastModified(); return true; diff --git a/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.frag b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.frag index 220c6ad4ba..812d9599a8 100644 --- a/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.frag +++ b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.frag @@ -15,11 +15,6 @@ in vec3 LocalPos; uniform samplerCube m_Texture; -#ifdef SH_COEF - uniform sampler2D m_ShCoef; -#endif -uniform vec2 m_Resolution; -uniform int m_FaceId; const float sqrtPi = sqrt(PI); const float sqrt3Pi = sqrt(3. / PI); @@ -30,80 +25,6 @@ const float sqrt15Pi = sqrt(15. / PI); uniform float m_RemapMaxValue; #endif - -vec3 getVectorFromCubemapFaceTexCoord(float x, float y, float mapSize, int face) { - float u; - float v; - - /* transform from [0..res - 1] to [- (1 - 1 / res) .. (1 - 1 / res)] - * (+ 0.5f is for texel center addressing) */ - u = (2.0 * (x + 0.5) / mapSize) - 1.0; - v = (2.0 * (y + 0.5) / mapSize) - 1.0; - - - // Warp texel centers in the proximity of the edges. - float warpDenom = max(mapSize - 1.0, 1.0); - float a = (mapSize * mapSize) / (warpDenom * warpDenom * warpDenom); - - u = a * u * u * u + u; - v = a * v * v * v + v; - //compute vector depending on the face - // Code from Nvtt : https://github.com/castano/nvidia-texture-tools/blob/master/src/nvtt/CubeSurface.cpp#L101 - vec3 o =vec3(0); - switch(face) { - case 0: - o= normalize(vec3(1., -v, -u)); - break; - case 1: - o= normalize(vec3(-1., -v, u)); - break; - case 2: - o= normalize(vec3(u, 1., v)); - break; - case 3: - o= normalize(vec3(u, -1., -v)); - break; - case 4: - o= normalize(vec3(u, -v, 1.)); - break; - case 5: - o= normalize(vec3(-u, -v, -1.)); - break; - } - - return o; -} - -float atan2(in float y, in float x) { - bool s = (abs(x) > abs(y)); - return mix(PI / 2.0 - atan(x, y), atan(y, x), s); -} - -float areaElement(float x, float y) { - return atan2(x * y, sqrt(x * x + y * y + 1.)); -} - -float getSolidAngleAndVector(float x, float y, float mapSize, int face, out vec3 store) { - /* transform from [0..res - 1] to [- (1 - 1 / res) .. (1 - 1 / res)] - (+ 0.5f is for texel center addressing) */ - float u = (2.0 * (x + 0.5) / mapSize) - 1.0; - float v = (2.0 * (y + 0.5) / mapSize) - 1.0; - - store = getVectorFromCubemapFaceTexCoord(x, y, mapSize, face); - - /* Solid angle weight approximation : - * U and V are the -1..1 texture coordinate on the current face. - * Get projected area for this texel */ - float x0, y0, x1, y1; - float invRes = 1.0 / mapSize; - x0 = u - invRes; - y0 = v - invRes; - x1 = u + invRes; - y1 = v + invRes; - - return areaElement(x0, y0) - areaElement(x0, y1) - areaElement(x1, y0) + areaElement(x1, y1); -} - void evalShBasis(vec3 texelVect, int i, out float shDir) { float xV = texelVect.x; float yV = texelVect.y; @@ -124,56 +45,29 @@ void evalShBasis(vec3 texelVect, int i, out float shDir) { else shDir = sqrt15Pi * (x2 - y2) / 4.; } -vec3 pixelFaceToV(int faceId, float pixelX, float pixelY, float cubeMapSize) { - vec2 normalizedCoords = vec2((2.0 * pixelX + 1.0) / cubeMapSize, (2.0 * pixelY + 1.0) / cubeMapSize); - - vec3 direction; - if(faceId == 0) { - direction = vec3(1.0, -normalizedCoords.y, -normalizedCoords.x); - } else if(faceId == 1) { - direction = vec3(-1.0, -normalizedCoords.y, normalizedCoords.x); - } else if(faceId == 2) { - direction = vec3(normalizedCoords.x, 1.0, normalizedCoords.y); - } else if(faceId == 3) { - direction = vec3(normalizedCoords.x, -1.0, -normalizedCoords.y); - } else if(faceId == 4) { - direction = vec3(normalizedCoords.x, -normalizedCoords.y, 1.0); - } else if(faceId == 5) { - direction = vec3(-normalizedCoords.x, -normalizedCoords.y, -1.0); - } - - return normalize(direction); -} - void sphKernel() { - int width = int(m_Resolution.x); - int height = int(m_Resolution.y); vec3 texelVect=vec3(0); float shDir=0.; - float weight=0.; vec4 color=vec4(0); int i=int(gl_FragCoord.x); - #ifdef SH_COEF - vec4 r=texelFetch(m_ShCoef, ivec2(i, 0), 0); - vec3 shCoef=r.rgb; - float weightAccum = r.a; - #else - vec3 shCoef=vec3(0.0); - float weightAccum = 0.0; - #endif - - for(int y = 0; y < height; y++) { - for(int x = 0; x < width; x++) { - weight = getSolidAngleAndVector(float(x), float(y), float(width), m_FaceId, texelVect); - evalShBasis(texelVect, i, shDir); - color = texture(m_Texture, texelVect); - shCoef.x = (shCoef.x + color.r * shDir * weight); - shCoef.y = (shCoef.y + color.g * shDir * weight); - shCoef.z = (shCoef.z + color.b * shDir * weight); - weightAccum += weight; - } + vec3 shCoef=vec3(0.0); + float weightAccum = 0.0; + + const uint SAMPLE_COUNT = 4096u; + for(uint sampleIndex = 0u; sampleIndex < SAMPLE_COUNT; sampleIndex++) { + vec4 xi = Hammersley(sampleIndex, SAMPLE_COUNT); + float z = 1.0 - 2.0 * xi.x; + float r = sqrt(max(0.0, 1.0 - z * z)); + float phi = 2.0 * PI * xi.y; + texelVect = vec3(r * cos(phi), z, r * sin(phi)); + evalShBasis(texelVect, i, shDir); + color = texture(m_Texture, texelVect); + shCoef.x = (shCoef.x + color.r * shDir); + shCoef.y = (shCoef.y + color.g * shDir); + shCoef.z = (shCoef.z + color.b * shDir); + weightAccum += 1.0; } @@ -189,4 +83,4 @@ void sphKernel() { void main() { sphKernel(); -} \ No newline at end of file +} diff --git a/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.j3md b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.j3md index eaafd2e108..be4f7c8b30 100644 --- a/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.j3md +++ b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.j3md @@ -3,9 +3,6 @@ MaterialDef IBLSphH { MaterialParameters { Int BoundDrawBuffer TextureCubeMap Texture -LINEAR - Int FaceId : 0 - Texture2D ShCoef -LINEAR - Vector2 Resolution Float RemapMaxValue } @@ -27,8 +24,7 @@ MaterialDef IBLSphH { Defines { BOUND_DRAW_BUFFER: BoundDrawBuffer REMAP_MAX_VALUE: RemapMaxValue - SH_COEF: ShCoef } } -} \ No newline at end of file +} diff --git a/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java index 0f24697cae..fbb631aa34 100644 --- a/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java +++ b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java @@ -97,7 +97,6 @@ public void simpleInitApp() { updateMaterial(); - } @@ -135,4 +134,5 @@ private void updateMaterial() { System.out.println( "Tank material -> metallic: " + metallic + ", roughness: " + roughness + " (N/P)"); } + } diff --git a/jme3-ios-examples/build.gradle b/jme3-ios-examples/build.gradle new file mode 100644 index 0000000000..b2e6554182 --- /dev/null +++ b/jme3-ios-examples/build.gradle @@ -0,0 +1,385 @@ +buildscript { + repositories { + exclusiveContent { + forRepositories( + maven { + name = 'CentralSnapshots' + url = uri('https://central.sonatype.com/repository/maven-snapshots/') + mavenContent { + snapshotsOnly() + } + } + ) + filter { + includeModule('org.ngengine', 'libjglios-gradle-plugin') + } + } + gradlePluginPortal() + mavenCentral() + } + dependencies { + classpath 'org.ngengine:libjglios-gradle-plugin:0.1.0-SNAPSHOT' + } +} + +import groovy.json.JsonOutput +import java.io.DataInputStream +import java.util.zip.ZipFile + +apply plugin: 'org.ngengine.libjglios' + +description = 'iOS libJGLIOS launcher for jme3-examples.' + +repositories { + exclusiveContent { + forRepositories( + maven { + name = 'CentralSnapshots' + url = uri('https://central.sonatype.com/repository/maven-snapshots/') + mavenContent { + snapshotsOnly() + } + } + ) + filter { + includeModule('org.ngengine', 'libjglios-core-ios') + includeModule('org.ngengine', 'libjglios-gles-ios') + includeModule('org.ngengine', 'libjglios-sdl3-ios') + includeModule('org.ngengine', 'libjglios-openal-ios') + includeModule('org.ngengine', 'libjglios-angle-ios') + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +def examplesJar = project(':jme3-examples').tasks.named('jar') +def generateExamplesTestChooserClassList = project(':jme3-examples').tasks.named('generateTestChooserClassList') +def generatedExamplesTestChooserResourcesDir = project(':jme3-examples').layout.buildDirectory.dir('generated/testchooser/resources') +def examplesTestClassListFile = generatedExamplesTestChooserResourcesDir.map { it.file('jme3test/test-classes.txt') } +def iosChooserLauncherClass = 'jme3test.ios.IosTestChooserLauncher' +def requestedExampleClass = findProperty('example')?.toString()?.trim() +def exampleClass = requestedExampleClass ?: iosChooserLauncherClass +def iosExampleClassesDir = layout.buildDirectory.dir('ios-example-classes') +def iosInitialExampleSourceDir = layout.buildDirectory.dir('generated/ios-initial-example/sources') +def iosNativeImageMetadataDir = layout.buildDirectory.dir('generated/ios-native-image-metadata/resources') +def iosChooserExcludedPrefixes = [ + 'jme3test.awt.', + 'jme3test.bullet.', + 'jme3test.niftygui.', + 'jme3test.opencl.', + 'jme3test.terrain.' +] +def iosChooserExcludedNames = [ + 'jme3test.app.TestChangeAppIcon', + 'jme3test.app.TestContextRestart', + 'jme3test.app.TestMonitorApp', + 'jme3test.app.TestResizableApp', + 'jme3test.asset.TestOnlineJar', + 'jme3test.audio.TestAudioDeviceDisconnect' +] +def isIosChooserClass = { String className -> + !iosChooserExcludedPrefixes.any { className.startsWith(it) } + && !iosChooserExcludedNames.contains(className) + && !className.contains('Jogl') + && !className.contains('Lwjgl') +} +def readExamplesTestClassList = { + def file = examplesTestClassListFile.get().asFile + if (!file.exists()) { + return [] + } + file.readLines('UTF-8') + .collect { it.trim() } + .findAll { it } + .findAll { isIosChooserClass(it) } +} +def selectedIosExampleClasses = { + requestedExampleClass ? [requestedExampleClass] : readExamplesTestClassList() +} +def javaStringLiteral = { String value -> + if (value == null) { + return 'null' + } + '"' + value + .replace('\\', '\\\\') + .replace('"', '\\"') + .replace('\n', '\\n') + .replace('\r', '\\r') + '"' +} +def readClassReferences = { ZipFile zip, String classPath -> + def entry = zip.getEntry("${classPath}.class") + if (entry == null) { + return [] as Set + } + zip.getInputStream(entry).withCloseable { input -> + def data = new DataInputStream(input) + if (data.readInt() != (int) 0xCAFEBABE) { + return [] as Set + } + data.readUnsignedShort() + data.readUnsignedShort() + def constantPool = new Object[data.readUnsignedShort()] + for (int index = 1; index < constantPool.length; index++) { + int tag = data.readUnsignedByte() + switch (tag) { + case 1: + constantPool[index] = data.readUTF() + break + case 3: + case 4: + data.skipBytes(4) + break + case 5: + case 6: + data.skipBytes(8) + index++ + break + case 7: + case 8: + case 16: + case 19: + case 20: + constantPool[index] = [tag: tag, nameIndex: data.readUnsignedShort()] + break + case 9: + case 10: + case 11: + case 12: + case 18: + data.skipBytes(4) + break + case 15: + data.skipBytes(3) + break + case 17: + data.skipBytes(4) + break + default: + throw new GradleException("Unsupported constant-pool tag ${tag} in ${classPath}.class") + } + } + constantPool.findAll { it instanceof Map && it.tag == 7 } + .collect { constantPool[it.nameIndex] } + .findAll { it instanceof String && it.startsWith('jme3test/') && !it.startsWith('jme3test/ios/') } + .collect { it.replace('/', '.') } + .findAll { !it.contains('[') } as Set + } +} +def expandExamplesClassNames = { Collection rootClasses -> + def expanded = new LinkedHashSet(rootClasses) + def jarFile = examplesJar.flatMap { it.archiveFile }.get().asFile + new ZipFile(jarFile).withCloseable { zip -> + def availablePaths = new LinkedHashSet() + zip.entries().each { entry -> + if (entry.directory || !entry.name.endsWith('.class')) { + return + } + def classPath = entry.name.substring(0, entry.name.length() - '.class'.length()) + if (classPath.startsWith('jme3test/')) { + availablePaths.add(classPath) + } + } + + def pending = new ArrayDeque(expanded) + while (!pending.isEmpty()) { + def className = pending.removeFirst() + def rootPath = className.replace('.', '/') + availablePaths.findAll { it.startsWith("${rootPath}\$") }.each { classPath -> + def nestedClass = classPath.replace('/', '.') + if (expanded.add(nestedClass)) { + pending.add(nestedClass) + } + } + readClassReferences(zip, rootPath).each { referencedClass -> + def referencedPath = referencedClass.replace('.', '/') + if (availablePaths.contains(referencedPath) && expanded.add(referencedClass)) { + pending.add(referencedClass) + } + } + } + } + expanded as List +} +def collectRuntimeClassNamesByPrefix = { Collection prefixes -> + def classes = new LinkedHashSet() + sourceSets.main.compileClasspath.files.findAll { it.exists() }.each { file -> + if (file.isDirectory()) { + file.eachFileRecurse { classFile -> + if (!classFile.name.endsWith('.class')) { + return + } + def relativePath = file.toPath().relativize(classFile.toPath()).toString().replace(File.separatorChar, (char) '/') + def className = relativePath.substring(0, relativePath.length() - '.class'.length()).replace('/', '.') + if (prefixes.any { className.startsWith(it) } && !className.endsWith('module-info') && !className.endsWith('package-info')) { + classes.add(className) + } + } + } else if (file.name.endsWith('.jar')) { + new ZipFile(file).withCloseable { zip -> + zip.entries().each { entry -> + if (entry.directory || !entry.name.endsWith('.class')) { + return + } + def className = entry.name.substring(0, entry.name.length() - '.class'.length()).replace('/', '.') + if (prefixes.any { className.startsWith(it) } && !className.endsWith('module-info') && !className.endsWith('package-info')) { + classes.add(className) + } + } + } + } + } + classes as List +} + +def prepareIosExampleClasses = tasks.register('prepareIosExampleClasses', Sync) { + dependsOn examplesJar, generateExamplesTestChooserClassList + inputs.property('exampleClass', exampleClass) + inputs.property('requestedExampleClass', requestedExampleClass ?: '') + inputs.property('iosExampleClassExpansionVersion', 'bytecode-reference-v1') + inputs.property('iosExampleIncludes', findProperty('iosExampleIncludes')?.toString() ?: '') + inputs.file(examplesTestClassListFile) + inputs.file(examplesJar.flatMap { it.archiveFile }) + from(zipTree(examplesJar.flatMap { it.archiveFile })) { + def expandedClasses + include { details -> + expandedClasses = expandedClasses ?: expandExamplesClassNames(selectedIosExampleClasses()) + expandedClasses.any { className -> details.path == "${className.replace('.', '/')}.class" } + } + def extraIncludes = findProperty('iosExampleIncludes')?.toString() + if (extraIncludes) { + extraIncludes.split(',').collect { it.trim() }.findAll { it }.each { include it } + } + } + into iosExampleClassesDir +} + +def generateIosInitialExampleSource = tasks.register('generateIosInitialExampleSource') { + def outputFile = iosInitialExampleSourceDir.map { it.file('jme3test/ios/IosInitialExample.java') } + outputs.file(outputFile) + inputs.property('requestedExampleClass', requestedExampleClass ?: '') + doLast { + def file = outputFile.get().asFile + file.parentFile.mkdirs() + file.text = """package jme3test.ios; + +final class IosInitialExample { + private static final String CLASS_NAME = ${javaStringLiteral(requestedExampleClass)}; + + private IosInitialExample() { + } + + static String className() { + return CLASS_NAME; + } +} +""" + } +} + +def generateIosNativeImageMetadata = tasks.register('generateIosNativeImageMetadata') { + dependsOn examplesJar, generateExamplesTestChooserClassList, tasks.named('prepareGraalHostNik') + def reflectConfig = iosNativeImageMetadataDir.map { + it.file('META-INF/native-image/org.jmonkeyengine/jme3-ios-testchooser/reflect-config.json') + } + outputs.file(reflectConfig) + inputs.property('exampleClass', exampleClass) + inputs.property('requestedExampleClass', requestedExampleClass ?: '') + inputs.property('iosExampleClassExpansionVersion', 'bytecode-reference-v1') + inputs.file(examplesTestClassListFile) + inputs.file(examplesJar.flatMap { it.archiveFile }) + inputs.files({ sourceSets.main.compileClasspath }) + doLast { + def exampleClasses = selectedIosExampleClasses() + def classes = (expandExamplesClassNames(exampleClasses) + + collectRuntimeClassNamesByPrefix(['com.bulletphysics.']) + + [iosChooserLauncherClass, 'jme3test.ios.IosTestChooser', 'jme3test.ios.IosInitialExample']).unique() + def metadata = classes.collect { className -> + [ + name: className, + allDeclaredConstructors: true, + allPublicConstructors: true, + allPublicMethods: true + ] + } + def outputFile = reflectConfig.get().asFile + outputFile.parentFile.mkdirs() + outputFile.text = JsonOutput.prettyPrint(JsonOutput.toJson(metadata)) + System.lineSeparator() + } +} + +libJGLIOS { + mainClass = iosChooserLauncherClass + bundleId = 'org.jmonkeyengine.jme3iosexamples' + appName = 'JmeIosExamples' + simulatorDevice = (findProperty('iosSimulatorDevice') ?: 'iPhone 16').toString() +} + +sourceSets { + main { + java.srcDir iosInitialExampleSourceDir + output.dir(iosExampleClassesDir, builtBy: prepareIosExampleClasses) + resources { + srcDir iosNativeImageMetadataDir + srcDir generatedExamplesTestChooserResourcesDir + srcDir '../jme3-testdata/src/main/resources' + srcDir '../jme3-examples/src/main/resources' + } + } +} + +dependencies { + implementation project(':jme3-core') + implementation project(':jme3-ios') + implementation 'org.ngengine:libjglios-core-ios:0.1.0-SNAPSHOT' + implementation 'org.ngengine:libjglios-gles-ios:0.1.0-SNAPSHOT' + implementation 'org.ngengine:libjglios-sdl3-ios:0.1.0-SNAPSHOT' + implementation 'org.ngengine:libjglios-openal-ios:0.1.0-SNAPSHOT' + implementation 'org.ngengine:libjglios-angle-ios:0.1.0-SNAPSHOT' + implementation project(':jme3-effects') + implementation project(':jme3-jbullet') + implementation project(':jme3-jogg') + implementation project(':jme3-networking') + implementation project(':jme3-plugins') + implementation project(':jme3-plugins-json') + implementation project(':jme3-plugins-json-gson') + + if ((findProperty('iosIncludeAwtUnsafeModules') ?: 'false').toString().toBoolean()) { + implementation project(':jme3-niftygui') + implementation project(':jme3-terrain') + } +} + +tasks.named('compileJava') { + dependsOn prepareIosExampleClasses, generateIosInitialExampleSource + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + options.release = 11 +} + +tasks.named('processResources') { + dependsOn generateIosNativeImageMetadata, generateExamplesTestChooserClassList +} + +tasks.register('runIosExamples') { + group = 'verification' + description = 'Builds, installs, and launches the jme3-examples iOS app.' + dependsOn tasks.named('runIosApp') +} + +tasks.register('printIosExamplesRunHelp') { + group = 'help' + description = 'Prints how to run jme3-examples on iOS through libJGLIOS.' + doLast { + println "Run :jme3-ios-examples:runIosExamples for the iOS test chooser." + println "Run :jme3-ios-examples:runIosExamples -Pexample=jme3test.helloworld.HelloJME3 to open one example automatically through the chooser launcher." + println "runIosExamples uses an available connected device automatically, otherwise it opens the simulator." + println "Use -PiosAppTarget=simulator to force simulator, or -PiosDevice='' to force a device." + println "For device signing, add -PiosSigningIdentity, -PiosProvisioningProfile, and optionally -PiosCodesignEntitlements." + println "For examples with helper classes, add -PiosExampleIncludes='jme3test/path/**'." + println "Terrain and niftygui are excluded by default because they pull java.awt into native-image; opt in with -PiosIncludeAwtUnsafeModules=true." + } +} diff --git a/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooser.java b/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooser.java new file mode 100644 index 0000000000..2e6acd5ad5 --- /dev/null +++ b/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooser.java @@ -0,0 +1,428 @@ +package jme3test.ios; + +import com.jme3.app.SimpleApplication; +import com.jme3.font.BitmapText; +import com.jme3.input.KeyInput; +import com.jme3.input.MouseInput; +import com.jme3.input.RawInputListenerAdapter; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.input.event.KeyInputEvent; +import com.jme3.input.event.TouchEvent; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector2f; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.shape.Quad; +import com.jme3.system.JmeSystem; +import java.util.ArrayList; +import java.util.List; + +public final class IosTestChooser extends SimpleApplication implements ActionListener { + private static final String SELECT_MAPPING = "IosTestChooserSelect"; + private static final float MIN_TEXT_SIZE = 10f; + private static final long TAP_DEBOUNCE_NANOS = 180_000_000L; + private static final ColorRGBA BACKGROUND_COLOR = new ColorRGBA(0.045f, 0.060f, 0.050f, 1f); + private static final ColorRGBA EXAMPLE_BUTTON_COLOR = new ColorRGBA(0.135f, 0.305f, 0.145f, 1f); + private static final ColorRGBA ACTION_BUTTON_COLOR = new ColorRGBA(0.760f, 0.360f, 0.060f, 1f); + private static final ColorRGBA DISABLED_BUTTON_COLOR = new ColorRGBA(0.090f, 0.105f, 0.095f, 1f); + private static final ColorRGBA SEARCH_BUTTON_COLOR = new ColorRGBA(0.070f, 0.085f, 0.075f, 1f); + private static final ColorRGBA PRIMARY_TEXT_COLOR = new ColorRGBA(0.930f, 0.960f, 0.900f, 1f); + private static final ColorRGBA MUTED_TEXT_COLOR = new ColorRGBA(0.500f, 0.560f, 0.480f, 1f); + private static final ColorRGBA SEARCH_TEXT_COLOR = new ColorRGBA(0.765f, 0.910f, 0.555f, 1f); + + private final List