JDK-8092373 : Improve Path rendering performance
  • Type: Enhancement
  • Component: javafx
  • Sub-Component: graphics
  • Affected Version: fx2.1
  • Priority: P3
  • Status: Closed
  • Resolution: Duplicate
  • Submitted: 2012-03-15
  • Updated: 2021-11-08
  • Resolved: 2021-11-08
Related Reports
Blocks :  
Duplicate :  
Duplicate :  
Relates :  
Description
I am developing some tests to achieve de loading of geographic information from a shapefile using JavaFX 2.0. I read the shapefile data using the feature facilites provided by GeoTools API, and rendering these data using JavaFX Path class (with MoveTo and LineTo). I have also implemented a zoom operation associated to mousePressed event, that simply changes the scaleXProperty and scaleYPropety in the map.

I have noticed that if the map is quite complex, the zoom operation is very slow. For example, with a map with 20000 lines the performance is very poor when making the zoom this way. The result is almos the same if I use Polylines to represent the map info.

I have made some tests with the scaling operation and the thing is that the problems start when I use Path objects. For example, If I render a scene with 40000 rectangles (object Rectangle) the scaling works fine, but if a render the same scene using 40000 lines (in paths) the performance falls down dramatically. Is this normal?
Comments
For a bug or enhancement request, a resolution of fixed is only used when there is a commit pushed with this as the bug ID. Reopening it to close as a duplicate of JDK-8177985 (the enhancement that made Marlin the default rasterizer).
08-11-2021

Marlin renderer is in action since jfx10, so this issue is outdated
07-11-2021

Is this bug still valid? As javafx 11+ relies on the Marlin renderer, its performance was greatly improved (clipping enabled): good enough ?
13-01-2021

Marlin is the new javafx shape rasterizer in 10. Of course it is faster than pisces, but software rendering is still slower than GPU shaders (rect). I think this bug should be closed and another RFE submitted about raw path rendering (gl, shaders)...
12-02-2018

Added JBS issue for improving the Path2D growth algorithm in JavaFX
08-11-2016

JDK-8169270 has been filed to add the Marlin rasterizer as a non-default option (enabled via a command line switch) to JavaFX.
05-11-2016

Here is the first Marlin-fx release: https://github.com/bourgesl/marlin-fx Will discuss soon how to contribute it ... to OpenJFX
14-10-2016

Probably javafx.scene.shape.Path or com.sun.javafx.geom.Path2D have the same growing array issue as java.awt.geom.Path2D: https://bugs.openjdk.java.net/browse/JDK-8078464
05-02-2016

Sounds very promising! Thanks! :-)
17-11-2015

The JBS issue for the Marlin rasterizer is JDK-8131760
16-11-2015

Sounds great. Do you have a link at hand regarding more information on that new rasterizer? :-)
16-11-2015

The next step would be to incorporate the new rasterizer being developed for the JDK. Marlin will be deployed in Java2D in the next couple of weeks and then we can bring it over to FX.
16-11-2015

For professional applications like CAD, solving this issue is really crucial. What's the schedule towards a final solution?
15-11-2015

Our application displays multiple complex and overlapping spectrum signatures (paths with 1000s of line segments) and we see rendering performance issues as well. It would be great if you added some rendering hints like a Join type of NONE; that would work very well for us. Of course, anything you guys can do to move Path/Polyline rendering to the hardware pipeline would be appreciated as well.
14-02-2014

All what was said above only applies to stroked paths but I have also observed that paths are equally slow if they are only filled (stroke set to null) and there is no alternative to get arround this bottleneck like with lines for stroked paths. If I understand you correctly we basically have to live with this poor performance just because someone might use a gimmick like a transparent stroke border or a fading path and for that reason you exclude all applications which urgently need a better rendering performance like GIS- or CAD-like programs. Why don't you introduce some rendering hints where the programmer can select between speed and quality like it is done for caching?
09-04-2012

So, according to the explanations provided, if we need to draw complex maps from a shapefile using JavaFX, and considering that no major optimizations regarding Paths are expected in the short-term, ¿which would be the best approach to get reasonable performance in the zoom operation? ¿working with Line objects? Thanks.
09-04-2012

