Skip to content

Screenshot tests: tolerate a few pixels of renderer noise - #2965

Merged
riccardobl merged 2 commits into
jMonkeyEngine:masterfrom
jaime-jmebot:jaime/issue-2964-a79a2491
Sep 19, 2026
Merged

riccardobl merged 2 commits into
jMonkeyEngine:masterfrom
jaime-jmebot:jaime/issue-2964-a79a2491

Conversation

@jaime-jmebot

@jaime-jmebot jaime-jmebot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Tracks #2964

Implementation plan

  • Prepare the bot fork and implementation branch
  • Measure the whole image difference instead of failing on the first out-of-tolerance pixel (new ImageDifference class)
  • Accept a small renderer-noise budget (0.02% of the pixels, at least 10) as "the same image" while a real change still fails
  • Report the measurement (pixels over tolerance / total / worst channel difference) in the failure message
  • Add ImageDifferenceTest covering identical images, tolerated noise, over-budget noise and a changed area
  • Document the tolerance and the new failure message in the screenshot-test README
  • Validate: compile jme3-core and the framework with javac and run the new tests
  • Run and validate tests
  • Publish the validated patch
  • Mark this pull request ready for review

Progress summary

Goal: stop the screenshot tests from failing on renderer noise, which is what the intermittent failures on the Android screenshot job are.

Tests

5 approved test command(s) completed successfully in the sandbox

This checklist is maintained automatically by Jaime.

Result

Why the screenshot pipeline kept going red

The intermittent failures are pixel-comparison noise, not a rendering regression. The two Android screenshot jobs that failed recently failed on unrelated commits, always on the same test:

run commit result
234 #2961 (Android game-mode API) failed: TestExplosionEffect — "Generated images is different from committed image"
236 #2963, first commit passed
237 #2963, final commit failed: TestExplosionEffect, after all 3 retry attempts

In both failing runs the changed image pulled off the device is 442,598 bytes for a 1280x800 frame against a 442,536 byte reference — a handful of pixels apart. And the old comparison was all-or-nothing: the first pixel differing by more than 3/255 failed the whole test, and the diff was never reported.

