pendingDestroy = new ArrayList<>();
+
+
+ @Override
+ public PSurface createSurface() {
+ return surface = new PSurfaceGLFW(this);
+ }
+
+ protected void initWebGPUSurface(long windowHandle, long displayHandle, int width, int height, float scaleFactor) {
+ surfaceId = PWebGPU.createSurface(windowHandle, displayHandle, width, height, scaleFactor);
+ if (surfaceId == 0) {
+ System.err.println("Failed to create WebGPU surface");
+ return;
+ }
+ graphicsId = PWebGPU.graphicsCreate(surfaceId, width, height);
+ if (graphicsId == 0) {
+ System.err.println("Failed to create WebGPU graphics context");
+ }
+ }
+
+ public long getSurfaceId() {
+ return surfaceId;
+ }
+
+ @Override
+ public void setSize(int w, int h) {
+ super.setSize(w, h);
+ if (surfaceId != 0) {
+ PWebGPU.windowResized(surfaceId, pixelWidth, pixelHeight);
+ }
+ }
+
+ @Override
+ public void beginDraw() {
+ super.beginDraw();
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.beginDraw(graphicsId);
+ checkSettings();
+ }
+
+ @Override
+ public void flush() {
+ super.flush();
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.flush(graphicsId);
+
+ for (long geometryId : pendingDestroy) {
+ PWebGPU.geometryDestroy(geometryId);
+ }
+ pendingDestroy.clear();
+ }
+
+ @Override
+ public void endDraw() {
+ super.endDraw();
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.endDraw(graphicsId);
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ if (surfaceId != 0) {
+ PWebGPU.destroySurface(surfaceId);
+ surfaceId = 0;
+ }
+ PWebGPU.exit();
+ }
+
+ // ── Background ──────────────────────────────────────────────────────
+
+ @Override
+ protected void backgroundImpl() {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.backgroundColor(graphicsId, backgroundR, backgroundG, backgroundB, backgroundA);
+ }
+
+ @Override
+ protected void backgroundImpl(PImage image) {
+ if (graphicsId == 0) {
+ return;
+ }
+ if (!(image instanceof PImageWebGPU)) {
+ throw new RuntimeException("WebGPU renderer requires PImageWebGPU. Use createImage().");
+ }
+ PImageWebGPU img = (PImageWebGPU) image;
+ if (img.getId() == 0) {
+ img.loadPixels();
+ byte[] rgba = pixelsToRGBA(img.pixels);
+ long imageId = PWebGPU.imageCreate(img.pixelWidth, img.pixelHeight, rgba);
+ img.setId(imageId);
+ }
+ PWebGPU.backgroundImage(graphicsId, img.getId());
+ }
+
+ // ── Fill / stroke ───────────────────────────────────────────────────
+
+ @Override
+ protected void fillFromCalc() {
+ super.fillFromCalc();
+ if (graphicsId == 0) {
+ return;
+ }
+ if (fill) {
+ PWebGPU.setFill(graphicsId, fillR, fillG, fillB, fillA);
+ } else {
+ PWebGPU.noFill(graphicsId);
+ }
+ }
+
+ @Override
+ protected void strokeFromCalc() {
+ super.strokeFromCalc();
+ if (graphicsId == 0) {
+ return;
+ }
+ if (stroke) {
+ PWebGPU.setStrokeColor(graphicsId, strokeR, strokeG, strokeB, strokeA);
+ } else {
+ PWebGPU.noStroke(graphicsId);
+ }
+ }
+
+ @Override
+ public void strokeWeight(float weight) {
+ super.strokeWeight(weight);
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.setStrokeWeight(graphicsId, weight);
+ }
+
+ @Override
+ public void noFill() {
+ super.noFill();
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.noFill(graphicsId);
+ }
+
+ @Override
+ public void noStroke() {
+ super.noStroke();
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.noStroke(graphicsId);
+ }
+
+ @Override
+ public void strokeCap(int cap) {
+ super.strokeCap(cap);
+ if (graphicsId == 0) {
+ return;
+ }
+ byte nativeCap = switch (cap) {
+ case ROUND -> PWebGPU.STROKE_CAP_ROUND;
+ case SQUARE -> PWebGPU.STROKE_CAP_SQUARE;
+ case PROJECT -> PWebGPU.STROKE_CAP_PROJECT;
+ default -> PWebGPU.STROKE_CAP_ROUND;
+ };
+ PWebGPU.setStrokeCap(graphicsId, nativeCap);
+ }
+
+ @Override
+ public void strokeJoin(int join) {
+ super.strokeJoin(join);
+ if (graphicsId == 0) {
+ return;
+ }
+ byte nativeJoin = switch (join) {
+ case ROUND -> PWebGPU.STROKE_JOIN_ROUND;
+ case MITER -> PWebGPU.STROKE_JOIN_MITER;
+ case BEVEL -> PWebGPU.STROKE_JOIN_BEVEL;
+ default -> PWebGPU.STROKE_JOIN_ROUND;
+ };
+ PWebGPU.setStrokeJoin(graphicsId, nativeJoin);
+ }
+
+ // ── Blend mode ──────────────────────────────────────────────────────
+
+ @Override
+ public void blendMode(int mode) {
+ super.blendMode(mode);
+ if (graphicsId == 0) {
+ return;
+ }
+ byte nativeMode = switch (mode) {
+ case BLEND -> PWebGPU.BLEND_MODE_BLEND;
+ case ADD -> PWebGPU.BLEND_MODE_ADD;
+ case SUBTRACT -> PWebGPU.BLEND_MODE_SUBTRACT;
+ case DARKEST -> PWebGPU.BLEND_MODE_DARKEST;
+ case LIGHTEST -> PWebGPU.BLEND_MODE_LIGHTEST;
+ case DIFFERENCE -> PWebGPU.BLEND_MODE_DIFFERENCE;
+ case EXCLUSION -> PWebGPU.BLEND_MODE_EXCLUSION;
+ case MULTIPLY -> PWebGPU.BLEND_MODE_MULTIPLY;
+ case SCREEN -> PWebGPU.BLEND_MODE_SCREEN;
+ case REPLACE -> PWebGPU.BLEND_MODE_REPLACE;
+ default -> PWebGPU.BLEND_MODE_BLEND;
+ };
+ PWebGPU.setBlendMode(graphicsId, nativeMode);
+ }
+
+ // ── 2D primitives ───────────────────────────────────────────────────
+
+ @Override
+ protected void rectImpl(float x1, float y1, float x2, float y2) {
+ rectImpl(x1, y1, x2, y2, 0, 0, 0, 0);
+ }
+
+ @Override
+ protected void rectImpl(float x1, float y1, float x2, float y2,
+ float tl, float tr, float br, float bl) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.rect(graphicsId, x1, y1, x2 - x1, y2 - y1, tl, tr, br, bl);
+ }
+
+ @Override
+ protected void ellipseImpl(float a, float b, float c, float d) {
+ if (graphicsId == 0) {
+ return;
+ }
+ // ellipseImpl receives corner-form; native expects center.
+ PWebGPU.ellipse(graphicsId, a + c / 2f, b + d / 2f, c, d);
+ }
+
+ @Override
+ protected void arcImpl(float a, float b, float c, float d,
+ float start, float stop, int mode) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.arc(graphicsId, a, b, c, d, start, stop, (byte) mode);
+ }
+
+ @Override
+ public void line(float x1, float y1, float x2, float y2) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.line(graphicsId, x1, y1, x2, y2);
+ }
+
+ @Override
+ public void point(float x, float y) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.point(graphicsId, x, y);
+ }
+
+ @Override
+ public void triangle(float x1, float y1, float x2, float y2, float x3, float y3) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.triangle(graphicsId, x1, y1, x2, y2, x3, y3);
+ }
+
+ @Override
+ public void quad(float x1, float y1, float x2, float y2,
+ float x3, float y3, float x4, float y4) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.quad(graphicsId, x1, y1, x2, y2, x3, y3, x4, y4);
+ }
+
+ // ── Curves ──────────────────────────────────────────────────────────
+
+ @Override
+ public void bezier(float x1, float y1, float x2, float y2,
+ float x3, float y3, float x4, float y4) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.bezier(graphicsId, x1, y1, x2, y2, x3, y3, x4, y4);
+ }
+
+ @Override
+ public void curve(float x1, float y1, float x2, float y2,
+ float x3, float y3, float x4, float y4) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.curve(graphicsId, x1, y1, x2, y2, x3, y3, x4, y4);
+ }
+
+ // ── 3D shapes ───────────────────────────────────────────────────────
+
+ @Override
+ public void box(float w, float h, float d) {
+ if (graphicsId == 0) {
+ return;
+ }
+ long boxGeometry = PWebGPU.geometryBox(w, h, d);
+ PWebGPU.model(graphicsId, boxGeometry);
+ pendingDestroy.add(boxGeometry);
+ }
+
+ @Override
+ public void sphere(float r) {
+ if (graphicsId == 0) {
+ return;
+ }
+ long sphereGeometry = PWebGPU.geometrySphere(r, sphereDetailU, sphereDetailV);
+ PWebGPU.model(graphicsId, sphereGeometry);
+ pendingDestroy.add(sphereGeometry);
+ }
+
+ // ── Vertex shapes ───────────────────────────────────────────────────
+
+ @Override
+ public void beginShape(int kind) {
+ super.beginShape(kind);
+ if (graphicsId == 0) {
+ return;
+ }
+ shapeKind = kind;
+ byte topology = shapeKindToTopology(kind);
+ currentGeometry = PWebGPU.geometryCreate(topology);
+ }
+
+ private byte shapeKindToTopology(int kind) {
+ return switch (kind) {
+ case POINTS -> PWebGPU.TOPOLOGY_POINT_LIST;
+ case LINES -> PWebGPU.TOPOLOGY_LINE_LIST;
+ case LINE_STRIP -> PWebGPU.TOPOLOGY_LINE_STRIP;
+ case TRIANGLES -> PWebGPU.TOPOLOGY_TRIANGLE_LIST;
+ case TRIANGLE_STRIP -> PWebGPU.TOPOLOGY_TRIANGLE_STRIP;
+ case TRIANGLE_FAN, QUADS, QUAD_STRIP, POLYGON -> PWebGPU.TOPOLOGY_TRIANGLE_LIST;
+ default -> PWebGPU.TOPOLOGY_TRIANGLE_LIST;
+ };
+ }
+
+ @Override
+ public void normal(float nx, float ny, float nz) {
+ normalX = nx;
+ normalY = ny;
+ normalZ = nz;
+ }
+
+ @Override
+ public void vertex(float x, float y) {
+ vertex(x, y, 0);
+ }
+
+ @Override
+ public void vertex(float x, float y, float z) {
+ if (currentGeometry == 0) {
+ return;
+ }
+ PWebGPU.geometryColor(currentGeometry, fillR, fillG, fillB, fillA);
+ PWebGPU.geometryNormal(currentGeometry, normalX, normalY, normalZ);
+ PWebGPU.geometryVertex(currentGeometry, x, y, z);
+ }
+
+ @Override
+ public void endShape(int mode) {
+ if (graphicsId == 0 || currentGeometry == 0) {
+ return;
+ }
+
+ if (shapeKind == QUADS) {
+ int vertexCount = PWebGPU.geometryVertexCount(currentGeometry);
+ for (int i = 0; i < vertexCount; i += 4) {
+ PWebGPU.geometryIndex(currentGeometry, i);
+ PWebGPU.geometryIndex(currentGeometry, i + 1);
+ PWebGPU.geometryIndex(currentGeometry, i + 2);
+ PWebGPU.geometryIndex(currentGeometry, i);
+ PWebGPU.geometryIndex(currentGeometry, i + 2);
+ PWebGPU.geometryIndex(currentGeometry, i + 3);
+ }
+ }
+
+ PWebGPU.model(graphicsId, currentGeometry);
+ pendingDestroy.add(currentGeometry);
+ currentGeometry = 0;
+ }
+
+ // ── Transform matrix ────────────────────────────────────────────────
+
+ @Override
+ public void pushMatrix() {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.pushMatrix(graphicsId);
+ }
+
+ @Override
+ public void popMatrix() {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.popMatrix(graphicsId);
+ }
+
+ @Override
+ public void resetMatrix() {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.resetMatrix(graphicsId);
+ }
+
+ @Override
+ public void translate(float x, float y) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.translate(graphicsId, x, y);
+ }
+
+ @Override
+ public void rotate(float angle) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.rotate(graphicsId, angle);
+ }
+
+ @Override
+ public void scale(float x, float y) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.scale(graphicsId, x, y);
+ }
+
+ @Override
+ public void shearX(float angle) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.shearX(graphicsId, angle);
+ }
+
+ @Override
+ public void shearY(float angle) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.shearY(graphicsId, angle);
+ }
+
+ // ── 3D camera / projection ──────────────────────────────────────────
+
+ @Override
+ public void camera(float eyeX, float eyeY, float eyeZ,
+ float centerX, float centerY, float centerZ,
+ float upX, float upY, float upZ) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.mode3d(graphicsId);
+ PWebGPU.transformSetPosition(graphicsId, eyeX, eyeY, eyeZ);
+ PWebGPU.transformLookAt(graphicsId, centerX, centerY, centerZ);
+ }
+
+ public void cameraPosition(float x, float y, float z) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.transformSetPosition(graphicsId, x, y, z);
+ }
+
+ public void cameraLookAt(float x, float y, float z) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.transformLookAt(graphicsId, x, y, z);
+ }
+
+ public void mode3d() {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.mode3d(graphicsId);
+ }
+
+ @Override
+ public void perspective(float fov, float aspect, float near, float far) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.mode3d(graphicsId);
+ PWebGPU.perspective(graphicsId, fov, aspect, near, far);
+ }
+
+ @Override
+ public void ortho(float left, float right, float bottom, float top, float near, float far) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.ortho(graphicsId, left, right, bottom, top, near, far);
+ }
+
+ // ── Lights ───────────────────────────────────────────────────────────
+
+ @Override
+ public void directionalLight(float r, float g, float b,
+ float nx, float ny, float nz) {
+ if (graphicsId == 0) return;
+ long light = PWebGPU.lightCreateDirectional(graphicsId, r, g, b, 1.0f, 600.0f);
+ PWebGPU.transformSetRotation(light, nx, ny, nz);
+ }
+
+ @Override
+ public void pointLight(float r, float g, float b,
+ float x, float y, float z) {
+ if (graphicsId == 0) return;
+ long light = PWebGPU.lightCreatePoint(graphicsId, r, g, b, 1.0f, 100000.0f, 800.0f, 0.0f);
+ PWebGPU.transformSetPosition(light, x, y, z);
+ }
+
+ public long directionalLight(float r, float g, float b, float illuminance) {
+ if (graphicsId == 0) return 0;
+ return PWebGPU.lightCreateDirectional(graphicsId, r, g, b, 1.0f, illuminance);
+ }
+
+ public long pointLight(float r, float g, float b,
+ float intensity, float range, float radius,
+ float x, float y, float z) {
+ if (graphicsId == 0) return 0;
+ long light = PWebGPU.lightCreatePoint(graphicsId, r, g, b, 1.0f, intensity, range, radius);
+ PWebGPU.transformSetPosition(light, x, y, z);
+ return light;
+ }
+
+ public long spotLight(float r, float g, float b,
+ float intensity, float range, float radius,
+ float innerAngle, float outerAngle) {
+ if (graphicsId == 0) return 0;
+ return PWebGPU.lightCreateSpot(graphicsId, r, g, b, 1.0f,
+ intensity, range, radius, innerAngle, outerAngle);
+ }
+
+ // ── Images / shapes ─────────────────────────────────────────────────
+
+ public PImageWebGPU createImage(int width, int height, int format) {
+ return new PImageWebGPU(width, height, format);
+ }
+
+ @Override
+ public PShape createShape() {
+ return new PShapeWebGPU(this, PShape.GEOMETRY);
+ }
+
+ @Override
+ public PShape createShape(int type) {
+ return new PShapeWebGPU(this, type);
+ }
+
+ public void model(long geometryId) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.model(graphicsId, geometryId);
+ }
+
+ // ── Materials ───────────────────────────────────────────────────────
+
+ public void useMaterial(Material mat) {
+ if (graphicsId == 0) {
+ return;
+ }
+ PWebGPU.material(graphicsId, mat.id());
+ }
+
+ // ── Helpers ──────────────────────────────────────────────────────────
+
+ private byte[] pixelsToRGBA(int[] pixels) {
+ byte[] rgba = new byte[pixels.length * 4];
+ for (int i = 0; i < pixels.length; i++) {
+ int pixel = pixels[i];
+ rgba[i * 4] = (byte) ((pixel >> 16) & 0xFF);
+ rgba[i * 4 + 1] = (byte) ((pixel >> 8) & 0xFF);
+ rgba[i * 4 + 2] = (byte) (pixel & 0xFF);
+ rgba[i * 4 + 3] = (byte) ((pixel >> 24) & 0xFF);
+ }
+ return rgba;
+ }
+}
diff --git a/core/src/processing/webgpu/PImageWebGPU.java b/core/src/processing/webgpu/PImageWebGPU.java
new file mode 100644
index 0000000000..d60d6530aa
--- /dev/null
+++ b/core/src/processing/webgpu/PImageWebGPU.java
@@ -0,0 +1,28 @@
+package processing.webgpu;
+
+import processing.core.PImage;
+
+public class PImageWebGPU extends PImage {
+
+ protected long id = 0;
+
+ public PImageWebGPU() {
+ super();
+ }
+
+ public PImageWebGPU(int width, int height) {
+ super(width, height);
+ }
+
+ public PImageWebGPU(int width, int height, int format) {
+ super(width, height, format);
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long imageId) {
+ this.id = imageId;
+ }
+}
diff --git a/core/src/processing/webgpu/PShapeWebGPU.java b/core/src/processing/webgpu/PShapeWebGPU.java
new file mode 100644
index 0000000000..54793c7a28
--- /dev/null
+++ b/core/src/processing/webgpu/PShapeWebGPU.java
@@ -0,0 +1,290 @@
+package processing.webgpu;
+
+import processing.core.PGraphics;
+import processing.core.PShape;
+import processing.core.PVector;
+
+/**
+ * WebGPU implementation of PShape.
+ */
+public class PShapeWebGPU extends PShape {
+
+ /** Reference to the graphics context */
+ protected PGraphicsWebGPU pg;
+
+ /** Native geometry ID (0 = not yet created) */
+ protected long geometryId = 0;
+
+ /** Native layout ID for custom vertex attributes */
+ protected long layoutId = 0;
+
+ /** Current topology for this shape */
+ protected byte topology = PWebGPU.TOPOLOGY_TRIANGLE_LIST;
+
+ /** Track if we're currently building the shape */
+ protected boolean building = false;
+
+ /** Pending normal for next vertex */
+ protected float normalX, normalY, normalZ;
+ protected boolean hasNormal = false;
+
+ /** Pending color for next vertex */
+ protected float colorR, colorG, colorB, colorA;
+ protected boolean hasColor = false;
+
+ /** Pending UV for next vertex */
+ protected float uvU, uvV;
+ protected boolean hasUV = false;
+
+ /**
+ * Create a new WebGPU shape.
+ */
+ public PShapeWebGPU(PGraphicsWebGPU pg, int family) {
+ this.pg = pg;
+ this.family = family;
+ }
+
+ /**
+ * Map Processing shape kinds to WebGPU topologies.
+ */
+ protected byte kindToTopology(int kind) {
+ return switch (kind) {
+ case POINTS -> PWebGPU.TOPOLOGY_POINT_LIST;
+ case LINES -> PWebGPU.TOPOLOGY_LINE_LIST;
+ case LINE_STRIP -> PWebGPU.TOPOLOGY_LINE_STRIP;
+ case TRIANGLES -> PWebGPU.TOPOLOGY_TRIANGLE_LIST;
+ case TRIANGLE_STRIP -> PWebGPU.TOPOLOGY_TRIANGLE_STRIP;
+ case TRIANGLE_FAN -> PWebGPU.TOPOLOGY_TRIANGLE_LIST; // Will need tessellation
+ case QUADS -> PWebGPU.TOPOLOGY_TRIANGLE_LIST; // Will need tessellation
+ case QUAD_STRIP -> PWebGPU.TOPOLOGY_TRIANGLE_STRIP;
+ default -> PWebGPU.TOPOLOGY_TRIANGLE_LIST;
+ };
+ }
+
+ @Override
+ public void beginShape(int kind) {
+ this.kind = kind;
+ this.topology = kindToTopology(kind);
+
+ // Create or reset the geometry
+ if (geometryId != 0) {
+ PWebGPU.geometryDestroy(geometryId);
+ }
+ geometryId = PWebGPU.geometryCreate(topology);
+ building = true;
+
+ // Reset pending attributes
+ hasNormal = false;
+ hasColor = false;
+ hasUV = false;
+ }
+
+ @Override
+ public void endShape(int mode) {
+ building = false;
+ // If CLOSE mode and we have a polygon, we might need to add indices
+ // For now, the geometry is ready to be rendered
+ }
+
+ @Override
+ public void normal(float nx, float ny, float nz) {
+ if (geometryId != 0) {
+ PWebGPU.geometryNormal(geometryId, nx, ny, nz);
+ }
+ normalX = nx;
+ normalY = ny;
+ normalZ = nz;
+ hasNormal = true;
+ }
+
+ @Override
+ public void vertex(float x, float y) {
+ vertex(x, y, 0);
+ }
+
+ @Override
+ public void vertex(float x, float y, float z) {
+ if (geometryId == 0) {
+ return;
+ }
+ if (hasColor) {
+ PWebGPU.geometryColor(geometryId, colorR, colorG, colorB, colorA);
+ }
+ if (hasUV) {
+ PWebGPU.geometryUv(geometryId, uvU, uvV);
+ }
+ PWebGPU.geometryVertex(geometryId, x, y, z);
+ }
+
+ @Override
+ public void vertex(float x, float y, float u, float v) {
+ texture(u, v);
+ vertex(x, y, 0);
+ }
+
+ @Override
+ public void vertex(float x, float y, float z, float u, float v) {
+ texture(u, v);
+ vertex(x, y, z);
+ }
+
+ /**
+ * Set texture coordinates for the next vertex.
+ */
+ public void texture(float u, float v) {
+ uvU = u;
+ uvV = v;
+ hasUV = true;
+ }
+
+ /**
+ * Set the fill color for subsequent vertices.
+ */
+ @Override
+ public void fill(int rgb) {
+ colorR = ((rgb >> 16) & 0xFF) / 255f;
+ colorG = ((rgb >> 8) & 0xFF) / 255f;
+ colorB = (rgb & 0xFF) / 255f;
+ colorA = ((rgb >> 24) & 0xFF) / 255f;
+ hasColor = true;
+ }
+
+ @Override
+ public void fill(float gray) {
+ colorR = colorG = colorB = gray / 255f;
+ colorA = 1f;
+ hasColor = true;
+ }
+
+ @Override
+ public void fill(float r, float g, float b) {
+ colorR = r / 255f;
+ colorG = g / 255f;
+ colorB = b / 255f;
+ colorA = 1f;
+ hasColor = true;
+ }
+
+ @Override
+ public void fill(float r, float g, float b, float a) {
+ colorR = r / 255f;
+ colorG = g / 255f;
+ colorB = b / 255f;
+ colorA = a / 255f;
+ hasColor = true;
+ }
+
+ /**
+ * Add an index for indexed rendering.
+ */
+ public void index(int i) {
+ if (geometryId != 0) {
+ PWebGPU.geometryIndex(geometryId, i);
+ }
+ }
+
+ @Override
+ public int getVertexCount() {
+ if (geometryId == 0) {
+ return 0;
+ }
+ return PWebGPU.geometryVertexCount(geometryId);
+ }
+
+ /**
+ * Get the index count for this shape.
+ */
+ public int getIndexCount() {
+ if (geometryId == 0) {
+ return 0;
+ }
+ return PWebGPU.geometryIndexCount(geometryId);
+ }
+
+ @Override
+ public void setVertex(int index, float x, float y, float z) {
+ if (geometryId != 0) {
+ PWebGPU.geometrySetVertex(geometryId, index, x, y, z);
+ }
+ }
+
+ @Override
+ public void setNormal(int index, float nx, float ny, float nz) {
+ if (geometryId != 0) {
+ PWebGPU.geometrySetNormal(geometryId, index, nx, ny, nz);
+ }
+ }
+
+ /**
+ * Set the color of a specific vertex.
+ */
+ public void setColor(int index, float r, float g, float b, float a) {
+ if (geometryId != 0) {
+ PWebGPU.geometrySetColor(geometryId, index, r, g, b, a);
+ }
+ }
+
+ /**
+ * Set the UV coordinates of a specific vertex.
+ */
+ public void setUv(int index, float u, float v) {
+ if (geometryId != 0) {
+ PWebGPU.geometrySetUv(geometryId, index, u, v);
+ }
+ }
+
+ /**
+ * Get the native geometry ID for direct rendering.
+ */
+ public long getGeometryId() {
+ return geometryId;
+ }
+
+ /**
+ * Draw this shape using the associated graphics context.
+ */
+ public void draw() {
+ if (geometryId != 0 && pg != null) {
+ pg.model(geometryId);
+ }
+ }
+
+ @Override
+ protected void drawImpl(PGraphics g) {
+ if (geometryId != 0 && g instanceof PGraphicsWebGPU webgpu) {
+ webgpu.model(geometryId);
+ }
+ }
+
+ /**
+ * Release native resources.
+ */
+ public void dispose() {
+ if (geometryId != 0) {
+ PWebGPU.geometryDestroy(geometryId);
+ geometryId = 0;
+ }
+ if (layoutId != 0) {
+ PWebGPU.geometryLayoutDestroy(layoutId);
+ layoutId = 0;
+ }
+ }
+
+ /**
+ * Create a box geometry.
+ */
+ public static PShapeWebGPU createBox(PGraphicsWebGPU pg, float width, float height, float depth) {
+ PShapeWebGPU shape = new PShapeWebGPU(pg, GEOMETRY);
+ shape.geometryId = PWebGPU.geometryBox(width, height, depth);
+ return shape;
+ }
+
+ /**
+ * Create a sphere geometry.
+ */
+ public static PShapeWebGPU createSphere(PGraphicsWebGPU pg, float radius, int sectors, int stacks) {
+ PShapeWebGPU shape = new PShapeWebGPU(pg, GEOMETRY);
+ shape.geometryId = PWebGPU.geometrySphere(radius, sectors, stacks);
+ return shape;
+ }
+}
diff --git a/core/src/processing/webgpu/PSurfaceGLFW.java b/core/src/processing/webgpu/PSurfaceGLFW.java
new file mode 100644
index 0000000000..852229729e
--- /dev/null
+++ b/core/src/processing/webgpu/PSurfaceGLFW.java
@@ -0,0 +1,635 @@
+package processing.webgpu;
+
+import org.lwjgl.glfw.*;
+import org.lwjgl.system.MemoryUtil;
+import org.lwjgl.system.Platform;
+
+import processing.core.PApplet;
+import processing.core.PConstants;
+import processing.core.PGraphics;
+import processing.core.PImage;
+import processing.core.PSurface;
+import processing.event.Event;
+import processing.event.KeyEvent;
+import processing.event.MouseEvent;
+
+import java.io.File;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+public class PSurfaceGLFW implements PSurface {
+
+ protected PApplet sketch;
+ protected PGraphics graphics;
+
+ protected long window;
+ protected long display;
+ protected boolean running = false;
+
+ protected boolean paused;
+ private final Lock pauseLock = new ReentrantLock();
+ private final Condition pauseCondition = pauseLock.newCondition();
+
+ protected float frameRateTarget = 60;
+ protected long frameRatePeriod = 1000000000L / 60L;
+
+ private static final AtomicInteger windowCount = new AtomicInteger(0);
+ private static AtomicBoolean glfwInitialized = new AtomicBoolean(false);
+
+ private GLFWFramebufferSizeCallback framebufferSizeCallback;
+ private GLFWWindowPosCallback windowPosCallback;
+ private GLFWCursorPosCallback cursorPosCallback;
+ private GLFWMouseButtonCallback mouseButtonCallback;
+ private GLFWScrollCallback scrollCallback;
+ private GLFWKeyCallback keyCallback;
+ private GLFWCharCallback charCallback;
+ private GLFWCursorEnterCallback cursorEnterCallback;
+ private GLFWWindowFocusCallback windowFocusCallback;
+
+ private double lastCursorX;
+ private double lastCursorY;
+ private int currentMouseButton;
+ private int currentModifiers;
+
+ // Cursor callbacks fire from inside glfwPollEvents and must do no FFI
+ // or allocation, or a high-poll-rate mouse can refill events faster
+ // than glfwPollEvents drains. Buffer the raw (x, y) here and replay
+ // them in runDrawLoop after polling returns.
+ private double[] cursorXs = new double[256];
+ private double[] cursorYs = new double[256];
+ private int cursorCount;
+
+ public PSurfaceGLFW(PGraphics graphics) {
+ this.graphics = graphics;
+ }
+
+ @Override
+ public void initOffscreen(PApplet sketch) {
+ throw new IllegalStateException("PSurfaceGLFW does not support offscreen rendering");
+ }
+
+ @Override
+ public void initFrame(PApplet sketch) {
+ this.sketch = sketch;
+
+ if (glfwInitialized.compareAndSet(false, true)) {
+ GLFWErrorCallback.createPrint(System.err).set();
+ if (!GLFW.glfwInit()) {
+ glfwInitialized.set(false);
+ throw new IllegalStateException("Failed to initialize GLFW");
+ }
+ System.out.println("PSurfaceGLFW: GLFW initialized successfully");
+ }
+
+ GLFW.glfwDefaultWindowHints();
+ GLFW.glfwWindowHint(GLFW.GLFW_CLIENT_API, GLFW.GLFW_NO_API);
+ GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);
+ GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_FALSE);
+
+ window = GLFW.glfwCreateWindow(sketch.sketchWidth(), sketch.sketchHeight(), "Processing",
+ MemoryUtil.NULL, MemoryUtil.NULL);
+ if (window == MemoryUtil.NULL) {
+ throw new RuntimeException("Failed to create GLFW window");
+ }
+
+ display = GLFW.glfwGetPrimaryMonitor();
+
+ windowCount.incrementAndGet();
+
+ initListeners();
+
+ if (graphics instanceof PGraphicsWebGPU webgpu) {
+ PWebGPU.init();
+
+ long windowHandle = getWindowHandle();
+ long displayHandle = getDisplayHandle();
+ int width = sketch.sketchWidth();
+ int height = sketch.sketchHeight();
+ float scaleFactor = sketch.sketchPixelDensity();
+
+ webgpu.initWebGPUSurface(windowHandle, displayHandle, width, height, scaleFactor);
+ }
+ }
+
+ protected void initListeners() {
+ long surfaceId = getSurfaceId();
+
+ // glfwSet*Callback returns the *previous* callback, not the new one.
+ // The new wrapper must be held in a field or it will be GC'd while
+ // still registered with native GLFW and events will silently stop.
+
+ framebufferSizeCallback = GLFWFramebufferSizeCallback.create((win, w, h) -> {
+ if (sketch != null) sketch.postWindowResized(w, h);
+ });
+ GLFW.glfwSetFramebufferSizeCallback(window, framebufferSizeCallback);
+
+ windowPosCallback = GLFWWindowPosCallback.create((win, xpos, ypos) -> {
+ if (sketch != null) sketch.postWindowMoved(xpos, ypos);
+ });
+ GLFW.glfwSetWindowPosCallback(window, windowPosCallback);
+
+ cursorPosCallback = GLFWCursorPosCallback.create((win, xpos, ypos) -> {
+ int n = cursorCount;
+ if (n >= cursorXs.length) {
+ int newLen = cursorXs.length * 2;
+ double[] nx = new double[newLen];
+ double[] ny = new double[newLen];
+ System.arraycopy(cursorXs, 0, nx, 0, n);
+ System.arraycopy(cursorYs, 0, ny, 0, n);
+ cursorXs = nx;
+ cursorYs = ny;
+ }
+ cursorXs[n] = xpos;
+ cursorYs[n] = ypos;
+ cursorCount = n + 1;
+ lastCursorX = xpos;
+ lastCursorY = ypos;
+ });
+ GLFW.glfwSetCursorPosCallback(window, cursorPosCallback);
+
+ mouseButtonCallback = GLFWMouseButtonCallback.create((win, button, action, mods) -> {
+ int peButton = switch (button) {
+ case GLFW.GLFW_MOUSE_BUTTON_LEFT -> PConstants.LEFT;
+ case GLFW.GLFW_MOUSE_BUTTON_MIDDLE -> PConstants.CENTER;
+ case GLFW.GLFW_MOUSE_BUTTON_RIGHT -> PConstants.RIGHT;
+ default -> 0;
+ };
+ currentModifiers = glfwModsToProcessing(mods);
+ boolean pressed = (action == GLFW.GLFW_PRESS);
+ if (surfaceId != 0 && peButton != 0) {
+ byte btn = (byte) (peButton == PConstants.LEFT ? 0
+ : peButton == PConstants.CENTER ? 1 : 2);
+ PWebGPU.inputMouseButton(surfaceId, btn, pressed);
+ }
+ if (peButton != 0) {
+ if (pressed) currentMouseButton = peButton;
+ if (sketch != null) {
+ int peAction = pressed ? MouseEvent.PRESS : MouseEvent.RELEASE;
+ sketch.postEvent(new MouseEvent(null, System.currentTimeMillis(),
+ peAction, currentModifiers,
+ (int) lastCursorX, (int) lastCursorY, peButton, 1));
+ }
+ if (!pressed) currentMouseButton = 0;
+ }
+ });
+ GLFW.glfwSetMouseButtonCallback(window, mouseButtonCallback);
+
+ scrollCallback = GLFWScrollCallback.create((win, xoffset, yoffset) -> {
+ if (surfaceId != 0) {
+ PWebGPU.inputScroll(surfaceId, (float) xoffset, (float) yoffset);
+ }
+ if (sketch != null) {
+ // Flip: Processing wheel is negative-up, GLFW is positive-up.
+ sketch.postEvent(new MouseEvent(null, System.currentTimeMillis(),
+ MouseEvent.WHEEL, currentModifiers,
+ (int) lastCursorX, (int) lastCursorY, 0,
+ (int) -yoffset));
+ }
+ });
+ GLFW.glfwSetScrollCallback(window, scrollCallback);
+
+ keyCallback = GLFWKeyCallback.create((win, key, scancode, action, mods) -> {
+ currentModifiers = glfwModsToProcessing(mods);
+ if (surfaceId != 0 && action != GLFW.GLFW_REPEAT) {
+ PWebGPU.inputKey(surfaceId, key, action == GLFW.GLFW_PRESS);
+ }
+ if (sketch != null && action != GLFW.GLFW_REPEAT) {
+ int peAction = (action == GLFW.GLFW_PRESS) ? KeyEvent.PRESS : KeyEvent.RELEASE;
+ sketch.postEvent(new KeyEvent(null, System.currentTimeMillis(),
+ peAction, currentModifiers,
+ glfwKeyToChar(key), key));
+ }
+ });
+ GLFW.glfwSetKeyCallback(window, keyCallback);
+
+ charCallback = GLFWCharCallback.create((win, codepoint) -> {
+ if (surfaceId != 0) {
+ PWebGPU.inputChar(surfaceId, 0, codepoint);
+ }
+ if (sketch != null) {
+ sketch.postEvent(new KeyEvent(null, System.currentTimeMillis(),
+ KeyEvent.TYPE, currentModifiers,
+ (char) codepoint, 0));
+ }
+ });
+ GLFW.glfwSetCharCallback(window, charCallback);
+
+ cursorEnterCallback = GLFWCursorEnterCallback.create((win, entered) -> {
+ if (surfaceId != 0) {
+ if (entered) PWebGPU.inputCursorEnter(surfaceId);
+ else PWebGPU.inputCursorLeave(surfaceId);
+ }
+ });
+ GLFW.glfwSetCursorEnterCallback(window, cursorEnterCallback);
+
+ windowFocusCallback = GLFWWindowFocusCallback.create((win, focused) -> {
+ if (surfaceId != 0) PWebGPU.inputFocus(surfaceId, focused);
+ });
+ GLFW.glfwSetWindowFocusCallback(window, windowFocusCallback);
+ }
+
+ private static int glfwModsToProcessing(int mods) {
+ int m = 0;
+ if ((mods & GLFW.GLFW_MOD_SHIFT) != 0) m |= Event.SHIFT;
+ if ((mods & GLFW.GLFW_MOD_CONTROL) != 0) m |= Event.CTRL;
+ if ((mods & GLFW.GLFW_MOD_ALT) != 0) m |= Event.ALT;
+ if ((mods & GLFW.GLFW_MOD_SUPER) != 0) m |= Event.META;
+ return m;
+ }
+
+ private static char glfwKeyToChar(int glfwKey) {
+ if (glfwKey >= 32 && glfwKey < 127) {
+ return (char) glfwKey;
+ }
+ return switch (glfwKey) {
+ case GLFW.GLFW_KEY_ENTER, GLFW.GLFW_KEY_KP_ENTER -> '\n';
+ case GLFW.GLFW_KEY_TAB -> '\t';
+ case GLFW.GLFW_KEY_BACKSPACE -> '\b';
+ case GLFW.GLFW_KEY_ESCAPE -> 27;
+ case GLFW.GLFW_KEY_DELETE -> 127;
+ default -> PConstants.CODED;
+ };
+ }
+
+ private long getSurfaceId() {
+ if (graphics instanceof PGraphicsWebGPU webgpu) {
+ return webgpu.getSurfaceId();
+ }
+ return 0;
+ }
+
+ @Override
+ public Object getNative() {
+ return window;
+ }
+
+ public long getWindowHandle() {
+ if (Platform.get() == Platform.MACOSX) {
+ return GLFWNativeCocoa.glfwGetCocoaWindow(window);
+ } else if (Platform.get() == Platform.WINDOWS) {
+ return GLFWNativeWin32.glfwGetWin32Window(window);
+ } else if (Platform.get() == Platform.LINUX) {
+ // TODO: need to check if x11 or wayland
+ return GLFWNativeWayland.glfwGetWaylandWindow(window);
+ } else {
+ throw new UnsupportedOperationException("Window handle retrieval not implemented for this platform");
+ }
+ }
+
+ public long getDisplayHandle() {
+ if (Platform.get() == Platform.MACOSX) {
+ return 0;
+ } else if (Platform.get() == Platform.WINDOWS) {
+ return 0;
+ } else if (Platform.get() == Platform.LINUX) {
+ // TODO: need to check if x11 or wayland
+ return GLFWNativeWayland.glfwGetWaylandDisplay();
+ } else {
+ throw new UnsupportedOperationException("Window handle retrieval not implemented for this platform");
+ }
+ }
+
+ @Override
+ public void setTitle(String title) {
+ if (window != MemoryUtil.NULL) {
+ GLFW.glfwSetWindowTitle(window, title);
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (window != MemoryUtil.NULL) {
+ if (visible) {
+ GLFW.glfwShowWindow(window);
+ } else {
+ GLFW.glfwHideWindow(window);
+ }
+ }
+ }
+
+ @Override
+ public void setResizable(boolean resizable) {
+ if (window != MemoryUtil.NULL) {
+ GLFW.glfwSetWindowAttrib(window, GLFW.GLFW_RESIZABLE,
+ resizable ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE);
+ }
+ }
+
+ @Override
+ public void setAlwaysOnTop(boolean always) {
+ if (window != MemoryUtil.NULL) {
+ GLFW.glfwSetWindowAttrib(window, GLFW.GLFW_FLOATING,
+ always ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE);
+ }
+ }
+
+ @Override
+ public void setIcon(PImage icon) {
+ // TODO: set icon with glfw
+ }
+
+ @Override
+ public void placeWindow(int[] location, int[] editorLocation) {
+ if (window == MemoryUtil.NULL) return;
+
+ int x, y;
+ if (location != null) {
+ x = location[0];
+ y = location[1];
+ } else if (editorLocation != null) {
+ x = editorLocation[0] - 20;
+ y = editorLocation[1];
+
+ if (x - sketch.sketchWidth() < 10) {
+ long monitor = GLFW.glfwGetPrimaryMonitor();
+ var vidmode = GLFW.glfwGetVideoMode(monitor);
+ if (vidmode != null) {
+ x = (vidmode.width() - sketch.sketchWidth()) / 2;
+ y = (vidmode.height() - sketch.sketchHeight()) / 2;
+ } else {
+ x = 100;
+ y = 100;
+ }
+ }
+ } else {
+ long monitor = GLFW.glfwGetPrimaryMonitor();
+ var vidmode = GLFW.glfwGetVideoMode(monitor);
+ if (vidmode != null) {
+ x = (vidmode.width() - sketch.sketchWidth()) / 2;
+ y = (vidmode.height() - sketch.sketchHeight()) / 2;
+ } else {
+ x = 100;
+ y = 100;
+ }
+ }
+
+ GLFW.glfwSetWindowPos(window, x, y);
+ }
+
+ @Override
+ public void placePresent(int stopColor) {
+ // TODO: implement present mode support
+ }
+
+ @Override
+ public void setLocation(int x, int y) {
+ if (window != MemoryUtil.NULL) {
+ GLFW.glfwSetWindowPos(window, x, y);
+ }
+ }
+
+ @Override
+ public void setSize(int width, int height) {
+ if (width == sketch.width && height == sketch.height) {
+ return;
+ }
+
+ sketch.width = width;
+ sketch.height = height;
+ graphics.setSize(width, height);
+
+ if (window != MemoryUtil.NULL) {
+ GLFW.glfwSetWindowSize(window, width, height);
+ }
+ }
+
+ @Override
+ public void setFrameRate(float fps) {
+ frameRateTarget = fps;
+ frameRatePeriod = (long) (1000000000.0 / frameRateTarget);
+ }
+
+ @Override
+ public void setCursor(int kind) {
+ // TODO: implement cursor types
+ }
+
+ @Override
+ public void setCursor(PImage image, int hotspotX, int hotspotY) {
+ // TODO: implement custom cursor
+ }
+
+ @Override
+ public void showCursor() {
+ if (window != MemoryUtil.NULL) {
+ GLFW.glfwSetInputMode(window, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL);
+ }
+ }
+
+ @Override
+ public void hideCursor() {
+ if (window != MemoryUtil.NULL) {
+ GLFW.glfwSetInputMode(window, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_HIDDEN);
+ }
+ }
+
+ @Override
+ public PImage loadImage(String path, Object... args) {
+ // TODO: implement image loading without awt
+ throw new UnsupportedOperationException("Image loading not yet implemented for WebGPU");
+ }
+
+ @Override
+ public boolean openLink(String url) {
+ // TODO: implement links without awt
+ return false;
+ }
+
+ @Override
+ public void selectInput(String prompt, String callback, File file, Object callbackObject) {
+ throw new UnsupportedOperationException("File dialogs not yet implemented for WebGPU");
+ }
+
+ @Override
+ public void selectOutput(String prompt, String callback, File file, Object callbackObject) {
+ throw new UnsupportedOperationException("File dialogs not yet implemented for WebGPU");
+ }
+
+ @Override
+ public void selectFolder(String prompt, String callback, File file, Object callbackObject) {
+ throw new UnsupportedOperationException("Folder selection not yet implemented for WebGPU");
+ }
+
+ @Override
+ public void startThread() {
+ if (running) {
+ throw new IllegalStateException("Draw loop already running");
+ }
+
+ running = true;
+ runDrawLoop();
+ }
+
+ protected void runDrawLoop() {
+ GLFW.glfwShowWindow(window);
+ // macOS: when the JVM is launched as a child of another GUI process,
+ // NSApplication isn't activated and input events stop after a brief
+ // initial spurt unless we force focus.
+ GLFW.glfwFocusWindow(window);
+
+ long beforeTime = System.nanoTime();
+ long overSleepTime = 0L;
+
+ sketch.start();
+
+ while (running) {
+ checkPause();
+
+ GLFW.glfwPollEvents();
+
+ if (GLFW.glfwWindowShouldClose(window)) {
+ sketch.exit();
+ break;
+ }
+
+ int n = cursorCount;
+ if (n > 0) {
+ long sid = getSurfaceId();
+ long now = System.currentTimeMillis();
+ for (int i = 0; i < n; i++) {
+ float x = (float) cursorXs[i];
+ float y = (float) cursorYs[i];
+ if (sid != 0) {
+ PWebGPU.inputMouseMove(sid, x, y);
+ }
+ if (sketch != null) {
+ int action = (currentMouseButton == 0) ? MouseEvent.MOVE : MouseEvent.DRAG;
+ sketch.postEvent(new MouseEvent(null, now, action, currentModifiers,
+ (int) x, (int) y, currentMouseButton, 0));
+ }
+ }
+ cursorCount = 0;
+ }
+
+ PWebGPU.inputFlush();
+
+ if (!sketch.finished) {
+ sketch.handleDraw();
+ }
+
+ long afterTime = System.nanoTime();
+ long timeDiff = afterTime - beforeTime;
+ long sleepTime = (frameRatePeriod - timeDiff) - overSleepTime;
+
+ if (sleepTime > 0) {
+ try {
+ Thread.sleep(sleepTime / 1000000L, (int) (sleepTime % 1000000L));
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ overSleepTime = (System.nanoTime() - afterTime) - sleepTime;
+ } else {
+ overSleepTime = 0L;
+ }
+
+ beforeTime = System.nanoTime();
+ }
+
+ sketch.dispose();
+ }
+
+ @Override
+ public void pauseThread() {
+ paused = true;
+ }
+
+ protected void checkPause() {
+ if (paused) {
+ pauseLock.lock();
+ try {
+ while (paused) {
+ pauseCondition.await();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ pauseLock.unlock();
+ }
+ }
+ }
+
+ @Override
+ public void resumeThread() {
+ pauseLock.lock();
+ try {
+ paused = false;
+ pauseCondition.signalAll();
+ } finally {
+ pauseLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean stopThread() {
+ if (!running) {
+ return false;
+ }
+
+ running = false;
+
+ try {
+ if (window != MemoryUtil.NULL) {
+ GLFW.glfwDestroyWindow(window);
+ window = MemoryUtil.NULL;
+
+ if (windowCount.decrementAndGet() == 0) {
+ if (glfwInitialized.compareAndSet(true, false)) {
+ GLFW.glfwTerminate();
+ System.out.println("PSurfaceGLFW: GLFW terminated");
+ }
+ }
+ }
+ } finally {
+ freeCallbacks();
+ }
+
+ return true;
+ }
+
+ private void freeCallbacks() {
+ if (framebufferSizeCallback != null) {
+ framebufferSizeCallback.free();
+ framebufferSizeCallback = null;
+ }
+ if (windowPosCallback != null) {
+ windowPosCallback.free();
+ windowPosCallback = null;
+ }
+ if (cursorPosCallback != null) {
+ cursorPosCallback.free();
+ cursorPosCallback = null;
+ }
+ if (mouseButtonCallback != null) {
+ mouseButtonCallback.free();
+ mouseButtonCallback = null;
+ }
+ if (scrollCallback != null) {
+ scrollCallback.free();
+ scrollCallback = null;
+ }
+ if (keyCallback != null) {
+ keyCallback.free();
+ keyCallback = null;
+ }
+ if (charCallback != null) {
+ charCallback.free();
+ charCallback = null;
+ }
+ if (cursorEnterCallback != null) {
+ cursorEnterCallback.free();
+ cursorEnterCallback = null;
+ }
+ if (windowFocusCallback != null) {
+ windowFocusCallback.free();
+ windowFocusCallback = null;
+ }
+ }
+
+ @Override
+ public boolean isStopped() {
+ return !running;
+ }
+}
diff --git a/core/src/processing/webgpu/PWebGPU.java b/core/src/processing/webgpu/PWebGPU.java
new file mode 100644
index 0000000000..fd3b5c751c
--- /dev/null
+++ b/core/src/processing/webgpu/PWebGPU.java
@@ -0,0 +1,878 @@
+package processing.webgpu;
+
+import processing.core.NativeLibrary;
+
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+
+import static java.lang.foreign.MemorySegment.NULL;
+import static processing.ffi.processing_h.*;
+import processing.ffi.Color;
+
+public class PWebGPU {
+
+ static {
+ ensureLoaded();
+ }
+
+ public static void ensureLoaded() {
+ NativeLibrary.ensureLoaded();
+ }
+
+ // ── Init / lifecycle ────────────────────────────────────────────────
+
+ public static void init() {
+ processing_init();
+ checkError();
+ }
+
+ public static void exit() {
+ processing_exit((byte) 0);
+ checkError();
+ }
+
+ // ── Surface ─────────────────────────────────────────────────────────
+
+ public static long createSurface(long windowHandle, long displayHandle, int width, int height, float scaleFactor) {
+ long surfaceId = processing_surface_create(windowHandle, displayHandle, width, height, scaleFactor);
+ checkError();
+ return surfaceId;
+ }
+
+ public static void destroySurface(long surfaceId) {
+ processing_surface_destroy(surfaceId);
+ checkError();
+ }
+
+ public static void windowResized(long surfaceId, int width, int height) {
+ processing_surface_resize(surfaceId, width, height);
+ checkError();
+ }
+
+ // ── Graphics context ────────────────────────────────────────────────
+
+ public static long graphicsCreate(long surfaceId, int width, int height) {
+ long graphicsId = processing_graphics_create(surfaceId, width, height);
+ checkError();
+ return graphicsId;
+ }
+
+ public static void graphicsDestroy(long graphicsId) {
+ processing_graphics_destroy(graphicsId);
+ checkError();
+ }
+
+ public static void beginDraw(long graphicsId) {
+ processing_begin_draw(graphicsId);
+ checkError();
+ }
+
+ public static void flush(long graphicsId) {
+ processing_flush(graphicsId);
+ checkError();
+ }
+
+ public static void endDraw(long graphicsId) {
+ processing_end_draw(graphicsId);
+ checkError();
+ }
+
+ // ── Background ──────────────────────────────────────────────────────
+
+ public static void backgroundColor(long graphicsId, float r, float g, float b, float a) {
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment color = allocateColor(arena, r, g, b, a);
+ processing_background_color(graphicsId, color);
+ checkError();
+ }
+ }
+
+ public static void backgroundImage(long graphicsId, long imageId) {
+ processing_background_image(graphicsId, imageId);
+ checkError();
+ }
+
+ // ── Color mode ──────────────────────────────────────────────────────
+
+ public static final byte COLOR_SPACE_SRGB = 0;
+ public static final byte COLOR_SPACE_HSB = 1;
+ public static final byte COLOR_SPACE_LINEAR = 2;
+
+ public static void colorMode(long graphicsId, byte space, float max1, float max2, float max3, float maxAlpha) {
+ processing_color_mode(graphicsId, space, max1, max2, max3, maxAlpha);
+ checkError();
+ }
+
+ // ── Fill / stroke ───────────────────────────────────────────────────
+
+ public static void setFill(long graphicsId, float r, float g, float b, float a) {
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment color = allocateColor(arena, r, g, b, a);
+ processing_set_fill(graphicsId, color);
+ checkError();
+ }
+ }
+
+ public static void setStrokeColor(long graphicsId, float r, float g, float b, float a) {
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment color = allocateColor(arena, r, g, b, a);
+ processing_set_stroke_color(graphicsId, color);
+ checkError();
+ }
+ }
+
+ public static void setStrokeWeight(long graphicsId, float weight) {
+ processing_set_stroke_weight(graphicsId, weight);
+ checkError();
+ }
+
+ public static void noFill(long graphicsId) {
+ processing_no_fill(graphicsId);
+ checkError();
+ }
+
+ public static void noStroke(long graphicsId) {
+ processing_no_stroke(graphicsId);
+ checkError();
+ }
+
+ // ── Stroke style ────────────────────────────────────────────────────
+
+ public static final byte STROKE_CAP_ROUND = 0;
+ public static final byte STROKE_CAP_SQUARE = 1;
+ public static final byte STROKE_CAP_PROJECT = 2;
+
+ public static final byte STROKE_JOIN_ROUND = 0;
+ public static final byte STROKE_JOIN_MITER = 1;
+ public static final byte STROKE_JOIN_BEVEL = 2;
+
+ public static void setStrokeCap(long graphicsId, byte cap) {
+ processing_set_stroke_cap(graphicsId, cap);
+ checkError();
+ }
+
+ public static void setStrokeJoin(long graphicsId, byte join) {
+ processing_set_stroke_join(graphicsId, join);
+ checkError();
+ }
+
+ // ── Shape modes ─────────────────────────────────────────────────────
+
+ public static void rectMode(long graphicsId, byte mode) {
+ processing_rect_mode(graphicsId, mode);
+ checkError();
+ }
+
+ public static void ellipseMode(long graphicsId, byte mode) {
+ processing_ellipse_mode(graphicsId, mode);
+ checkError();
+ }
+
+ // ── Blend modes ─────────────────────────────────────────────────────
+
+ public static final byte BLEND_MODE_BLEND = 0;
+ public static final byte BLEND_MODE_ADD = 1;
+ public static final byte BLEND_MODE_SUBTRACT = 2;
+ public static final byte BLEND_MODE_DARKEST = 3;
+ public static final byte BLEND_MODE_LIGHTEST = 4;
+ public static final byte BLEND_MODE_DIFFERENCE = 5;
+ public static final byte BLEND_MODE_EXCLUSION = 6;
+ public static final byte BLEND_MODE_MULTIPLY = 7;
+ public static final byte BLEND_MODE_SCREEN = 8;
+ public static final byte BLEND_MODE_REPLACE = 9;
+
+ public static void setBlendMode(long graphicsId, byte mode) {
+ processing_set_blend_mode(graphicsId, mode);
+ checkError();
+ }
+
+ // ── 2D drawing matrix ───────────────────────────────────────────────
+
+ public static void pushMatrix(long graphicsId) {
+ processing_push_matrix(graphicsId);
+ checkError();
+ }
+
+ public static void popMatrix(long graphicsId) {
+ processing_pop_matrix(graphicsId);
+ checkError();
+ }
+
+ public static void resetMatrix(long graphicsId) {
+ processing_reset_matrix(graphicsId);
+ checkError();
+ }
+
+ public static void translate(long graphicsId, float x, float y) {
+ processing_translate(graphicsId, x, y);
+ checkError();
+ }
+
+ public static void rotate(long graphicsId, float angle) {
+ processing_rotate(graphicsId, angle);
+ checkError();
+ }
+
+ public static void scale(long graphicsId, float x, float y) {
+ processing_scale(graphicsId, x, y);
+ checkError();
+ }
+
+ public static void shearX(long graphicsId, float angle) {
+ processing_shear_x(graphicsId, angle);
+ checkError();
+ }
+
+ public static void shearY(long graphicsId, float angle) {
+ processing_shear_y(graphicsId, angle);
+ checkError();
+ }
+
+ // ── 2D primitives ───────────────────────────────────────────────────
+
+ public static void rect(long graphicsId, float x, float y, float w, float h,
+ float tl, float tr, float br, float bl) {
+ processing_rect(graphicsId, x, y, w, h, tl, tr, br, bl);
+ checkError();
+ }
+
+ public static void ellipse(long graphicsId, float cx, float cy, float w, float h) {
+ processing_ellipse(graphicsId, cx, cy, w, h);
+ checkError();
+ }
+
+ public static void circle(long graphicsId, float cx, float cy, float d) {
+ processing_circle(graphicsId, cx, cy, d);
+ checkError();
+ }
+
+ public static void line(long graphicsId, float x1, float y1, float x2, float y2) {
+ processing_line(graphicsId, x1, y1, x2, y2);
+ checkError();
+ }
+
+ public static void triangle(long graphicsId, float x1, float y1, float x2, float y2,
+ float x3, float y3) {
+ processing_triangle(graphicsId, x1, y1, x2, y2, x3, y3);
+ checkError();
+ }
+
+ public static void quad(long graphicsId, float x1, float y1, float x2, float y2,
+ float x3, float y3, float x4, float y4) {
+ processing_quad(graphicsId, x1, y1, x2, y2, x3, y3, x4, y4);
+ checkError();
+ }
+
+ public static void point(long graphicsId, float x, float y) {
+ processing_point(graphicsId, x, y);
+ checkError();
+ }
+
+ public static void square(long graphicsId, float x, float y, float s) {
+ processing_square(graphicsId, x, y, s);
+ checkError();
+ }
+
+ public static void arc(long graphicsId, float cx, float cy, float w, float h,
+ float start, float stop, byte mode) {
+ processing_arc(graphicsId, cx, cy, w, h, start, stop, mode);
+ checkError();
+ }
+
+ public static void bezier(long graphicsId, float x1, float y1, float x2, float y2,
+ float x3, float y3, float x4, float y4) {
+ processing_bezier(graphicsId, x1, y1, x2, y2, x3, y3, x4, y4);
+ checkError();
+ }
+
+ public static void curve(long graphicsId, float x1, float y1, float x2, float y2,
+ float x3, float y3, float x4, float y4) {
+ processing_curve(graphicsId, x1, y1, x2, y2, x3, y3, x4, y4);
+ checkError();
+ }
+
+ // ── 3D primitives ───────────────────────────────────────────────────
+
+ public static void cylinder(long graphicsId, float radius, float height, int detail) {
+ processing_cylinder(graphicsId, radius, height, detail);
+ checkError();
+ }
+
+ public static void cone(long graphicsId, float radius, float height, int detail) {
+ processing_cone(graphicsId, radius, height, detail);
+ checkError();
+ }
+
+ public static void torus(long graphicsId, float radius, float tubeRadius, int majorSegments, int minorSegments) {
+ processing_torus(graphicsId, radius, tubeRadius, majorSegments, minorSegments);
+ checkError();
+ }
+
+ public static void plane(long graphicsId, float width, float height) {
+ processing_plane(graphicsId, width, height);
+ checkError();
+ }
+
+ public static void capsule(long graphicsId, float radius, float length, int detail) {
+ processing_capsule(graphicsId, radius, length, detail);
+ checkError();
+ }
+
+ public static void conicalFrustum(long graphicsId, float radiusTop, float radiusBottom, float height, int detail) {
+ processing_conical_frustum(graphicsId, radiusTop, radiusBottom, height, detail);
+ checkError();
+ }
+
+ public static void tetrahedron(long graphicsId, float radius) {
+ processing_tetrahedron(graphicsId, radius);
+ checkError();
+ }
+
+ // ── Vertex shapes ───────────────────────────────────────────────────
+
+ public static void beginShape(long graphicsId, byte kind) {
+ processing_begin_shape(graphicsId, kind);
+ checkError();
+ }
+
+ public static void endShape(long graphicsId, boolean close) {
+ processing_end_shape(graphicsId, close);
+ checkError();
+ }
+
+ public static void shapeVertex(long graphicsId, float x, float y) {
+ processing_vertex(graphicsId, x, y);
+ checkError();
+ }
+
+ public static void bezierVertex(long graphicsId, float cx1, float cy1, float cx2, float cy2, float x, float y) {
+ processing_bezier_vertex(graphicsId, cx1, cy1, cx2, cy2, x, y);
+ checkError();
+ }
+
+ public static void quadraticVertex(long graphicsId, float cx, float cy, float x, float y) {
+ processing_quadratic_vertex(graphicsId, cx, cy, x, y);
+ checkError();
+ }
+
+ public static void curveVertex(long graphicsId, float x, float y) {
+ processing_curve_vertex(graphicsId, x, y);
+ checkError();
+ }
+
+ public static void beginContour(long graphicsId) {
+ processing_begin_contour(graphicsId);
+ checkError();
+ }
+
+ public static void endContour(long graphicsId) {
+ processing_end_contour(graphicsId);
+ checkError();
+ }
+
+ // ── 3D mode / projection ────────────────────────────────────────────
+
+ public static void mode3d(long graphicsId) {
+ processing_mode_3d(graphicsId);
+ checkError();
+ }
+
+ public static void mode2d(long graphicsId) {
+ processing_mode_2d(graphicsId);
+ checkError();
+ }
+
+ public static void perspective(long graphicsId, float fov, float aspect, float near, float far) {
+ processing_perspective(graphicsId, fov, aspect, near, far);
+ checkError();
+ }
+
+ public static void ortho(long graphicsId, float left, float right, float bottom, float top, float near, float far) {
+ processing_ortho(graphicsId, left, right, bottom, top, near, far);
+ checkError();
+ }
+
+ // ── Entity transforms (3D objects: lights, geometry, etc.) ──────────
+
+ public static void transformSetPosition(long entityId, float x, float y, float z) {
+ processing_transform_set_position(entityId, x, y, z);
+ checkError();
+ }
+
+ public static void transformTranslate(long entityId, float x, float y, float z) {
+ processing_transform_translate(entityId, x, y, z);
+ checkError();
+ }
+
+ public static void transformSetRotation(long entityId, float x, float y, float z) {
+ processing_transform_set_rotation(entityId, x, y, z);
+ checkError();
+ }
+
+ public static void transformRotateX(long entityId, float angle) {
+ processing_transform_rotate_x(entityId, angle);
+ checkError();
+ }
+
+ public static void transformRotateY(long entityId, float angle) {
+ processing_transform_rotate_y(entityId, angle);
+ checkError();
+ }
+
+ public static void transformRotateZ(long entityId, float angle) {
+ processing_transform_rotate_z(entityId, angle);
+ checkError();
+ }
+
+ public static void transformRotateAxis(long entityId, float angle, float axisX, float axisY, float axisZ) {
+ processing_transform_rotate_axis(entityId, angle, axisX, axisY, axisZ);
+ checkError();
+ }
+
+ public static void transformSetScale(long entityId, float x, float y, float z) {
+ processing_transform_set_scale(entityId, x, y, z);
+ checkError();
+ }
+
+ public static void transformScale(long entityId, float x, float y, float z) {
+ processing_transform_scale(entityId, x, y, z);
+ checkError();
+ }
+
+ public static void transformLookAt(long entityId, float targetX, float targetY, float targetZ) {
+ processing_transform_look_at(entityId, targetX, targetY, targetZ);
+ checkError();
+ }
+
+ public static void transformReset(long entityId) {
+ processing_transform_reset(entityId);
+ checkError();
+ }
+
+ // ── Lights ──────────────────────────────────────────────────────────
+
+ public static long lightCreateDirectional(long graphicsId, float r, float g, float b, float a, float illuminance) {
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment color = allocateColor(arena, r, g, b, a);
+ long id = processing_light_create_directional(graphicsId, color, illuminance);
+ checkError();
+ return id;
+ }
+ }
+
+ public static long lightCreatePoint(long graphicsId, float r, float g, float b, float a,
+ float intensity, float range, float radius) {
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment color = allocateColor(arena, r, g, b, a);
+ long id = processing_light_create_point(graphicsId, color, intensity, range, radius);
+ checkError();
+ return id;
+ }
+ }
+
+ public static long lightCreateSpot(long graphicsId, float r, float g, float b, float a,
+ float intensity, float range, float radius,
+ float innerAngle, float outerAngle) {
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment color = allocateColor(arena, r, g, b, a);
+ long id = processing_light_create_spot(graphicsId, color, intensity, range, radius, innerAngle, outerAngle);
+ checkError();
+ return id;
+ }
+ }
+
+ // ── Materials ───────────────────────────────────────────────────────
+
+ public static long materialCreatePbr() {
+ long id = processing_material_create_pbr();
+ checkError();
+ return id;
+ }
+
+ public static void materialSetFloat(long matId, String name, float value) {
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment nameSegment = arena.allocateFrom(name);
+ processing_material_set_float(matId, nameSegment, value);
+ checkError();
+ }
+ }
+
+ public static void materialSetFloat4(long matId, String name, float r, float g, float b, float a) {
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment nameSegment = arena.allocateFrom(name);
+ processing_material_set_float4(matId, nameSegment, r, g, b, a);
+ checkError();
+ }
+ }
+
+ public static void materialDestroy(long matId) {
+ processing_material_destroy(matId);
+ checkError();
+ }
+
+ public static void material(long graphicsId, long matId) {
+ processing_material(graphicsId, matId);
+ checkError();
+ }
+
+ // ── Images ──────────────────────────────────────────────────────────
+
+ public static long imageCreate(int width, int height, byte[] data) {
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment dataSegment = arena.allocateFrom(java.lang.foreign.ValueLayout.JAVA_BYTE, data);
+ long imageId = processing_image_create(width, height, dataSegment, data.length);
+ checkError();
+ return imageId;
+ }
+ }
+
+ public static long imageLoad(String path) {
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment pathSegment = arena.allocateFrom(path);
+ long imageId = processing_image_load(pathSegment);
+ checkError();
+ return imageId;
+ }
+ }
+
+ public static void imageResize(long imageId, int newWidth, int newHeight) {
+ processing_image_resize(imageId, newWidth, newHeight);
+ checkError();
+ }
+
+ public static void imageReadback(long imageId, float[] buffer) {
+ try (Arena arena = Arena.ofConfined()) {
+ int numPixels = buffer.length / 4;
+ MemorySegment colorBuffer = Color.allocateArray(numPixels, arena);
+ processing_image_readback(imageId, colorBuffer, numPixels);
+ checkError();
+
+ for (int i = 0; i < numPixels; i++) {
+ MemorySegment color = Color.asSlice(colorBuffer, i);
+ buffer[i * 4] = Color.c1(color);
+ buffer[i * 4 + 1] = Color.c2(color);
+ buffer[i * 4 + 2] = Color.c3(color);
+ buffer[i * 4 + 3] = Color.a(color);
+ }
+ }
+ }
+
+ // ── Input: event ingestion (called by PSurfaceGLFW) ─────────────────
+
+ public static void inputMouseMove(long surfaceId, float x, float y) {
+ processing_input_mouse_move(surfaceId, x, y);
+ checkError();
+ }
+
+ public static void inputMouseButton(long surfaceId, byte button, boolean pressed) {
+ processing_input_mouse_button(surfaceId, button, pressed);
+ checkError();
+ }
+
+ public static void inputScroll(long surfaceId, float x, float y) {
+ processing_input_scroll(surfaceId, x, y);
+ checkError();
+ }
+
+ public static void inputKey(long surfaceId, int keyCode, boolean pressed) {
+ processing_input_key(surfaceId, keyCode, pressed);
+ checkError();
+ }
+
+ public static void inputChar(long surfaceId, int keyCode, int codepoint) {
+ processing_input_char(surfaceId, keyCode, codepoint);
+ checkError();
+ }
+
+ public static void inputCursorEnter(long surfaceId) {
+ processing_input_cursor_enter(surfaceId);
+ checkError();
+ }
+
+ public static void inputCursorLeave(long surfaceId) {
+ processing_input_cursor_leave(surfaceId);
+ checkError();
+ }
+
+ public static void inputFocus(long surfaceId, boolean focused) {
+ processing_input_focus(surfaceId, focused);
+ checkError();
+ }
+
+ public static void inputFlush() {
+ processing_input_flush();
+ checkError();
+ }
+
+ // ── Input: state queries ────────────────────────────────────────────
+
+ public static float mouseX(long surfaceId) {
+ return processing_mouse_x(surfaceId);
+ }
+
+ public static float mouseY(long surfaceId) {
+ return processing_mouse_y(surfaceId);
+ }
+
+ public static float pmouseX(long surfaceId) {
+ return processing_pmouse_x(surfaceId);
+ }
+
+ public static float pmouseY(long surfaceId) {
+ return processing_pmouse_y(surfaceId);
+ }
+
+ public static boolean mouseIsPressed() {
+ return processing_mouse_is_pressed();
+ }
+
+ public static byte mouseButton() {
+ return processing_mouse_button();
+ }
+
+ public static boolean keyIsPressed() {
+ return processing_key_is_pressed();
+ }
+
+ public static boolean keyIsDown(int keyCode) {
+ return processing_key_is_down(keyCode);
+ }
+
+ public static boolean keyJustPressed(int keyCode) {
+ return processing_key_just_pressed(keyCode);
+ }
+
+ public static int key() {
+ return processing_key();
+ }
+
+ public static int keyCode() {
+ return processing_key_code();
+ }
+
+ public static float movedX() {
+ return processing_moved_x();
+ }
+
+ public static float movedY() {
+ return processing_moved_y();
+ }
+
+ public static float mouseWheel() {
+ return processing_mouse_wheel();
+ }
+
+ // ── Geometry ────────────────────────────────────────────────────────
+
+ public static final byte TOPOLOGY_POINT_LIST = 0;
+ public static final byte TOPOLOGY_LINE_LIST = 1;
+ public static final byte TOPOLOGY_LINE_STRIP = 2;
+ public static final byte TOPOLOGY_TRIANGLE_LIST = 3;
+ public static final byte TOPOLOGY_TRIANGLE_STRIP = 4;
+
+ public static final byte ATTR_FORMAT_FLOAT = 1;
+ public static final byte ATTR_FORMAT_FLOAT2 = 2;
+ public static final byte ATTR_FORMAT_FLOAT3 = 3;
+ public static final byte ATTR_FORMAT_FLOAT4 = 4;
+
+ public static long geometryLayoutCreate() {
+ long layoutId = processing_geometry_layout_create();
+ checkError();
+ return layoutId;
+ }
+
+ public static void geometryLayoutAddPosition(long layoutId) {
+ processing_geometry_layout_add_position(layoutId);
+ checkError();
+ }
+
+ public static void geometryLayoutAddNormal(long layoutId) {
+ processing_geometry_layout_add_normal(layoutId);
+ checkError();
+ }
+
+ public static void geometryLayoutAddColor(long layoutId) {
+ processing_geometry_layout_add_color(layoutId);
+ checkError();
+ }
+
+ public static void geometryLayoutAddUv(long layoutId) {
+ processing_geometry_layout_add_uv(layoutId);
+ checkError();
+ }
+
+ public static void geometryLayoutAddAttribute(long layoutId, long attrId) {
+ processing_geometry_layout_add_attribute(layoutId, attrId);
+ checkError();
+ }
+
+ public static void geometryLayoutDestroy(long layoutId) {
+ processing_geometry_layout_destroy(layoutId);
+ checkError();
+ }
+
+ public static long geometryCreate(byte topology) {
+ long geoId = processing_geometry_create(topology);
+ checkError();
+ return geoId;
+ }
+
+ public static long geometryCreateWithLayout(long layoutId, byte topology) {
+ long geoId = processing_geometry_create_with_layout(layoutId, topology);
+ checkError();
+ return geoId;
+ }
+
+ public static long geometryBox(float width, float height, float depth) {
+ long geoId = processing_geometry_box(width, height, depth);
+ checkError();
+ return geoId;
+ }
+
+ public static long geometrySphere(float radius, int sectors, int stacks) {
+ long geoId = processing_geometry_sphere(radius, sectors, stacks);
+ checkError();
+ return geoId;
+ }
+
+ public static void geometryDestroy(long geoId) {
+ processing_geometry_destroy(geoId);
+ checkError();
+ }
+
+ public static void geometryNormal(long geoId, float nx, float ny, float nz) {
+ processing_geometry_normal(geoId, nx, ny, nz);
+ checkError();
+ }
+
+ public static void geometryColor(long geoId, float r, float g, float b, float a) {
+ processing_geometry_color(geoId, r, g, b, a);
+ checkError();
+ }
+
+ public static void geometryUv(long geoId, float u, float v) {
+ processing_geometry_uv(geoId, u, v);
+ checkError();
+ }
+
+ public static void geometryVertex(long geoId, float x, float y, float z) {
+ processing_geometry_vertex(geoId, x, y, z);
+ checkError();
+ }
+
+ public static void geometryIndex(long geoId, int i) {
+ processing_geometry_index(geoId, i);
+ checkError();
+ }
+
+ public static long geometryAttributeCreate(String name, byte format) {
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment nameSegment = arena.allocateFrom(name);
+ long attrId = processing_geometry_attribute_create(nameSegment, format);
+ checkError();
+ return attrId;
+ }
+ }
+
+ public static void geometryAttributeDestroy(long attrId) {
+ processing_geometry_attribute_destroy(attrId);
+ checkError();
+ }
+
+ public static long geometryAttributePosition() {
+ return processing_geometry_attribute_position();
+ }
+
+ public static long geometryAttributeNormal() {
+ return processing_geometry_attribute_normal();
+ }
+
+ public static long geometryAttributeColor() {
+ return processing_geometry_attribute_color();
+ }
+
+ public static long geometryAttributeUv() {
+ return processing_geometry_attribute_uv();
+ }
+
+ public static void geometryAttributeFloat(long geoId, long attrId, float v) {
+ processing_geometry_attribute_float(geoId, attrId, v);
+ checkError();
+ }
+
+ public static void geometryAttributeFloat2(long geoId, long attrId, float x, float y) {
+ processing_geometry_attribute_float2(geoId, attrId, x, y);
+ checkError();
+ }
+
+ public static void geometryAttributeFloat3(long geoId, long attrId, float x, float y, float z) {
+ processing_geometry_attribute_float3(geoId, attrId, x, y, z);
+ checkError();
+ }
+
+ public static void geometryAttributeFloat4(long geoId, long attrId, float x, float y, float z, float w) {
+ processing_geometry_attribute_float4(geoId, attrId, x, y, z, w);
+ checkError();
+ }
+
+ public static int geometryVertexCount(long geoId) {
+ int count = processing_geometry_vertex_count(geoId);
+ checkError();
+ return count;
+ }
+
+ public static int geometryIndexCount(long geoId) {
+ int count = processing_geometry_index_count(geoId);
+ checkError();
+ return count;
+ }
+
+ public static void geometrySetVertex(long geoId, int index, float x, float y, float z) {
+ processing_geometry_set_vertex(geoId, index, x, y, z);
+ checkError();
+ }
+
+ public static void geometrySetNormal(long geoId, int index, float nx, float ny, float nz) {
+ processing_geometry_set_normal(geoId, index, nx, ny, nz);
+ checkError();
+ }
+
+ public static void geometrySetColor(long geoId, int index, float r, float g, float b, float a) {
+ processing_geometry_set_color(geoId, index, r, g, b, a);
+ checkError();
+ }
+
+ public static void geometrySetUv(long geoId, int index, float u, float v) {
+ processing_geometry_set_uv(geoId, index, u, v);
+ checkError();
+ }
+
+ public static void model(long graphicsId, long geoId) {
+ processing_model(graphicsId, geoId);
+ checkError();
+ }
+
+ // ── Helpers ──────────────────────────────────────────────────────────
+
+ private static MemorySegment allocateColor(Arena arena, float r, float g, float b, float a) {
+ MemorySegment color = Color.allocate(arena);
+ Color.c1(color, r);
+ Color.c2(color, g);
+ Color.c3(color, b);
+ Color.a(color, a);
+ Color.space(color, COLOR_SPACE_SRGB);
+ return color;
+ }
+
+ private static void checkError() {
+ MemorySegment ret = processing_check_error();
+ if (ret.equals(NULL)) {
+ return;
+ }
+
+ String errorMsg = ret.getString(0);
+ if (errorMsg != null && !errorMsg.isEmpty()) {
+ throw new PWebGPUException(errorMsg);
+ }
+ }
+}
diff --git a/core/src/processing/webgpu/PWebGPUException.java b/core/src/processing/webgpu/PWebGPUException.java
new file mode 100644
index 0000000000..b3907e0653
--- /dev/null
+++ b/core/src/processing/webgpu/PWebGPUException.java
@@ -0,0 +1,13 @@
+package processing.webgpu;
+
+/**
+ * Unchecked exception thrown for WebGPU-related errors.
+ *
+ * WebGPU operations can fail for various reasons, such as unsupported hardware, but are not
+ * expected to be recoverable by the Processing application.
+ */
+public class PWebGPUException extends RuntimeException {
+ public PWebGPUException(String message) {
+ super(message);
+ }
+}
diff --git a/core/test/processing/webgpu/PWebGPUTest.java b/core/test/processing/webgpu/PWebGPUTest.java
new file mode 100644
index 0000000000..405e70e1c4
--- /dev/null
+++ b/core/test/processing/webgpu/PWebGPUTest.java
@@ -0,0 +1,13 @@
+package processing.webgpu;
+
+import org.junit.Test;
+
+/**
+ * Tests for the PWebGPU native interface.
+ */
+public class PWebGPUTest {
+ @Test
+ public void itLoads() {
+ PWebGPU.ensureLoaded();
+ }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000000..03a4a7dcc0
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,2 @@
+group=org.processing
+version=4.5.5
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 49d41db2ad..4455f92044 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
[versions]
-kotlin = "2.2.20"
+kotlin = "2.3.21"
compose-plugin = "1.9.1"
jogl = "2.6.0"
antlr = "4.13.2"
diff --git a/gradle/plugins/library/src/main/kotlin/ProcessingLibraryPlugin.kt b/gradle/plugins/library/src/main/kotlin/ProcessingLibraryPlugin.kt
index 4514f581fd..9cac968141 100644
--- a/gradle/plugins/library/src/main/kotlin/ProcessingLibraryPlugin.kt
+++ b/gradle/plugins/library/src/main/kotlin/ProcessingLibraryPlugin.kt
@@ -36,8 +36,9 @@ class ProcessingLibraryPlugin : Plugin {
target.dependencies.add("compileOnly", "org.processing:core:$processingVersion")
}
}
+ val javaVersionOverride = target.findProperty("enableWebGPU")?.toString()?.toBoolean()?.let { if (it) 25 else 17 } ?: 17
target.extensions.configure(JavaPluginExtension::class.java) { extension ->
- extension.toolchain.languageVersion.set(JavaLanguageVersion.of(17))
+ extension.toolchain.languageVersion.set(JavaLanguageVersion.of(javaVersionOverride))
}
target.plugins.withType(JavaPlugin::class.java) {
@@ -122,4 +123,4 @@ class ProcessingLibraryPlugin : Plugin {
}
}
-}
\ No newline at end of file
+}
diff --git a/gradle/plugins/settings.gradle.kts b/gradle/plugins/settings.gradle.kts
index ab39f6aca7..dc4f98668e 100644
--- a/gradle/plugins/settings.gradle.kts
+++ b/gradle/plugins/settings.gradle.kts
@@ -1,5 +1,5 @@
plugins {
- id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
include("library")
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index aaaabb3cb9..2e1113280e 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/java/gradle/example/settings.gradle.kts b/java/gradle/example/settings.gradle.kts
index ee9c97e155..297c7c2f8d 100644
--- a/java/gradle/example/settings.gradle.kts
+++ b/java/gradle/example/settings.gradle.kts
@@ -2,4 +2,5 @@ rootProject.name = "processing-gradle-plugin-demo"
pluginManagement {
includeBuild("../../../")
-}
\ No newline at end of file
+}
+includeBuild("../../../")
diff --git a/java/gradle/src/main/kotlin/DependenciesTask.kt b/java/gradle/src/main/kotlin/DependenciesTask.kt
deleted file mode 100644
index 8e2cb9bca3..0000000000
--- a/java/gradle/src/main/kotlin/DependenciesTask.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-package org.processing.java.gradle
-
-import org.gradle.api.DefaultTask
-import org.gradle.api.GradleException
-import org.gradle.api.file.RegularFileProperty
-import org.gradle.api.tasks.InputFile
-import org.gradle.api.tasks.TaskAction
-import java.io.File
-import java.io.ObjectInputStream
-
-/*
-* The DependenciesTask resolves the dependencies for the sketch based on the libraries used
- */
-abstract class DependenciesTask: DefaultTask() {
- @InputFile
- val librariesMetaData: RegularFileProperty = project.objects.fileProperty()
-
- @InputFile
- val sketchMetaData: RegularFileProperty = project.objects.fileProperty()
-
- init{
- librariesMetaData.convention(project.layout.buildDirectory.file("processing/libraries"))
- sketchMetaData.convention(project.layout.buildDirectory.file("processing/sketch"))
- }
-
- @TaskAction
- fun execute() {
- val sketchMetaFile = sketchMetaData.get().asFile
- val librariesMetaFile = librariesMetaData.get().asFile
-
- val libraries = librariesMetaFile.inputStream().use { input ->
- ObjectInputStream(input).readObject() as ArrayList
- }
-
- val sketch = sketchMetaFile.inputStream().use { input ->
- ObjectInputStream(input).readObject() as PDETask.SketchMeta
- }
-
- val dependencies = mutableSetOf()
-
- // Loop over the import statements in the sketch and import the relevant jars from the libraries
- sketch.importStatements.forEach import@{ statement ->
- libraries.forEach { library ->
- library.jars.forEach { jar ->
- jar.classes.forEach { className ->
- if (className.startsWith(statement)) {
- dependencies.addAll(library.jars.map { it.path } )
- return@import
- }
- }
- }
- }
- }
- project.dependencies.add("implementation", project.files(dependencies) )
-
- // TODO: Mutating the dependencies of configuration ':implementation' after it has been resolved or consumed. This
-
- // TODO: Add only if user is compiling for P2D or P3D
- // Add JOGL and Gluegen dependencies
- project.dependencies.add("runtimeOnly", "org.jogamp.jogl:jogl-all-main:2.5.0")
- project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt:2.5.0")
-
- val os = System.getProperty("os.name").lowercase()
- val arch = System.getProperty("os.arch").lowercase()
-
- val variant = when {
- os.contains("mac") -> "macosx-universal"
- os.contains("win") && arch.contains("64") -> "windows-amd64"
- os.contains("linux") && arch.contains("aarch64") -> "linux-aarch64"
- os.contains("linux") && arch.contains("arm") -> "linux-arm"
- os.contains("linux") && arch.contains("amd64") -> "linux-amd64"
- else -> throw GradleException("Unsupported OS/architecture: $os / $arch")
- }
-
- project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt:2.5.0:natives-$variant")
- project.dependencies.add("runtimeOnly", "org.jogamp.jogl:nativewindow:2.5.0:natives-$variant")
- project.dependencies.add("runtimeOnly", "org.jogamp.jogl:newt:2.5.0:natives-$variant")
- }
-}
\ No newline at end of file
diff --git a/java/gradle/src/main/kotlin/LibrariesTask.kt b/java/gradle/src/main/kotlin/LibrariesTask.kt
deleted file mode 100644
index 2ccca5cde7..0000000000
--- a/java/gradle/src/main/kotlin/LibrariesTask.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package org.processing.java.gradle
-
-import org.gradle.api.DefaultTask
-import org.gradle.api.file.ConfigurableFileCollection
-import org.gradle.api.file.RegularFileProperty
-import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.TaskAction
-import java.io.File
-import java.io.ObjectOutputStream
-import java.util.jar.JarFile
-
-/*
-The libraries task scans the sketchbook libraries folder for all the libraries
-This task stores the resulting information in a file that can be used later to resolve dependencies
- */
-abstract class LibrariesTask : DefaultTask() {
-
- @InputFiles
- val libraryDirectories: ConfigurableFileCollection = project.files()
-
- @OutputFile
- val librariesMetaData: RegularFileProperty = project.objects.fileProperty()
-
- init{
- librariesMetaData.convention { project.gradle.gradleUserHomeDir.resolve("common/processing/libraries") }
- }
-
- data class Jar(
- val path: File,
- val classes: List
- ) : java.io.Serializable
-
- data class Library(
- val jars: List
- ) : java.io.Serializable
-
- @TaskAction
- fun execute() {
- val output = libraryDirectories.flatMap { librariesDirectory ->
- if (!librariesDirectory.exists()) {
- logger.error("Libraries directory (${librariesDirectory.path}) does not exist. Libraries will not be imported.")
- return@flatMap emptyList()
- }
- val libraries = librariesDirectory
- .listFiles { file -> file.isDirectory }
- ?.map { folder ->
- // Find all the jars in the sketchbook
- val jars = folder.resolve("library")
- .listFiles{ file -> file.extension == "jar" }
- ?.map{ file ->
-
- // Inside each jar, look for the defined classes
- val jar = JarFile(file)
- val classes = jar.entries().asSequence()
- .filter { entry -> entry.name.endsWith(".class") }
- .map { entry -> entry.name }
- .map { it.substringBeforeLast('/').replace('/', '.') }
- .distinct()
- .toList()
-
- // Return a reference to the jar and its classes
- return@map Jar(
- path = file,
- classes = classes
- )
- }?: emptyList()
-
- // Save the parsed jars and which folder
- return@map Library(
- jars = jars
- )
- }?: emptyList()
-
- return@flatMap libraries
- }
- val meta = ObjectOutputStream(librariesMetaData.get().asFile.outputStream())
- meta.writeObject(output)
- meta.close()
- }
-}
\ No newline at end of file
diff --git a/java/gradle/src/main/kotlin/ProcessingPlugin.kt b/java/gradle/src/main/kotlin/ProcessingPlugin.kt
index b5c99367b5..24f6f4540c 100644
--- a/java/gradle/src/main/kotlin/ProcessingPlugin.kt
+++ b/java/gradle/src/main/kotlin/ProcessingPlugin.kt
@@ -9,8 +9,11 @@ import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.JavaExec
+import org.gradle.jvm.toolchain.JavaLanguageVersion
import org.jetbrains.compose.ComposeExtension
import org.jetbrains.compose.desktop.DesktopExtension
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.File
import java.net.Socket
import javax.inject.Inject
@@ -35,12 +38,24 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact
val settings = project.findProperty("processing.settings") as String?
val root = project.findProperty("processing.root") as String?
+ val webgpu = (project.findProperty("processing.webgpu") as String?)?.toBoolean() ?: false
+ val javaVersion = if (webgpu) 25 else 17
+
// Apply the Java plugin to the Project, equivalent of
// plugins {
// java
// }
project.plugins.apply(JavaPlugin::class.java)
+ project.extensions.configure(JavaPluginExtension::class.java) { ext ->
+ ext.toolchain { spec ->
+ spec.languageVersion.set(JavaLanguageVersion.of(javaVersion))
+ }
+ }
+ project.tasks.withType(KotlinCompile::class.java).configureEach { task ->
+ task.compilerOptions.jvmTarget.set(JvmTarget.fromTarget(javaVersion.toString()))
+ }
+
if(isProcessing){
// Set the build directory to a temp file so it doesn't clutter up the sketch folder
// Only if the build directory doesn't exist, otherwise proceed as normal
@@ -98,6 +113,7 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact
// Set the class to be executed initially
application.mainClass = sketchName
application.nativeDistributions.includeAllModules = true
+ application.jvmArgs("--enable-native-access=ALL-UNNAMED")
if(debugPort != null) {
application.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
}
@@ -133,6 +149,9 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact
}
}
+ val javaToolchains = project.extensions.getByType(org.gradle.jvm.toolchain.JavaToolchainService::class.java)
+ val launcher = javaToolchains.launcherFor { it.languageVersion.set(JavaLanguageVersion.of(javaVersion)) }
+
project.afterEvaluate {
// Copy the result of create distributable to the project directory
project.tasks.named("createDistributable") { task ->
@@ -143,6 +162,13 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact
}
}
}
+ project.tasks.withType(JavaExec::class.java).configureEach { task ->
+ task.executable(launcher.get().executablePath.asFile.absolutePath)
+ task.jvmArgs("--enable-native-access=ALL-UNNAMED")
+ if (System.getProperty("os.name").lowercase().contains("mac")) {
+ task.jvmArgs("-XstartOnFirstThread")
+ }
+ }
}
// Move the processing variables into javaexec tasks so they can be used in the sketch as well
@@ -151,6 +177,11 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact
.filterKeys { it.startsWith("processing") }
.forEach { (key, value) -> task.systemProperty(key, value) }
+ task.jvmArgs("--enable-native-access=ALL-UNNAMED")
+ if (System.getProperty("os.name").lowercase().contains("mac")) {
+ task.jvmArgs("-XstartOnFirstThread")
+ }
+
// Connect the stdio to the PDE if ports are specified
if(logPort != null) task.standardOutput = Socket("localhost", logPort.toInt()).outputStream
if(errPort != null) task.errorOutput = Socket("localhost", errPort.toInt()).outputStream
@@ -180,13 +211,6 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact
include("/*.java")
}
- // Scan the libraries before compiling the sketches
- val librariesTaskName = sourceSet.getTaskName("scanLibraries", "PDE")
- val librariesScan = project.tasks.register(librariesTaskName, LibrariesTask::class.java) { task ->
- task.description = "Scans the libraries in the sketchbook"
- task.libraryDirectories.from(sketchbook?.let { File(it, "libraries") }, root?.let { File(it).resolve("modes/java/libraries") })
- }
-
// Create a task to process the .pde files before compiling the java sources
val pdeTaskName = sourceSet.getTaskName("preprocess", "PDE")
val pdeTask = project.tasks.register(pdeTaskName, PDETask::class.java) { task ->
@@ -198,19 +222,68 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact
sourceSet.java.srcDir(task.outputDirectory)
}
- val depsTaskName = sourceSet.getTaskName("addLegacyDependencies", "PDE")
- project.tasks.register(depsTaskName, DependenciesTask::class.java){ task ->
- // Link the output of the libraries task to the dependencies task
- task.librariesMetaData.set(librariesScan.get().librariesMetaData)
- task.dependsOn(pdeTask, librariesScan)
- }
+ // Resolve sketch+library deps at config time. Adding deps from a
+ // TaskAction fails once a downstream task has already resolved
+ // runtimeClasspath (e.g. Compose's `run`).
+ addLegacyDependencies(project, pdeSourceSet.srcDirs,
+ listOfNotNull(sketchbook?.let { File(it, "libraries") },
+ root?.let { File(it).resolve("modes/java/libraries") }))
// Make sure that the PDE tasks runs before the java compilation task
project.tasks.named(sourceSet.compileJavaTaskName) { task ->
- task.dependsOn(pdeTaskName, depsTaskName)
+ task.dependsOn(pdeTaskName)
}
}
}
+ private fun addLegacyDependencies(project: Project, sketchDirs: Set, libraryRoots: List) {
+ project.dependencies.add("runtimeOnly", "org.jogamp.jogl:jogl-all-main:2.6.0")
+ project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt:2.6.0")
+
+ val os = System.getProperty("os.name").lowercase()
+ val arch = System.getProperty("os.arch").lowercase()
+ val variant = when {
+ os.contains("mac") -> "macosx-universal"
+ os.contains("win") && arch.contains("64") -> "windows-amd64"
+ os.contains("linux") && arch.contains("aarch64") -> "linux-aarch64"
+ os.contains("linux") && arch.contains("arm") -> "linux-arm"
+ os.contains("linux") && arch.contains("amd64") -> "linux-amd64"
+ else -> null
+ }
+ if (variant != null) {
+ project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt:2.6.0:natives-$variant")
+ project.dependencies.add("runtimeOnly", "org.jogamp.jogl:nativewindow:2.6.0:natives-$variant")
+ project.dependencies.add("runtimeOnly", "org.jogamp.jogl:newt:2.6.0:natives-$variant")
+ }
+
+ val imports = sketchDirs
+ .flatMap { dir -> dir.walkTopDown().filter { it.extension == "pde" }.toList() }
+ .flatMap { Regex("""^\s*import\s+([\w.]+)\s*;""", RegexOption.MULTILINE).findAll(it.readText()).map { m -> m.groupValues[1] } }
+ .toSet()
+ if (imports.isEmpty()) return
+
+ val libraryJars = libraryRoots
+ .filter { it.exists() }
+ .flatMap { it.listFiles { f -> f.isDirectory }?.toList() ?: emptyList() }
+ .mapNotNull { folder -> folder.resolve("library").takeIf { it.isDirectory } }
+ .flatMap { it.listFiles { f -> f.extension == "jar" }?.toList() ?: emptyList() }
+
+ val matched = mutableSetOf()
+ imports.forEach { import ->
+ libraryJars.forEach { jar ->
+ java.util.jar.JarFile(jar).use { jf ->
+ val hit = jf.entries().asSequence()
+ .filter { it.name.endsWith(".class") }
+ .map { it.name.substringBeforeLast('/').replace('/', '.') }
+ .any { it.startsWith(import) }
+ if (hit) matched.add(jar)
+ }
+ }
+ }
+ if (matched.isNotEmpty()) {
+ project.dependencies.add("implementation", project.files(matched))
+ }
+ }
+
abstract class DefaultPDESourceDirectorySet @Inject constructor(
sourceDirectorySet: SourceDirectorySet,
taskDependencyFactory: TaskDependencyFactory
diff --git a/java/src/processing/mode/java/JavaBuild.java b/java/src/processing/mode/java/JavaBuild.java
index b696ab0e20..af802c66ff 100644
--- a/java/src/processing/mode/java/JavaBuild.java
+++ b/java/src/processing/mode/java/JavaBuild.java
@@ -66,6 +66,7 @@ public class JavaBuild {
private boolean foundMain = false;
private String classPath;
protected String sketchClassName;
+ protected String sketchRenderer;
/**
* This will include the code folder, any library folders, etc. that might
@@ -118,6 +119,7 @@ public String build(File srcFolder, File binFolder, boolean sizeWarning) throws
// that will bubble up to whomever called build().
if (Compiler.compile(this)) {
sketchClassName = classNameFound;
+ sketchRenderer = result.getSketchRenderer();
return classNameFound;
}
return null;
@@ -128,6 +130,10 @@ public String getSketchClassName() {
return sketchClassName;
}
+ public String getSketchRenderer() {
+ return sketchRenderer;
+ }
+
/**
* Build all the code for this sketch.
diff --git a/java/src/processing/mode/java/PreprocService.java b/java/src/processing/mode/java/PreprocService.java
index 410cff02f6..a8a4a0c1c2 100644
--- a/java/src/processing/mode/java/PreprocService.java
+++ b/java/src/processing/mode/java/PreprocService.java
@@ -667,7 +667,7 @@ private void setupParser(boolean resolveBindings, String className,
if (resolveBindings) {
parser.setUnitName(className);
- parser.setEnvironment(classPathArray, null, null, false);
+ parser.setEnvironment(classPathArray, null, null, true);
parser.setResolveBindings(true);
}
}
diff --git a/java/src/processing/mode/java/runner/Runner.java b/java/src/processing/mode/java/runner/Runner.java
index a9baf1ea6a..d0bded04de 100644
--- a/java/src/processing/mode/java/runner/Runner.java
+++ b/java/src/processing/mode/java/runner/Runner.java
@@ -342,6 +342,10 @@ protected StringList getMachineParams() {
// No longer needed / doesn't seem to do anything differently
//params.append("-Dcom.apple.mrj.application.apple.menu.about.name=" +
// build.getSketchClassName());
+
+ if ("WEBGPU".equals(build.getSketchRenderer())) {
+ params.append("-XstartOnFirstThread");
+ }
}
/*
if (Platform.isWindows()) {
@@ -380,6 +384,10 @@ protected StringList getMachineParams() {
// http://processing.org/bugs/bugzilla/1188.html
params.append("-ea");
+ // we need to open up access to internal jdk modules for libraries that use reflection
+ // this will break at some point in the future when these modules are removed from the jdk :(
+ params.append("--enable-native-access=ALL-UNNAMED");
+
return params;
}
@@ -508,6 +516,9 @@ protected StringList getSketchParams(boolean present, String[] args) {
}
*/
+ // TODO: excise AWT to make webgpu work properly
+ params.append(PApplet.ARGS_DISABLE_AWT);
+
params.append(build.getSketchClassName());
}
// Add command-line arguments to be given to the sketch itself
diff --git a/libprocessing b/libprocessing
new file mode 160000
index 0000000000..a9f0774597
--- /dev/null
+++ b/libprocessing
@@ -0,0 +1 @@
+Subproject commit a9f0774597a81afe2cfccac07c99f81eada253e6