Note that the Swing test program attached to RT-20857 is not identical to the FX test program. The Swing test program does not use antialiasing which costs a significant amount of time for these types of paths. Without antialiasing then the rendering of a single-pixel-wide polyline of an opaque color is a special case that can be rendered directly on the hardware. With antialiasing then the two test cases perform similarly, as expected, though the proprietary renderer we use in the JDK is faster than the open source renderer we use in FX currently - which is the target of some of the "tweaks" that I was referring to in a prior comment. The performance of our software renderer still needs some polishing to reach the levels that the JDK had, but it will never reach the performance of the GPU-implemented single line rendering performance seen in the "bunch of lines". Also, note that if you turn AA on and use the list of lines then the performance of FX is much, much greater than the performance of Swing since FX does optimize single line rendering, but Java never had that optimization (it should be backported at some point). Finally, I'll note that rendering the bunch of lines as a path has different rendering semantics than rendering them as individual line objects. If you fade them then the list of line objects will have greater opacity where they overlap since each line is rendered independently and the color contribution at the intersections is additive. But, if you fade them as a path then the coverages of every intersection are computed and rendered once so the entire figure will fade evenly. This is an example of the complexity of expressing this object as a Path that must be taken into account and prevents us from simply rendering it the same way as we render thousands of individual line requests. Note that without AA then an "opaque bunch of single pixel lines" optimization is possible, but since the edges of an AA path are "faded" in a sense, then we must render the entire path as a single software-computed pass.
06-04-2012

This cannot be normal. See my comments and attached test programs in RT-20857.
06-04-2012

