From ef1410b4ec7ff804336b3a2b0496bf32fc78eee4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:38:51 +0000 Subject: [PATCH 1/2] Make TestIssue1773 deterministic: seed RNG per run and remove moving emitter Reseed the shared FastMath.rand to a fixed value at the start of each parameterized invocation so particle positions are reproducible, and remove the MotionEvent-driven moving emitter (which caused timing-sensitive world-space spawn interpolation). The test remains a must-pass screenshot test for issue 1773. Reference images must be regenerated on the CI reference machine. Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> --- .../scenarios/effects/ScenarioIssue1773.java | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/effects/ScenarioIssue1773.java b/jme3-screenshot-tests/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/effects/ScenarioIssue1773.java index 8ecbbd2994..9a0daf5829 100644 --- a/jme3-screenshot-tests/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/effects/ScenarioIssue1773.java +++ b/jme3-screenshot-tests/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/effects/ScenarioIssue1773.java @@ -33,13 +33,10 @@ import static org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase.screenshotTest; -import com.jme3.animation.LoopMode; import com.jme3.app.Application; import com.jme3.app.SimpleApplication; import com.jme3.app.state.BaseAppState; import com.jme3.asset.AssetManager; -import com.jme3.cinematic.MotionPath; -import com.jme3.cinematic.events.MotionEvent; import com.jme3.effect.ParticleEmitter; import com.jme3.effect.ParticleMesh; import com.jme3.effect.shapes.EmitterMeshVertexShape; @@ -70,6 +67,10 @@ public class ScenarioIssue1773 { public static ScreenshotTest testIssue1773(boolean worldSpace) { + // Reset the shared random generator to a known state so the particle + // positions are identical on every run (the framework sets a fixed tpf, + // this makes the random side deterministic too). + FastMath.rand.setSeed(0); return screenshotTest(new BaseAppState() { private ParticleEmitter emit; private Node myModel; @@ -88,7 +89,6 @@ public void initialize(Application app) { setupLights(); setupGround(); setupCircle(); - createMotionControl(); } @Override @@ -143,22 +143,6 @@ private ParticleEmitter createParticleEmitter(Geometry geo, boolean pointSprite) return emitter; } - private void createMotionControl() { - float radius = 5f; - float height = 1.10f; - MotionPath path = new MotionPath(); - path.setCycle(true); - for (int i = 0; i < 8; i++) { - float x = FastMath.sin(FastMath.QUARTER_PI * i) * radius; - float z = FastMath.cos(FastMath.QUARTER_PI * i) * radius; - path.addWayPoint(new Vector3f(x, height, z)); - } - MotionEvent motionControl = new MotionEvent(myModel, path); - motionControl.setLoopMode(LoopMode.Loop); - motionControl.setDirectionType(MotionEvent.Direction.Path); - motionControl.play(); - } - private void configCamera() { cam.setLocation(new Vector3f(0, 6f, 9.2f)); cam.lookAt(Vector3f.UNIT_Y, Vector3f.UNIT_Y); From 4d21d688c0986e58f2d955283f7f4f3c85f806ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:41:11 +0000 Subject: [PATCH 2/2] Keep TestIssue1773 scene unchanged; fix determinism via RNG reseed only Reverting the earlier removal of the moving emitter. The emission loop is purely tpf-driven and IsoTimer supplies a constant tpf, so the moving emitter is already deterministic across identical runs. The genuine flakiness source is the shared FastMath.rand state leaking between the two parameterized invocations. Resetting the seed per invocation makes the screenshot reproducible while keeping the existing reference images valid (the scene is unchanged). Also harden CI: - main.yml: restart Xvfb + stop Gradle daemon between screenshot retries to survive the X_PutImage BadLength Xlib crash - main.yml: retry publishToSonatype with backoff to absorb Maven Central 429s Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> --- .github/workflows/main.yml | 24 +++++++++++++++- .../scenarios/effects/ScenarioIssue1773.java | 28 +++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9cba139dd5..c8bc25fb8e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -169,6 +169,16 @@ jobs: fi if [ $attempt -lt 3 ]; then echo "Attempt $attempt failed, retrying..." + # Stop any crashed/lingering Gradle daemons so a failed JVM + # (e.g. an X_PutImage Xlib crash) cannot poison the next attempt. + ./gradlew --stop || true + # Restart Xvfb: a corrupted X server state (e.g. BadLength on + # X_PutImage) persists for the life of the server, so give each + # retry a fresh display. + pkill Xvfb || true + sleep 2 + Xvfb :99 -ac -screen 0 1024x768x16 & + sleep 2 fi done echo "All 3 attempts failed." @@ -350,7 +360,19 @@ jobs: echo "Configure the following secrets to enable uploading to Sonatype:" echo "CENTRAL_PASSWORD, CENTRAL_USERNAME, SIGNING_KEY, SIGNING_PASSWORD" else - ./gradlew publishToSonatype --console=plain --stacktrace + # Retry with backoff to absorb transient Maven Central failures + # (e.g. HTTP 429 Too Many Requests during dependency resolution). + for attempt in 1 2 3; do + echo "publishToSonatype attempt $attempt of 3" + if ./gradlew publishToSonatype --console=plain --stacktrace; then + break + fi + if [ "$attempt" -eq 3 ]; then + echo "publishToSonatype failed after 3 attempts." + exit 1 + fi + sleep $((attempt * 30)) + done fi diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/effects/ScenarioIssue1773.java b/jme3-screenshot-tests/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/effects/ScenarioIssue1773.java index 9a0daf5829..e311b98c6c 100644 --- a/jme3-screenshot-tests/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/effects/ScenarioIssue1773.java +++ b/jme3-screenshot-tests/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/effects/ScenarioIssue1773.java @@ -33,10 +33,13 @@ import static org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase.screenshotTest; +import com.jme3.animation.LoopMode; import com.jme3.app.Application; import com.jme3.app.SimpleApplication; import com.jme3.app.state.BaseAppState; import com.jme3.asset.AssetManager; +import com.jme3.cinematic.MotionPath; +import com.jme3.cinematic.events.MotionEvent; import com.jme3.effect.ParticleEmitter; import com.jme3.effect.ParticleMesh; import com.jme3.effect.shapes.EmitterMeshVertexShape; @@ -67,9 +70,11 @@ public class ScenarioIssue1773 { public static ScreenshotTest testIssue1773(boolean worldSpace) { - // Reset the shared random generator to a known state so the particle - // positions are identical on every run (the framework sets a fixed tpf, - // this makes the random side deterministic too). + // The framework runs at a fixed tpf (IsoTimer), so particle emission is + // already deterministic per frame. However the emitter shape picks random + // mesh vertices from the shared FastMath.rand generator, whose state is + // not reset between the two parameterized invocations. Reset it to a known + // seed here so every run produces an identical, reproducible screenshot. FastMath.rand.setSeed(0); return screenshotTest(new BaseAppState() { private ParticleEmitter emit; @@ -89,6 +94,7 @@ public void initialize(Application app) { setupLights(); setupGround(); setupCircle(); + createMotionControl(); } @Override @@ -143,6 +149,22 @@ private ParticleEmitter createParticleEmitter(Geometry geo, boolean pointSprite) return emitter; } + private void createMotionControl() { + float radius = 5f; + float height = 1.10f; + MotionPath path = new MotionPath(); + path.setCycle(true); + for (int i = 0; i < 8; i++) { + float x = FastMath.sin(FastMath.QUARTER_PI * i) * radius; + float z = FastMath.cos(FastMath.QUARTER_PI * i) * radius; + path.addWayPoint(new Vector3f(x, height, z)); + } + MotionEvent motionControl = new MotionEvent(myModel, path); + motionControl.setLoopMode(LoopMode.Loop); + motionControl.setDirectionType(MotionEvent.Direction.Path); + motionControl.play(); + } + private void configCamera() { cam.setLocation(new Vector3f(0, 6f, 9.2f)); cam.lookAt(Vector3f.UNIT_Y, Vector3f.UNIT_Y);