JDK-8094374 : Rendering Artifacts with Groups and Transformations
  • Type: Bug
  • Component: javafx
  • Sub-Component: scenegraph
  • Affected Version: 7u25,7u40,8
  • Priority: P2
  • Status: Closed
  • Resolution: Fixed
  • Submitted: 2013-08-16
  • Updated: 2015-06-18
  • Resolved: 2013-08-23
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 7 JDK 8
7u60Fixed 8Fixed
Related Reports
Duplicate :  
Description
tested with JDK 1.7_15 and 1.7_25. This does not appear in JavaFX 8 (lombard) so far. This bug is related to RT-23351, however, the fix for this issue did not fix it and the workaround does seem to fix it in our application code, but not in the SSCCE that follows. This should have been fixed by T-24091 and needs to be backported to JavaFX 2.x

consider the following application:

== RenderingArtifactTest  == 

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;

public class RenderingArtifactTest extends Application {

  int step = 0;

  public static void main(String[] args) {
    launch(args);
  }

  @Override
  public void start(Stage primaryStage) {

    Group root = new Group();
    final Group body = new Group();

    // rectangle that is bound to the bounds in parent of the body rectangle (similar to what ScenicView overlay does)
    final Rectangle bipRectangle = new Rectangle(500, 500, 90, 90);
    bipRectangle.setOpacity(0.5);
    bipRectangle.setFill(Color.YELLOW);

    // this is necessary to reproduce the bug. setting the bipRectangle bounds without a listener, for example in the buttons onActions, won't do it.
    body.boundsInParentProperty().addListener(new ChangeListener<Bounds>() {
      @Override
      public void changed(ObservableValue<? extends Bounds> observableValue, Bounds bounds, Bounds bounds2) {
        bipRectangle.setX(bounds2.getMinX());
        bipRectangle.setY(bounds2.getMinY());
        bipRectangle.setWidth(bounds2.getWidth());
        bipRectangle.setHeight(bounds2.getHeight());
      }
    });

    root.getChildren().addAll(body, bipRectangle);

    int rectWidth = 40;
    int rectheight = 40;

    int offset = 500;

    int rectLowerX = offset;
    int rectLowerY = offset;
    int rectUpperX = offset + 50;
    int rectUpperY = offset + 50;

    // 2 rectangles are the contents of the body-group
    final Rectangle breaker1 = new Rectangle(rectLowerX, rectLowerY, rectWidth, rectheight);
    final Transform translateTransformation1 = Transform.affine(1, 0, 0, 1, -70, -70);
    final Transform translateTransformation1Inv = Transform.affine(1, 0, 0, 1, 70, 70);

    final Rectangle breaker2 = new Rectangle(rectUpperX, rectUpperY, rectWidth, rectheight);
    final Transform translateTransformation4 = Transform.affine(1, 0, 0, 1, 70, 70);
    final Transform translateTransformation4Inv = Transform.affine(1, 0, 0, 1, -70, -70);

    body.getChildren().addAll(breaker1, breaker2);

    HBox buttons = new HBox(10);
    Button stepButton = new Button("step");
    Button addScaleDownTransform = new Button("scale down");
    Button addScaleUpTransform = new Button("scale up");
    Button moveAwayButton = new Button("move away");
    Button moveCloserButton = new Button("move closer");
    buttons.getChildren().addAll(stepButton, addScaleDownTransform, addScaleUpTransform, moveAwayButton, moveCloserButton);
    root.getChildren().add(buttons);

    /*
     button that performs the necessary steps to reproduce the bug.
     to reproduce the bug by the other buttons manually:
     1) scale down
     2) move away (make the group bigger)
     3) scale down again
      -> rendering artifacts appear
     */
    stepButton.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent actionEvent) {
        switch (step) {
          case 0:
            body.getTransforms().add(new Scale(.5, .5));
            break;
          case 1:
            breaker1.getTransforms().add(translateTransformation1);
            breaker1.getTransforms().add(translateTransformation1);
            breaker2.getTransforms().add(translateTransformation4);
            breaker2.getTransforms().add(translateTransformation4);
            break;
          case 2:
            body.getTransforms().add(new Scale(.5, .5));
            break;
        }
        step++;
      }
    });

    // adds a scale transform to the group the makes it smaller
    addScaleDownTransform.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent actionEvent) {
        body.getTransforms().add(new Scale(.5, .5));
      }
    });

    // adds the inverse transform of the scaling transform to the group
    addScaleUpTransform.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent actionEvent) {
        body.getTransforms().add(new Scale(2, 2));
      }
    });

    // adds a translate transform to the rectangles the moves them away from each other (thus making the group bigger)
    moveAwayButton.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent actionEvent) {

        breaker1.getTransforms().add(translateTransformation1);
        breaker2.getTransforms().add(translateTransformation4);
      }
    });

    // adds a transform to the rectangles that is the inverse to the other translate transform (thus making the group smaller)
    moveCloserButton.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent actionEvent) {

        breaker1.getTransforms().add(translateTransformation1Inv);
        breaker2.getTransforms().add(translateTransformation4Inv);
      }
    });

    Scene scene = new Scene(root, 600, 600, Color.WHITE);
    primaryStage.setScene(scene);
    primaryStage.show();
  }
}


This demo shows that, when transformations are applied to a group and another is applied to its children, this leads to weird behavior with stuff that is bound to the bounds of the group.
The demo actually does what ScenicView does: It displays a rectangle in front of a group that is bound to the groups bounds.

To reproduce the rendering artifacts, hit the scale down button once, then the move away button a few times, and then scale down again. the rectangles will still be visible until a repaint is forced, for example, when the window is resized. Alternativeley the step button reproduces the bug by itself.

Note that this doesn't stop only when trying to implement something like zoom, but also when stuff is displayed this way and only translate transformations are applied, like in our application.


Comments
Verified on 8.0b104
27-08-2013

Yes, I can do that. Please send me the files at sebastian.rheinnecker@yworks.com
26-08-2013

Sebastian, it would be good to test whether this exact issue was causing rendering artifacts in your application. I can send you a fixed Node.class file to replace in 2.2.40 jfxrt.jar. Will you have time for testing?
23-08-2013

The problem is reproducible even with JavaFX 8 if the body group transformation is little more complex. If the following line body.getTransforms().add(new Rotate(1)); is added in the start method, the rendering artifacts are visible even with JavaFX 8. The problem is that a child node doesn't notify its parent of bounds change if its cached transformed bounds are invalid. But in the case the parent transformation is complex enough, the parent won't need the child's cached transformed bounds and so they will remain invalid and cause the required notification to be skipped.
22-08-2013

I downloaded 7u40 and run my application above. By pressing the "step" button and i was still able to produce said rendering artifacts. This issue is not fixed by RT-23351 or RT-24091, respectively. Note that using the "move away" button this does not happen with 7u40 anymore (unlike in 7u25), but if you replace the onAction of the "move away" button with moveAwayButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent actionEvent) { breaker1.getTransforms().add(translateTransformation1); breaker2.getTransforms().add(translateTransformation4); breaker1.getTransforms().add(translateTransformation1); breaker2.getTransforms().add(translateTransformation4); } }); then it fails again. And again, this does not happen in JavaFX 8.
19-08-2013

I just ran the test program and it fails as expected with 7u25 and works with 7u40. So I believe this is a duplicate of RT-23351.
16-08-2013

The backport of RT-23351 to 2.2.40, which is part of the soon-to-be-released JDK 7u40, is tracked by RT-24091. I double-checked, and verified that the changeset for RT-23351 is in the 2.2.40 master repository. Can you test your application with an early access release of 7u40?
16-08-2013