Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@
<artifactId>PiccodeScript</artifactId>
<version>main-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>com.github.Glimmr-Lang</groupId>
<artifactId>JHLabs</artifactId>
<version>main-SNAPSHOT</version>
</dependency>

</dependencies>

Expand Down
49 changes: 32 additions & 17 deletions src/main/java/org/editor/CanvasFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.vlsolutions.swing.docking.DockKey;
import com.vlsolutions.swing.docking.Dockable;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
Expand All @@ -21,6 +22,7 @@
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
Expand All @@ -46,6 +48,7 @@ public class CanvasFrame extends JPanel implements MouseListener, MouseMotionLis

private int lastMouseX, lastMouseY;
private BufferedImage gridImage;
private BufferedImage render;
private Point lastDragPoint;
public static Graphics2D gfx = null;
private long lastTime;
Expand All @@ -66,17 +69,13 @@ public class CanvasFrame extends JPanel implements MouseListener, MouseMotionLis
private static CanvasFrame _the = null;
private DockKey key = new DockKey("canvas");

public static String code = null;
public static String file = null;
public static boolean start = false;

private CanvasFrame() {
super(new BorderLayout());
this.setBackground(new Color(18, 18, 18));
this.addMouseListener(this);
this.addMouseMotionListener(this);
drawGrid();
setPreferredSize(new Dimension(getWidth(), getHeight()));
compile(() -> null);
Timer timer = new Timer(16, new ActionListener() {
public void actionPerformed(ActionEvent e) {
repaint(getVisibleRect());
Expand Down Expand Up @@ -108,30 +107,21 @@ protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;

drawGrid();

g2.drawImage(gridImage, 0, 0, null);

// Smooth rendering
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(Color.BLACK);
gfx = g2;
if (start && file != null && code != null) {
AccessFrame.msgs.setText("");
compileFrame();
}

if (render != null) {
g2.drawImage(render, 0, 0, null);
}
drawSelection(g2);
if (showHighlight) {
drawCrosshair(g2);
}
}

private PiccodeValue compileFrame() {
Context.top.resetContext();
var result = Compiler.compile(file, code);
return result;
}

private void drawSelection(Graphics2D g2) {
if (selecting && selectionStart != null && selectionEnd != null) {
Point start = selectionStart;
Expand Down Expand Up @@ -306,8 +296,33 @@ private void drawGrid() {

lastGridOffsetX = offsetX;
lastGridOffsetY = offsetY;
g2.dispose();
}

public void compile(Supplier<PiccodeValue> fx) {
int width = getWidth();
int height = getHeight();
if (width <= 0 || height <= 0) {
return;
}

render = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);

// Get the Graphics2D object
gfx = render.createGraphics();

// Enable transparency by drawing a fully transparent background
gfx.setComposite(AlphaComposite.Clear);
gfx.fillRect(0, 0, width, height);

gfx.setColor(Color.BLACK);
// Switch back to normal composite mode for drawing
gfx.setComposite(AlphaComposite.SrcOver);

fx.get();
}
Comment on lines +302 to 323
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Dispose previous gfx and reuse buffers; avoid leaks and allocations; repaint after compile

The current compile always allocates a new BufferedImage, reassigns gfx, and never disposes the previous Graphics2D. This leaks native resources and increases GC pressure. Also, if the component size hasn't changed, you can reuse the existing buffer.

Proposed fix:

  • Dispose the previous gfx before creating a new one (or reuse existing when size unchanged).
  • Reuse render if width/height are unchanged and just clear it.
  • Set useful rendering hints on gfx.
  • Trigger a repaint after updating the offscreen buffer.
 public void compile(Supplier<PiccodeValue> fx) {
   int width = getWidth();
   int height = getHeight();
   if (width <= 0 || height <= 0) {
     return;
   }
-
-  render = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
-
-  // Get the Graphics2D object
-  gfx = render.createGraphics();
-
-  // Enable transparency by drawing a fully transparent background
-  gfx.setComposite(AlphaComposite.Clear);
-  gfx.fillRect(0, 0, width, height);
-
-  gfx.setColor(Color.BLACK);
-  // Switch back to normal composite mode for drawing
-  gfx.setComposite(AlphaComposite.SrcOver);
-
-  fx.get();
+  // Reuse buffer if size unchanged; otherwise dispose and recreate
+  boolean recreate = render == null || render.getWidth() != width || render.getHeight() != height;
+  if (recreate) {
+    if (gfx != null) {
+      try { gfx.dispose(); } catch (Exception ignore) {}
+    }
+    render = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+    gfx = render.createGraphics();
+    // Optional: quality hints for higher fidelity drawing to the offscreen buffer
+    gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+    gfx.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
+    gfx.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+  }
+
+  // Clear buffer to full transparency
+  gfx.setComposite(AlphaComposite.Clear);
+  gfx.fillRect(0, 0, width, height);
+  gfx.setComposite(AlphaComposite.SrcOver);
+  gfx.setColor(Color.BLACK);
+
+  // Execute supplied drawing/compilation logic
+  fx.get();
+
+  // Ensure the latest image is presented
+  repaint(getVisibleRect());
 }

Note: If you plan to allow long-running drawing from background threads into gfx, consider guarding gfx and render with a lock and using a snapshot for painting to avoid tearing/races.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void compile(Supplier<PiccodeValue> fx) {
int width = getWidth();
int height = getHeight();
if (width <= 0 || height <= 0) {
return;
}
render = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
// Get the Graphics2D object
gfx = render.createGraphics();
// Enable transparency by drawing a fully transparent background
gfx.setComposite(AlphaComposite.Clear);
gfx.fillRect(0, 0, width, height);
gfx.setColor(Color.BLACK);
// Switch back to normal composite mode for drawing
gfx.setComposite(AlphaComposite.SrcOver);
fx.get();
}
public void compile(Supplier<PiccodeValue> fx) {
int width = getWidth();
int height = getHeight();
if (width <= 0 || height <= 0) {
return;
}
// Reuse buffer if size unchanged; otherwise dispose and recreate
boolean recreate = render == null
|| render.getWidth() != width
|| render.getHeight() != height;
if (recreate) {
if (gfx != null) {
try {
gfx.dispose();
} catch (Exception ignore) {
}
}
render = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
gfx = render.createGraphics();
// Optional: quality hints for higher-fidelity drawing to the offscreen buffer
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
gfx.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
gfx.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
}
// Clear buffer to full transparency
gfx.setComposite(AlphaComposite.Clear);
gfx.fillRect(0, 0, width, height);
gfx.setComposite(AlphaComposite.SrcOver);
gfx.setColor(Color.BLACK);
// Execute supplied drawing/compilation logic
fx.get();
// Ensure the latest image is presented
repaint(getVisibleRect());
}
🤖 Prompt for AI Agents
In src/main/java/org/editor/CanvasFrame.java lines 302 to 323, the compile
method leaks native resources by not disposing the previous gfx Graphics2D
object and always allocating a new BufferedImage even if the size is unchanged.
Fix this by checking if the current render buffer matches the component size; if
so, reuse it by clearing its content instead of creating a new one. If a new
BufferedImage is needed, dispose the old gfx before creating a new one. Also,
set appropriate rendering hints on gfx for better quality and call repaint()
after updating the buffer to refresh the display.



@Override
public void mouseClicked(MouseEvent e) {
}
Expand Down
4 changes: 1 addition & 3 deletions src/main/java/org/editor/events/AccessEvents.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ public static void compileAndRender(ActionEvent e) {

var file = ed.file.toString();
var code = ed.textArea.getText();
CanvasFrame.file = file;
CanvasFrame.code = code;
CanvasFrame.start = true;
CanvasFrame.the().compile(() -> Compiler.compile(file, code));
AccessFrame.writeSuccess("Compilation started: ");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package org.editor.nativemods;

import com.jhlabs.image.BrushedMetalFilter;
import java.awt.Color;
import java.awt.Graphics2D;
import java.util.HashMap;
import java.util.List;
import org.editor.CanvasFrame;
import org.piccode.rt.Context;
import org.piccode.rt.PiccodeException;
import org.piccode.rt.PiccodeNumber;
import org.piccode.rt.PiccodeObject;
import org.piccode.rt.PiccodeReference;
import org.piccode.rt.PiccodeString;
import org.piccode.rt.PiccodeUnit;
import org.piccode.rt.PiccodeValue;
import org.piccode.rt.PiccodeValue.Type;
import org.piccode.rt.modules.NativeFunctionFactory;

/**
*
* @author hexaredecimal
*/
public class PiccodeBrushedMetalFilterModule {

public static void addFunctions() {
NativeFunctionFactory.create("brush_metal_new", List.of(), (args, namedArgs, frame) -> {
var bmFilter = new BrushedMetalFilter();
return new PiccodeReference(bmFilter);
}, null);

NativeFunctionFactory.create("brush_metal_set_rad", List.of("filter", "rad"), (args, namedArgs, frame) -> {
var _filter = namedArgs.get("filter");
var rad = namedArgs.get("rad");

var ctx = frame == null
? Context.top
: Context.getContextAt(frame);
var caller = ctx.getTopFrame().caller;

PiccodeValue.verifyType(caller, _filter, Type.REFERENCE);
PiccodeValue.verifyType(caller, rad, Type.NUMBER);

var obj = (PiccodeReference) _filter;
var _filter_object = obj.deref();
if (!(_filter_object instanceof BrushedMetalFilter)) {
throw new PiccodeException(caller.file, caller.line, caller.column, "Filter is not a correct object.");
}

var filter = (BrushedMetalFilter) _filter_object;
var radius = (int) (double) ((PiccodeNumber) rad).raw();

filter.setRadius(radius);

return new PiccodeReference(_filter_object);
}, null);

NativeFunctionFactory.create("brush_metal_set_amount", List.of("filter", "amount"), (args, namedArgs, frame) -> {
var _filter = namedArgs.get("filter");
var amount = namedArgs.get("amount");

var ctx = frame == null
? Context.top
: Context.getContextAt(frame);
var caller = ctx.getTopFrame().caller;

PiccodeValue.verifyType(caller, _filter, Type.REFERENCE);
PiccodeValue.verifyType(caller, amount, Type.NUMBER);

var obj = (PiccodeReference) _filter;
var _filter_object = obj.deref();
if (!(_filter_object instanceof BrushedMetalFilter)) {
throw new PiccodeException(caller.file, caller.line, caller.column, "Filter is not a correct object.");
}

var filter = (BrushedMetalFilter) _filter_object;
var _amount = (int) (double) ((PiccodeNumber) amount).raw();

filter.setRadius(_amount);

return new PiccodeReference(_filter_object);
}, null);

NativeFunctionFactory.create("brush_metal_set_shine", List.of("filter", "shine"), (args, namedArgs, frame) -> {
var _filter = namedArgs.get("filter");
var shine = namedArgs.get("shine");

var ctx = frame == null
? Context.top
: Context.getContextAt(frame);
var caller = ctx.getTopFrame().caller;

PiccodeValue.verifyType(caller, _filter, Type.REFERENCE);
PiccodeValue.verifyType(caller, shine, Type.NUMBER);

var obj = (PiccodeReference) _filter;
var _filter_object = obj.deref();
if (!(_filter_object instanceof BrushedMetalFilter)) {
throw new PiccodeException(caller.file, caller.line, caller.column, "Filter is not a correct object.");
}

var filter = (BrushedMetalFilter) _filter_object;
var _shine = (int) (double) ((PiccodeNumber) shine).raw();

filter.setRadius(_shine);

return new PiccodeReference(_filter_object);
}, null);

}

}
57 changes: 57 additions & 0 deletions src/main/java/org/editor/nativemods/PiccodeFilterModule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.editor.nativemods;

import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.util.List;
import org.piccode.rt.Context;
import org.piccode.rt.PiccodeException;
import org.piccode.rt.PiccodeReference;
import org.piccode.rt.PiccodeValue;
import org.piccode.rt.PiccodeValue.Type;
import org.piccode.rt.modules.NativeFunctionFactory;

/**
*
* @author hexaredecimal
*/
public class PiccodeFilterModule {

public static void addFunctions() {
NativeFunctionFactory.create("filter_apply", List.of("filter", "image"), (args, namedArgs, frame) -> {
var _filter = namedArgs.get("filter");
var image = namedArgs.get("image");

var ctx = frame == null
? Context.top
: Context.getContextAt(frame);
var caller = ctx.getTopFrame().caller;

PiccodeValue.verifyType(caller, _filter, Type.REFERENCE);
PiccodeValue.verifyType(caller, image, Type.REFERENCE);

var obj = (PiccodeReference) _filter;
var img = (PiccodeReference) image;
var _filter_object = obj.deref();
var _buffered_image = img.deref();

if (!(_filter_object instanceof BufferedImageOp)) {
throw new PiccodeException(caller.file, caller.line, caller.column, "Filter is not a correct object.");
}

if (!(_buffered_image instanceof BufferedImage)) {
throw new PiccodeException(caller.file, caller.line, caller.column, "Expected a buffer image. Found " + _buffered_image);
}


var filter = (BufferedImageOp) _filter_object;
var _image = (BufferedImage) _buffered_image;

var result = filter.filter(_image, null);

return new PiccodeReference(result);
}, null);


}

}
92 changes: 92 additions & 0 deletions src/main/java/org/editor/nativemods/PiccodeImageModule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package org.editor.nativemods;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.List;
import org.editor.CanvasFrame;
import org.piccode.rt.Context;
import org.piccode.rt.PiccodeException;
import org.piccode.rt.PiccodeNumber;
import org.piccode.rt.PiccodeObject;
import org.piccode.rt.PiccodeReference;
import org.piccode.rt.PiccodeString;
import org.piccode.rt.PiccodeUnit;
import org.piccode.rt.PiccodeValue;
import org.piccode.rt.PiccodeValue.Type;
import org.piccode.rt.modules.NativeFunctionFactory;

/**
*
* @author hexaredecimal
*/
public class PiccodeImageModule {

public static void addFunctions() {

NativeFunctionFactory.create("image_new", List.of("w", "h"), (args, namedArgs, frame) -> {
var w = namedArgs.get("w");
var h = namedArgs.get("h");

var ctx = frame == null ?
Context.top
: Context.getContextAt(frame);
var caller = ctx.getTopFrame().caller;

PiccodeValue.verifyType(caller, w, Type.NUMBER);
PiccodeValue.verifyType(caller, h, Type.NUMBER);

var _w = (int) (double) ((PiccodeNumber) w).raw();
var _h = (int) (double) ((PiccodeNumber) h).raw();

var image = new BufferedImage(_w, _h, BufferedImage.TYPE_INT_ARGB);
return new PiccodeReference(image);
}, null);

NativeFunctionFactory.create("image_new_typed", List.of("w", "h", "type"), (args, namedArgs, frame) -> {
var w = namedArgs.get("w");
var h = namedArgs.get("h");
var type = namedArgs.get("type");

var ctx = frame == null ?
Context.top
: Context.getContextAt(frame);
var caller = ctx.getTopFrame().caller;

PiccodeValue.verifyType(caller, w, Type.NUMBER);
PiccodeValue.verifyType(caller, h, Type.NUMBER);
PiccodeValue.verifyType(caller, type, Type.NUMBER);

var _w = (int) (double) ((PiccodeNumber) w).raw();
var _h = (int) (double) ((PiccodeNumber) h).raw();
var _type = (int) (double) ((PiccodeNumber) type).raw();

var image = new BufferedImage(_w, _h, _type);
return new PiccodeReference(image);
}, null);

NativeFunctionFactory.create("image_get_context", List.of("img"), (args, namedArgs, frame) -> {
var img = namedArgs.get("img");

var ctx = frame == null ?
Context.top
: Context.getContextAt(frame);
var caller = ctx.getTopFrame().caller;

PiccodeValue.verifyType(caller, img, Type.REFERENCE);

var _buffered_image = ((PiccodeReference)img).deref();

if (!(_buffered_image instanceof BufferedImage)) {
throw new PiccodeException(caller.file, caller.line, caller.column, "Expected a buffer image. Found " + _buffered_image);
}

var bufferedmage = (BufferedImage) _buffered_image;
var gfx = bufferedmage.createGraphics();
return new PiccodeReference(gfx);
}, null);

}

}