-
Bug
-
Resolution: Fixed
-
P3
-
jfx13
ADDITIONAL SYSTEM INFORMATION :
java.runtime.version: 12.0.2+10
javafx.runtime.version: 13-ea+12
A DESCRIPTION OF THE PROBLEM :
The newly introduced WritableImages, which are backed by a java.nio.Buffer,
fail to update with an exception when the callback returns an empty Rectangle2D.
java.lang.IllegalArgumentException: srcw (0) and srch (0) must be > 0
at com.sun.prism.impl.BaseTexture.checkUpdateParams(BaseTexture.java:354)
at com.sun.prism.es2.ES2Texture.update(ES2Texture.java:640)
at com.sun.prism.impl.BaseResourceFactory.getCachedTexture(BaseResourceFactory.java:232)
at com.sun.prism.impl.BaseResourceFactory.getCachedTexture(BaseResourceFactory.java:156)
at com.sun.javafx.sg.prism.NGImageView.renderContent(NGImageView.java:121)
at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2072)
at com.sun.javafx.sg.prism.NGImageView.doRender(NGImageView.java:103)
at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1964)
at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:270)
at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:578)
at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2072)
at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1964)
at com.sun.javafx.tk.quantum.ViewPainter.doPaint(ViewPainter.java:479)
at com.sun.javafx.tk.quantum.ViewPainter.paintImpl(ViewPainter.java:321)
at com.sun.javafx.tk.quantum.PresentingPainter.run(PresentingPainter.java:91)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:305)
at com.sun.javafx.tk.RenderJob.run(RenderJob.java:58)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.run(QuantumRenderer.java:125)
at java.base/java.lang.Thread.run(Thread.java:835)
According to the documentation the update of the buffer should happen inside the callback
of the updateBuffer method of the PixelBuffer. The returned Rectangle2D is supposed to
indicate which part of the image has become dirty. In practice it may well turn out that
no pixels have changed during the update and it is thus just natural to indicate this
by returning an empty Rectangle2D but obviously the implementation does not handle this
case appropriately. For performance reasons this case should actually be filtered out very
early in the processing pipeline because in this case no further processing is required.
I have attached a test program which links an AWT BufferedImage via an IntBuffer to a
JavaFX WritableImage so that I can draw directly into the BufferedImage via its Graphics2D
context and the result is then displayed via the linked JavaFX image.
The program has three buttons. The first one indicates a full update,
the second one a partial update and the third one an empty update. The third one
crashes for the above mentioned reason which should not be the case.
See also: https://github.com/javafxports/openjdk-jfx/issues/567
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the attached test program and click on the buttons from left to right.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
1. Full update, 2. partial update ,3. no update and no exception.
ACTUAL -
1. Full update, 2. partial update ,3. exception.
---------- BEGIN SOURCE ----------
package de.mpmediasoft.nativefxeval;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.nio.IntBuffer;
import javafx.geometry.Rectangle2D;
import javafx.scene.image.Image;
import javafx.scene.image.PixelBuffer;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.util.Callback;
public class AWTImageCanvas {
private BufferedImage bufImg;
private Graphics2D g2d;
private PixelBuffer<IntBuffer> pixelBuffer;
private WritableImage img;
private Callback<java.awt.Graphics2D, java.awt.geom.Rectangle2D> updateCallback;
public AWTImageCanvas(int width, int height) {
bufImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
g2d = (Graphics2D) bufImg.getGraphics();
DataBuffer db = bufImg.getRaster().getDataBuffer();
DataBufferInt dbi = (DataBufferInt) db;
int[] raw = dbi.getData();
IntBuffer ib = IntBuffer.wrap(raw);
assert raw.length == width * height;
PixelFormat<IntBuffer> pixelFormat = PixelFormat.getIntArgbPreInstance();
pixelBuffer = new PixelBuffer<>(bufImg.getWidth(), bufImg.getHeight(), ib, pixelFormat);
img = new WritableImage(pixelBuffer);
}
public Image getImage() {return img;}
public int getWidth() {return bufImg.getWidth();}
public int getHeight() {return bufImg.getHeight();}
public void update() {
if (updateCallback != null) {
pixelBuffer.updateBuffer(pb -> {
final java.awt.geom.Rectangle2D r = updateCallback.call(g2d);
return (r != null) ? (r.isEmpty() ? Rectangle2D.EMPTY : new Rectangle2D(r.getX(), r.getY(), r.getWidth(), r.getHeight())) : null;
});
}
}
public void setOnUpdate(Callback<java.awt.Graphics2D, java.awt.geom.Rectangle2D> updateCallback) {
this.updateCallback = updateCallback;
}
}
package de.mpmediasoft.nativefxeval;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.geom.Path2D;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ToolBar;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class AWTImageCanvasDemo extends Application {
private AWTImageCanvas canvas = new AWTImageCanvas(800, 600);
@Override
public void init() {
System.out.println("java.runtime.version: " + System.getProperty("java.runtime.version", "(undefined)"));
System.out.println("javafx.runtime.version: " + System.getProperty("javafx.runtime.version", "(undefined)"));
}
Color c1 = Color.red;
Color c2 = Color.green;
Color c3 = Color.blue;
Color c;
@Override
public void start(Stage primaryStage) throws Exception {
Button b1 = new Button("Full (RED)");
b1.setOnAction(e -> {
c = c1;
canvas.update();
});
Button b2 = new Button("Partial (GREEN)");
b2.setOnAction(e -> {
c = c2;
canvas.update();
});
Button b3 = new Button("Empty (BLUE)");
b3.setOnAction(e -> {
c = c3;
canvas.update();
});
ToolBar toolbar = new ToolBar(b1, b2, b3);
BorderPane root = new BorderPane();
root.setTop(toolbar);
root.setCenter(new ImageView(canvas.getImage()));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
canvas.setOnUpdate(g2d -> {
// This is pure AWT.
g2d.setBackground(Color.decode("#F0F0FF"));
g2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
Path2D p = new Path2D.Double();
p.moveTo(100, 100);
p.lineTo(700, 300);
p.lineTo(200, 500);
p.closePath();
g2d.setColor(c);
g2d.fill(p);
g2d.setColor(new Color(50, 100, 150));
g2d.setStroke(new BasicStroke(10));
g2d.draw(p);
if (c == c1) {
System.out.println("Full update.");
return null; // Full
} else if (c == c2) {
System.out.println("Partial update.");
return new java.awt.geom.Rectangle2D.Double(0, 0, canvas.getWidth() / 2, canvas.getHeight()); // Partial
} else {
System.out.println("Empty update.");
return new java.awt.geom.Rectangle2D.Double(); // Empty
// return new java.awt.geom.Rectangle2D.Double(0, 0, 1, 1); // Almost Empty
}
});
}
public static void main(String[] args) {
launch(args);
}
}
class AWTImageCanvasDemoLauncher {public static void main(String[] args) {AWTImageCanvasDemo.main(args);}}
---------- END SOURCE ----------
FREQUENCY : always
java.runtime.version: 12.0.2+10
javafx.runtime.version: 13-ea+12
A DESCRIPTION OF THE PROBLEM :
The newly introduced WritableImages, which are backed by a java.nio.Buffer,
fail to update with an exception when the callback returns an empty Rectangle2D.
java.lang.IllegalArgumentException: srcw (0) and srch (0) must be > 0
at com.sun.prism.impl.BaseTexture.checkUpdateParams(BaseTexture.java:354)
at com.sun.prism.es2.ES2Texture.update(ES2Texture.java:640)
at com.sun.prism.impl.BaseResourceFactory.getCachedTexture(BaseResourceFactory.java:232)
at com.sun.prism.impl.BaseResourceFactory.getCachedTexture(BaseResourceFactory.java:156)
at com.sun.javafx.sg.prism.NGImageView.renderContent(NGImageView.java:121)
at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2072)
at com.sun.javafx.sg.prism.NGImageView.doRender(NGImageView.java:103)
at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1964)
at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:270)
at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:578)
at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2072)
at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1964)
at com.sun.javafx.tk.quantum.ViewPainter.doPaint(ViewPainter.java:479)
at com.sun.javafx.tk.quantum.ViewPainter.paintImpl(ViewPainter.java:321)
at com.sun.javafx.tk.quantum.PresentingPainter.run(PresentingPainter.java:91)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:305)
at com.sun.javafx.tk.RenderJob.run(RenderJob.java:58)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.run(QuantumRenderer.java:125)
at java.base/java.lang.Thread.run(Thread.java:835)
According to the documentation the update of the buffer should happen inside the callback
of the updateBuffer method of the PixelBuffer. The returned Rectangle2D is supposed to
indicate which part of the image has become dirty. In practice it may well turn out that
no pixels have changed during the update and it is thus just natural to indicate this
by returning an empty Rectangle2D but obviously the implementation does not handle this
case appropriately. For performance reasons this case should actually be filtered out very
early in the processing pipeline because in this case no further processing is required.
I have attached a test program which links an AWT BufferedImage via an IntBuffer to a
JavaFX WritableImage so that I can draw directly into the BufferedImage via its Graphics2D
context and the result is then displayed via the linked JavaFX image.
The program has three buttons. The first one indicates a full update,
the second one a partial update and the third one an empty update. The third one
crashes for the above mentioned reason which should not be the case.
See also: https://github.com/javafxports/openjdk-jfx/issues/567
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the attached test program and click on the buttons from left to right.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
1. Full update, 2. partial update ,3. no update and no exception.
ACTUAL -
1. Full update, 2. partial update ,3. exception.
---------- BEGIN SOURCE ----------
package de.mpmediasoft.nativefxeval;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.nio.IntBuffer;
import javafx.geometry.Rectangle2D;
import javafx.scene.image.Image;
import javafx.scene.image.PixelBuffer;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.util.Callback;
public class AWTImageCanvas {
private BufferedImage bufImg;
private Graphics2D g2d;
private PixelBuffer<IntBuffer> pixelBuffer;
private WritableImage img;
private Callback<java.awt.Graphics2D, java.awt.geom.Rectangle2D> updateCallback;
public AWTImageCanvas(int width, int height) {
bufImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
g2d = (Graphics2D) bufImg.getGraphics();
DataBuffer db = bufImg.getRaster().getDataBuffer();
DataBufferInt dbi = (DataBufferInt) db;
int[] raw = dbi.getData();
IntBuffer ib = IntBuffer.wrap(raw);
assert raw.length == width * height;
PixelFormat<IntBuffer> pixelFormat = PixelFormat.getIntArgbPreInstance();
pixelBuffer = new PixelBuffer<>(bufImg.getWidth(), bufImg.getHeight(), ib, pixelFormat);
img = new WritableImage(pixelBuffer);
}
public Image getImage() {return img;}
public int getWidth() {return bufImg.getWidth();}
public int getHeight() {return bufImg.getHeight();}
public void update() {
if (updateCallback != null) {
pixelBuffer.updateBuffer(pb -> {
final java.awt.geom.Rectangle2D r = updateCallback.call(g2d);
return (r != null) ? (r.isEmpty() ? Rectangle2D.EMPTY : new Rectangle2D(r.getX(), r.getY(), r.getWidth(), r.getHeight())) : null;
});
}
}
public void setOnUpdate(Callback<java.awt.Graphics2D, java.awt.geom.Rectangle2D> updateCallback) {
this.updateCallback = updateCallback;
}
}
package de.mpmediasoft.nativefxeval;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.geom.Path2D;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ToolBar;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class AWTImageCanvasDemo extends Application {
private AWTImageCanvas canvas = new AWTImageCanvas(800, 600);
@Override
public void init() {
System.out.println("java.runtime.version: " + System.getProperty("java.runtime.version", "(undefined)"));
System.out.println("javafx.runtime.version: " + System.getProperty("javafx.runtime.version", "(undefined)"));
}
Color c1 = Color.red;
Color c2 = Color.green;
Color c3 = Color.blue;
Color c;
@Override
public void start(Stage primaryStage) throws Exception {
Button b1 = new Button("Full (RED)");
b1.setOnAction(e -> {
c = c1;
canvas.update();
});
Button b2 = new Button("Partial (GREEN)");
b2.setOnAction(e -> {
c = c2;
canvas.update();
});
Button b3 = new Button("Empty (BLUE)");
b3.setOnAction(e -> {
c = c3;
canvas.update();
});
ToolBar toolbar = new ToolBar(b1, b2, b3);
BorderPane root = new BorderPane();
root.setTop(toolbar);
root.setCenter(new ImageView(canvas.getImage()));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
canvas.setOnUpdate(g2d -> {
// This is pure AWT.
g2d.setBackground(Color.decode("#F0F0FF"));
g2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
Path2D p = new Path2D.Double();
p.moveTo(100, 100);
p.lineTo(700, 300);
p.lineTo(200, 500);
p.closePath();
g2d.setColor(c);
g2d.fill(p);
g2d.setColor(new Color(50, 100, 150));
g2d.setStroke(new BasicStroke(10));
g2d.draw(p);
if (c == c1) {
System.out.println("Full update.");
return null; // Full
} else if (c == c2) {
System.out.println("Partial update.");
return new java.awt.geom.Rectangle2D.Double(0, 0, canvas.getWidth() / 2, canvas.getHeight()); // Partial
} else {
System.out.println("Empty update.");
return new java.awt.geom.Rectangle2D.Double(); // Empty
// return new java.awt.geom.Rectangle2D.Double(0, 0, 1, 1); // Almost Empty
}
});
}
public static void main(String[] args) {
launch(args);
}
}
class AWTImageCanvasDemoLauncher {public static void main(String[] args) {AWTImageCanvasDemo.main(args);}}
---------- END SOURCE ----------
FREQUENCY : always
- links to