On Mon, 10 Nov 2025 13:59:30 GMT, John Hendrikx <[email protected]> wrote:

> This PR adds a `getDrawingContext` method to `WritableImage`, which works 
> similar to `Canvas::getGraphicsContext` and shares the same signatures. Key 
> features include:
> 
> - **Shape rendering**: `strokeRect`, `fillRect`, `strokeOval`, `fillOval`, 
> `strokeArc`, `fillArc`, `strokePolyline`, `fillPolygon`, etc.
> - **Stroke and fill attributes**: `lineWidth`, `lineCap`, `lineJoin`, 
> `miterLimit`, `fillRule`, `stroke` and `fill` paints.
> - **Global graphics settings**: `globalAlpha` and `globalBlendMode`.
> - **Image drawing**: draw other `Image` instances with scaling and 
> source/destination rectangles.
> 
> This feature enables direct software rendering to `WritableImage` without 
> requiring a `Canvas` + snapshot.
> 
> **Additional notes**:
> 
> - The implementation leverages the software `Pisces` renderer.
> - This lays the groundwork for future support of text rendering and path 
> operations.
> 
> **Example usage**:
> 
> 
> WritableImage img = new WritableImage(400, 400);
> DrawingContext ctx = img.getDrawingContext();
> ctx.setFill(Color.RED);
> ctx.fillRect(50, 50, 100, 100);
> 
> 
> See the sample program `RandomShapesDemo` to see a `WritableImage` and 
> `Canvas` side by side performing the same operations:
> 
> <img width="1249" height="741" alt="image" 
> src="https://github.com/user-attachments/assets/4a0b9dcc-8f96-4faa-99cf-83c66d2c851e";
>  />
> 
> Newer version:
> 
> <img width="1249" height="741" alt="image" 
> src="https://github.com/user-attachments/assets/c0502620-3d02-4fbd-8c33-43bd5138b42c";
>  />
> 
> ---------
> - [x] I confirm that I make this contribution in accordance with the [OpenJDK 
> Interim AI Policy](https://openjdk.org/legal/ai).

Adding a note here for myself:


    private final AbstractNotifyListener platformImageChangeListener =
            new AbstractNotifyListener() {
        @Override
        public void invalidated(Observable valueModel) {
            invalidateWidthHeight();
            NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_CONTENTS);
            NodeHelper.geomChanged(ImageView.this);
        }
    };


The above code in `ImageView` responds to **pixel** changes in the platform 
image, however, such a change can never influence the width/height (it is 
triggered by `pixelsDirty` in `WritableImage`).  Because it also invalidates 
width/height, and triggers a `geomChanged` a new layout is triggered.  If you 
happen to be drawing something in your writable image as part of 
`layoutChildren` via `getDrawingContext` or the pixel writer, this may then 
trigger infinite layouts.

I get the impression that the code is unaware that even though it is called 
"platform image **CHANGE** listener" there is no actual image reference change; 
only some pixels were dirty in the same image...

I think the `invalidateWidthHeight` and `NodeHelper.geomChanged` lines need to 
be removed as part of this PR.

I've not given up on it, just lack a bit of time to work on it (and my use case 
has changed enough that this is not an immediate need for me currently). It 
should still be quite useful for cases where you must read back the result of 
drawing operations (for say undo/redo or drawing a custom cursor) as having to 
make a snapshot of the entire area could be much more expensive.

It can also be useful for reproducible results, as the CPU renderer will work 
the same in all cases, and could be used to synthesize images/icons instead of 
storing those as an image. It could be also be useful for say an SVG 
implementation that works directly with WritableImage (instead of via 
BufferedImage/AWT).

Discovered another reason why I think we should integrate this.

A large `Canvas` (say 4096x4096 pixels) gets scaled up to 8192x8192 pixels when 
there is a display attached to your system that has more than 1.0 renderscale. 
That represents a 256 MB texture that is being allocated that lives on the GPU.

In contrast, a WritableImage does no such thing. It remains at the indicated 
render scale, using only about 64 MB of VRAM. Also ImageView + WritableImage 
uses texture tiling, which means it can exceed the max allowed texture size 
(ie. you can make a 64k x 1k writable image, something that Canvas can't do at 
all).

So I've completed this now, as far as I think is reasonable. `WritableImage`'s 
`DrawingContext` has now got almost full parity with `GraphicsContext` from 
`Canvas`, including fonts, dashed lines, paths, clips (rectangular only), 
transforms and save/restore functionality.

I added a new demo image showing they render almost pixel perfect the same 
(this is not too surprising as the software renderer is the core of the JavaFX 
software pipeline).

Please review :)

modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/QuantumToolkit.java
 line 1510:

> 1508:     public PlatformImage createPlatformImage(int w, int h) {
> 1509:         IntBuffer buf = IntBuffer.allocate(w * h);
> 1510:         return com.sun.prism.Image.fromIntArgbPreData(buf, w, h);

This seems to be no problem to change (an image heavy application still runs 
absolutely fine), but it is kind of a global change.  If there are issues with 
this, we could make a specific method for writable images to use so we always 
get an `int[]` buffer that the software renderer expects.

As it is now, the renderer is writing directly into the underlying image 
storage, without any copies being made (which is nice and efficient).

-------------

PR Comment: https://git.openjdk.org/jfx/pull/1969#issuecomment-3539473278
PR Comment: https://git.openjdk.org/jfx/pull/1969#issuecomment-4066957830
PR Comment: https://git.openjdk.org/jfx/pull/1969#issuecomment-5368561084
PR Comment: https://git.openjdk.org/jfx/pull/1969#issuecomment-5462832904
PR Review Comment: https://git.openjdk.org/jfx/pull/1969#discussion_r2510754465

Reply via email to