This is fairly normal for apps that use arbitrary paths (whether the Path, SVGPath, Polyline, or Polygon node objects) since those paths are rendered in Software. The primitive shapes like Circle, Ellipse, Line, and Rectangle map very easily to operations that can be performed entirely on the GPU which makes them essentially cheap. It is meaningless to compare rendering of a complicated shape to the rendering of simple primitives for this reason. [Note, I had originally said that Arc was also one of the simple shapes that can be expressed directly on the GPU, but I removed it from the list since we don't currently optimize it...] There are a number of tasks on the planning board to improve path rendering, but keep in mind that these are not simple afternoon tasks, they require weeks and sometimes months of concerted effort and testing to achieve any gains. As a result, none of them has been prioritized for delivering in the 2.x update releases. Some tweak-type tasks might be done, like improving the sorting of edge lists, but those are not likely to make significant improvements such as are sought here. With respect to a Polyline or Polygon or Path, the rendering of that object compared to a list of Line objects that hits the same vertices are quite different because the path objects must join the lines together so we cannot simply perform the simplified "line" operation N times on every pair of vertices. Perhaps if we added a JOIN type of "NONE" then we could allow the developer to specify that they do not want the joins and we could achieve similar performance to using separate line objects, but until then rendering a series of line segments in a path object requires a global analysis of the path to compute the join geometry correctly that we have no way of doing in hardware (some theories, but they are far from even being prototypes and not necessarily scalable to 20000 connected lines).
05-04-2012

As an aside - please add test cases as attached files rather than inline in comments so that they are easier to download and test and do not clutter up the surrounding discussions.
05-04-2012

Please raise the priority of this issue. It is really a killer for any graphics application which uses path directly or indirectly via Polyline or Polygon (as they are internally using a path too).
05-04-2012

I have created some test code (below) to draw random lines using either Line or Path. (Mouse wheel zooms, left button drag pans, right button click resets). Zoom is adequate when using Line with 50,000 lines, but for comprable perrfomance with Path only 50 lines can be used. What is the problem with rendering of Paths? When will it be fixed? what other shapes are affected? What workarounds are available? Thanks. Jim import javafx.application.Application; import javafx.stage.Stage; import javafx.scene.Node; import javafx.scene.Group; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.Cursor; import javafx.scene.paint.Color; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.event.EventHandler; import javafx.scene.shape.Shape; import javafx.scene.shape.Path; import javafx.scene.shape.MoveTo; import javafx.scene.shape.LineTo; import javafx.scene.shape.Rectangle; import javafx.geometry.Bounds; import java.awt.Insets; import javafx.scene.shape.Line; /**** @author Jim Kay */ public class TestRandomPath extends Application { private double dragBaseX, dragBaseY; private double dragBase2X, dragBase2Y; double tX; double tY; StackPane root; Node out; double scale = 1; double oldscale = 1; int coordMinX; int coordMaxX; int coordMinY; int coordMaxY; int coordRangeX; int coordRangeY; //############################### change number of line here // 50000 Lines is OK // 50 Paths is comparable int numberLines = 50; //########################################################## double lineWidth = 1; Rectangle clipRect; Insets margin = new Insets(50,50,50,50); public static void main(String[] args) { launch(args); } @Override public void start(Stage stage) { stage.setTitle("Large Number of Lines Test"); root = new StackPane(); final Scene scene = new Scene(root, 800, 800, Color.WHITE); addMouseHandler(root, scene); stage.setScene(scene); stage.show(); coordMinX = 5000; coordMaxX = 8000; coordMinY = 3000; coordMaxY = 9000; coordRangeX = coordMaxX - coordMinX; coordRangeY = coordMaxY - coordMinY; makeRandom(); reset(root, scene); scale=0.1; zoom(root, 1); } private void addMouseHandler(final Pane root, final Scene scene) { root.setOnMousePressed(new EventHandler<MouseEvent>() { public void handle(MouseEvent event) { if (event.isSecondaryButtonDown()) { reset(root, scene); } else{ scene.setCursor(Cursor.MOVE); dragBaseX = out.translateXProperty().get(); dragBaseY = out.translateYProperty().get(); dragBase2X = event.getSceneX(); dragBase2Y = event.getSceneY(); } } }); root.setOnMouseDragged(new EventHandler<MouseEvent>() { public void handle(MouseEvent event) { if (event.isPrimaryButtonDown()) { final long startTime = System.nanoTime(); out.setTranslateX(dragBaseX + (event.getSceneX() - dragBase2X)); out.setTranslateY(dragBaseY + (event.getSceneY() - dragBase2Y)); tX = out.getTranslateX(); tY = out.getTranslateY(); Bounds oy = out.getLayoutBounds(); double minX = (oy.getMinX()+oy.getMaxX())/2 - ((700/2)+tX)/scale; double minY = (oy.getMinY()+oy.getMaxY())/2 - ((700/2)+tY)/scale; double widthX = 700/scale; double widthY = 700/scale; System.out.format("ZOOM Clip minX=%4.1f minY=%4.1f width=%4.1f height=%4.1f\n",minX,minY,widthX,widthY); out.setClip(new Rectangle(minX, minY, widthX, widthY)); //System.out.println("DRAG duration = " + (System.nanoTime() - startTime) / 1e9); } } }); root.setOnMouseReleased(new EventHandler<MouseEvent>() { public void handle(MouseEvent event) { scene.setCursor(Cursor.DEFAULT); System.out.println("TranslateX = " + out.getTranslateX()); System.out.println("TranslateY = " + out.getTranslateY()); } }); root.setOnMouseClicked(new EventHandler<MouseEvent>() { public void handle(MouseEvent event) { } }); root.addEventHandler(MouseEvent.IMPL_MOUSE_WHEEL_ROTATED, new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { double wr = event.impl_getWheelRotation(); double d = 1.0 - wr / 10; zoom(root, d); } }); } private void makeRandom() { int pXstart; int pYstart; int pXend; int pYend; out = new Group(); for (int i = 0; i < numberLines; i++) { pXstart = coordMinX + (int) (Math.random() * ((coordMaxX - coordMinX) + 1)); pYstart = coordMinY + (int) (Math.random() * ((coordMaxY - coordMinY) + 1)); pXend = coordMinX + (int) (Math.random() * ((coordMaxX - coordMinX) + 1)); pYend = coordMinY + (int) (Math.random() * ((coordMaxY - coordMinY) + 1)); //##################################### change shape here // for Path use this code MoveTo start = new MoveTo(pXstart, pYstart); LineTo end = new LineTo(pXend, pYend); Path path = new Path(); path.getElements().addAll(start, end); ((Group) out).getChildren().add(path); // for Line use this code ((Group) out).getChildren().add(new Line(pXstart, pYstart, pXend, pYend)); //##################################################################### } } private void reset(Pane root, Scene scene) { ((StackPane) root).getChildren().clear(); double drawWidth = root.getWidth()-margin.left-margin.right; double drawHeight = root.getHeight()-margin.top-margin.bottom; scale = 1; scale = Math.min(drawWidth/ out.getLayoutBounds().getWidth(), drawHeight/ out.getLayoutBounds().getHeight()); //scale = Math.min(scene.getWidth() / out.getBoundsInLocal().getWidth(), scene.getHeight() / out.getBoundsInLocal().getHeight()); out.setTranslateX(0); out.setTranslateY(0); tX = out.getTranslateX(); tY = out.getTranslateY(); out.setScaleX(scale); out.setScaleY(scale); ((StackPane) root).getChildren().add(out); traverse(root); Bounds oy = out.getLayoutBounds(); Bounds ol = out.getBoundsInLocal(); Bounds op = out.getBoundsInParent(); System.out.format("RESET LayoutBounds minX=%4.1f minY=%4.1f width=%4.1f height=%4.1f maxX=%4.1f maxY=%4.1f\n",oy.getMinX(),oy.getMinY(),oy.getWidth(),oy.getHeight(),oy.getMaxX(),oy.getMaxY()); System.out.format("RESET LocalBounds minX=%4.1f minY=%4.1f width=%4.1f height=%4.1f maxX=%4.1f maxY=%4.1f\n",ol.getMinX(),ol.getMinY(),ol.getWidth(),ol.getHeight(),ol.getMaxX(),ol.getMaxY()); System.out.format("RESET ParentBounds minX=%4.1f minY=%4.1f width=%4.1f height=%4.1f maxX=%4.1f maxY=%4.1f\n",op.getMinX(),op.getMinY(),op.getWidth(),op.getHeight(),op.getMaxX(),op.getMaxY()); System.out.format("RESET scale =%4.2f lineWidth=%4.1f local width=%4.1f parent width=%4.1f\n ",scale,lineWidth,ol.getWidth(),op.getWidth()); double minX = (oy.getMinX()+oy.getMaxX())/2 - ((drawWidth/2)+tX)/scale; double minY = (oy.getMinY()+oy.getMaxY())/2 - ((drawHeight/2)+tY)/scale; double widthX = 700/scale; double widthY = 700/scale; System.out.format("RESET Clip minX=%4.1f minY=%4.1f width=%4.1f height=%4.1f\n",minX,minY,widthX,widthY); System.out.println(); out.setClip(new Rectangle(minX, minY, widthX, widthY)); } private void zoom(Pane root, double d) { final long startTime = System.nanoTime(); double drawWidth = root.getWidth()-margin.left-margin.right; double drawHeight = root.getHeight()-margin.top-margin.bottom; scale = scale * d; System.out.format("ZOOM tX=%4.1f tY=%4.1f\n",tX,tY); out.setScaleX(scale); out.setScaleY(scale); out.setTranslateX(tX*scale/oldscale); out.setTranslateY(tY*scale/oldscale); tX=out.getTranslateX(); tY=out.getTranslateY(); System.out.format("ZOOM tX=%4.1f tY=%4.1f\n",tX,tY); oldscale = scale; Bounds oy = out.getLayoutBounds(); Bounds ol = out.getBoundsInLocal(); Bounds op = out.getBoundsInParent(); double cXnew = (oy.getMinX() + oy.getMaxX())/2 - tX/scale; double cYnew = (oy.getMinY() + oy.getMaxY())/2 - tY/scale; System.out.format("ZOOM cXnew=%4.1f cYnew=%4.1f\n",cXnew,cYnew); System.out.format("ZOOM LayoutBounds minX=%4.1f minY=%4.1f width=%4.1f height=%4.1f maxX=%4.1f maxY=%4.1f\n",oy.getMinX(),oy.getMinY(),oy.getWidth(),oy.getHeight(),oy.getMaxX(),oy.getMaxY()); System.out.format("ZOOM LocalBounds minX=%4.1f minY=%4.1f width=%4.1f height=%4.1f maxX=%4.1f maxY=%4.1f\n",ol.getMinX(),ol.getMinY(),ol.getWidth(),ol.getHeight(),ol.getMaxX(),ol.getMaxY()); System.out.format("ZOOM ParentBounds minX=%4.1f minY=%4.1f width=%4.1f height=%4.1f maxX=%4.1f maxY=%4.1f\n",op.getMinX(),op.getMinY(),op.getWidth(),op.getHeight(),op.getMaxX(),op.getMaxY()); double minX = (oy.getMinX()+oy.getMaxX())/2 - ((drawWidth/2)+tX)/scale; double minY = (oy.getMinY()+oy.getMaxY())/2 - ((drawHeight/2)+tY)/scale; double widthX = drawWidth/scale; double widthY = drawHeight/scale; System.out.format("ZOOM Clip minX=%4.1f minY=%4.1f width=%4.1f height=%4.1f\n",minX,minY,widthX,widthY); out.setClip(new Rectangle(minX, minY, widthX, widthY)); lineWidth = 1; traverse(root); System.out.format("ZOOM scale =%4.2f lineWidth=%4.1f local width=%4.1f parent width=%4.1f\n ",scale,lineWidth,ol.getWidth(),op.getWidth()); System.out.println("ZOOM duration = " + (System.nanoTime() - startTime) / 1e9); System.out.println(); } public void traverse(Node node) { if (node instanceof Shape) { ((Shape) node).strokeWidthProperty().setValue(lineWidth); } else if (node instanceof Parent) { for (Node subNode : ((Parent) node).getChildrenUnmodifiable()) { traverse(subNode); } } } }
05-04-2012

I am trying something very similar and also have very poor performance. I am loading shapefiles and tried drawing Paths, Polylines and Lines, they are all very very slow when drawing 20,000 lines. When I try profiling it in Netbeans, I find it spends 99.999% of time in java.util.concurrent.locks.LockSupport.park(), the root of this call was com.sun.javafx.application.LauncherImpl.launchApplication(). Does anybody know what this is doing and why? I am going to try a test case with 10,000 random lines to see if this has the same peformance. Also going to try running outside of Netbeans. Has anybody successfully run JavaFX applications with ten's of thousands of nodes?
03-04-2012

Regarding your claim that 99.9999% of the time is spent in com.sun.javafx.application.LauncherImpl.launchApplication(), you are misinterpreting the results of the profile. Irrespective of what your program is doing, the thread that calls launchApplication spends the entire life of the application asleep doing nothing, and is in no way contributing to any performance issue. The performance issue is with the rendering of paths, particularly when stroke != null.
03-04-2012