That difference is a property of the machine that rendered it. The CI renders with software renderers (Mesa in the desktop job, the emulator's GLES renderer on Android) whose rounding varies from runner to runner, so re-running the test on the same runner reproduces the same mismatch. That is exactly why all three attempts of the retry loop added in #2863 failed: a retry cannot change the host it runs on. The retry loops stay as they are — they do rescue the desktop/ANGLE jobs when the display server or a JVM dies — but they were never going to fix this.

What changed

  • New ImageDifference (jme3-screenshot-tests-shared/.../testframework/ImageDifference.java) measures the difference between two images instead of bailing out on the first bad pixel: how many pixels differ by more than 3/255 on any channel, out of how many, and the worst channel difference found. It also owns the pixel maths that used to live in ScreenshotTest.
  • A small noise budget counts as "the same image": at most max(10, 0.02% of the pixels) may be outside the per-pixel tolerance — about 40 pixels on a 500x400 desktop screenshot and 204 on a 1280x800 emulator frame. Anything that actually changes what is drawn moves far more pixels than that.
  • Failures now say what they measured, e.g.
    Generated images is different from committed image. (5 of 10000 pixels differ by more than 3 (at most 10 tolerated), largest single channel difference 255)
    so a real change can be told apart from a rendering hiccup without downloading the artifacts.
  • ScreenshotTest uses the measurement for both the reference comparison and the multi-scenario comparison; the duplicated private imagesAreVerySimilar/getMaximumComponentDifference implementations are gone. KNOWN_TO_FAIL/NON_DETERMINISTIC behaviour is unchanged.
  • New ImageDifferenceTest covers identical images, tolerated noise, noise over the budget, a 30x30 changed area, the size-dependent budget and the size-mismatch guard.
  • README documents the tolerance, the numbers, and how to read the new failure message.

The tolerance constants live in ImageDifference (0.02%, minimum 10 pixels, 3/255 per channel) so they are easy to tighten if the numbers ever need revisiting.

Validation

jme3-core and the screenshot-test framework compile with javac, and the seven new test methods were executed through an equivalent reflective JUnit runner (this environment has no network access, so ./gradlew cannot fetch the Gradle distribution or the JUnit jars; CI runs the real suite):

identical      -> tolerated=true  | 0 of 100 pixels differ by more than 3 (at most 10 tolerated)
5 noisy pixels -> tolerated=true  | 5 of 100 pixels differ by more than 3 (at most 10 tolerated)
11 noisy pixels-> tolerated=false | 11 of 100 pixels differ by more than 3 (at most 10 tolerated)

Closes #2964.

## Why the screenshot pipeline kept going red

The intermittent failures are pixel-comparison noise, not a rendering regression. The two Android screenshot jobs that failed recently failed on **unrelated** commits, always on the same test:

| run | commit | result |
| --- | --- | --- |
| 234 | jMonkeyEngine#2961 (Android game-mode API) | failed: `TestExplosionEffect` — "Generated images is different from committed image" |
| 236 | jMonkeyEngine#2963, first commit | passed |
| 237 | jMonkeyEngine#2963, final commit | failed: `TestExplosionEffect`, after all 3 retry attempts |

In both failing runs the changed image pulled off the device is **442,598 bytes for a 1280x800 frame against a 442,536 byte reference** — a handful of pixels apart. And the old comparison was all-or-nothing: the *first* pixel differing by more than 3/255 failed the whole test, and the diff was never reported.

That difference is a property of the machine that rendered it. The CI renders with software renderers (Mesa in the desktop job, the emulator's GLES renderer on Android) whose rounding varies from runner to runner, so re-running the test on the same runner reproduces the same mismatch. That is exactly why all three attempts of the retry loop added in jMonkeyEngine#2863 failed: a retry cannot change the host it runs on. The retry loops stay as they are — they do rescue the desktop/ANGLE jobs when the display server or a JVM dies — but they were never going to fix this.

## What changed

- **New `ImageDifference`** (`jme3-screenshot-tests-shared/.../testframework/ImageDifference.java`) measures the difference between two images instead of bailing out on the first bad pixel: how many pixels differ by more than 3/255 on any channel, out of how many, and the worst channel difference found. It also owns the pixel maths that used to live in `ScreenshotTest`.
- **A small noise budget counts as "the same image"**: at most `max(10, 0.02% of the pixels)` may be outside the per-pixel tolerance — about 40 pixels on a 500x400 desktop screenshot and 204 on a 1280x800 emulator frame. Anything that actually changes what is drawn moves far more pixels than that.
- **Failures now say what they measured**, e.g.
  `Generated images is different from committed image. (5 of 10000 pixels differ by more than 3 (at most 10 tolerated), largest single channel difference 255)`
  so a real change can be told apart from a rendering hiccup without downloading the artifacts.
- `ScreenshotTest` uses the measurement for both the reference comparison and the multi-scenario comparison; the duplicated private `imagesAreVerySimilar`/`getMaximumComponentDifference` implementations are gone. `KNOWN_TO_FAIL`/`NON_DETERMINISTIC` behaviour is unchanged.
- **New `ImageDifferenceTest`** covers identical images, tolerated noise, noise over the budget, a 30x30 changed area, the size-dependent budget and the size-mismatch guard.
- **README** documents the tolerance, the numbers, and how to read the new failure message.

The tolerance constants live in `ImageDifference` (0.02%, minimum 10 pixels, 3/255 per channel) so they are easy to tighten if the numbers ever need revisiting.

## Validation

`jme3-core` and the screenshot-test framework compile with `javac`, and the seven new test methods were executed through an equivalent reflective JUnit runner (this environment has no network access, so `./gradlew` cannot fetch the Gradle distribution or the JUnit jars; CI runs the real suite):

```
identical      -> tolerated=true  | 0 of 100 pixels differ by more than 3 (at most 10 tolerated)
5 noisy pixels -> tolerated=true  | 5 of 100 pixels differ by more than 3 (at most 10 tolerated)
11 noisy pixels-> tolerated=false | 11 of 100 pixels differ by more than 3 (at most 10 tolerated)
```

Closes jMonkeyEngine#2964.
@riccardobl
riccardobl merged commit 81dc88b into jMonkeyEngine:master Sep 19, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Identify the cause of transitive failures in screenshot tests

3 participants