From 35057c509e710618215cba31ed055b1dddda2d04 Mon Sep 17 00:00:00 2001
From: stepan <stepan.sindelar@oracle.com>
Date: Mon, 18 Jun 2018 10:05:07 +0200
Subject: [PATCH] Implemented grid-device server.

---
 .../fastrGrid/server/RemoteDeviceServer.java  | 545 +++++++++++++++
 .../r/library/fastrGrid/GridContext.java      |  20 +-
 .../r/library/fastrGrid/WindowDevice.java     |  34 +-
 .../NotSupportedImageFormatException.java     |  34 +
 .../device/awt/BufferedImageDevice.java       |   9 +-
 .../fastrGrid/device/remote/RemoteDevice.java | 653 ++++++++++++++++++
 .../remote/RemoteDeviceDataExchange.java      | 268 +++++++
 .../grDevices/InitWindowedDevice.java         |   7 +-
 .../oracle/truffle/r/runtime/FastRConfig.java |  17 +-
 mx.fastr/mx_fastr.py                          |   6 +
 mx.fastr/native-image.properties              |   5 +-
 mx.fastr/suite.py                             |  28 +
 12 files changed, 1587 insertions(+), 39 deletions(-)
 create mode 100644 com.oracle.truffle.r.library.fastrGrid.server/src/com/oracle/truffle/r/library/fastrGrid/server/RemoteDeviceServer.java
 create mode 100644 com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/NotSupportedImageFormatException.java
 create mode 100644 com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDevice.java
 create mode 100644 com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDeviceDataExchange.java

diff --git a/com.oracle.truffle.r.library.fastrGrid.server/src/com/oracle/truffle/r/library/fastrGrid/server/RemoteDeviceServer.java b/com.oracle.truffle.r.library.fastrGrid.server/src/com/oracle/truffle/r/library/fastrGrid/server/RemoteDeviceServer.java
new file mode 100644
index 0000000000..dd8c181226
--- /dev/null
+++ b/com.oracle.truffle.r.library.fastrGrid.server/src/com/oracle/truffle/r/library/fastrGrid/server/RemoteDeviceServer.java
@@ -0,0 +1,545 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 3 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 3 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.truffle.r.library.fastrGrid.server;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.time.LocalTime;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext;
+import com.oracle.truffle.r.library.fastrGrid.device.GridColor;
+import com.oracle.truffle.r.library.fastrGrid.device.GridDevice;
+import com.oracle.truffle.r.library.fastrGrid.device.GridDevice.DeviceCloseException;
+import com.oracle.truffle.r.library.fastrGrid.device.GridDevice.ImageInterpolation;
+import com.oracle.truffle.r.library.fastrGrid.device.NotSupportedImageFormatException;
+import com.oracle.truffle.r.library.fastrGrid.device.remote.RemoteDevice;
+import com.oracle.truffle.r.library.fastrGrid.device.remote.RemoteDevice.DeviceType;
+import com.oracle.truffle.r.library.fastrGrid.device.remote.RemoteDeviceDataExchange;
+import com.oracle.truffle.r.library.fastrGrid.GridContext;
+import com.oracle.truffle.r.library.fastrGrid.WindowDevice;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+public class RemoteDeviceServer {
+
+    private static final Logger log = Logger.getLogger(RemoteDevice.class.getName());
+
+    private static final String STATUS_HANDLER = "/status";
+    private static final String QUIT_HANDLER = "/quit";
+
+    private static HttpServer server;
+
+    private static int lastDeviceId = 0;
+
+    private static Map<Integer, GridDevice> id2Device = new ConcurrentHashMap<>();
+
+    private static int lastDrawingContextId = 0;
+
+    private static Map<Integer, ServerDrawingContext> id2DrawingContext = new ConcurrentHashMap<>();
+
+    private static Map<DrawingContext, Integer> drawingContext2id = new ConcurrentHashMap<>();
+
+    private static long totalRequestsServiced;
+
+    private static long totalBytesRead;
+
+    private static long totalBytesWritten;
+
+    private static synchronized ServerDrawingContext getDrawingContext(Integer ctxId) {
+        return getDrawingContextImpl(ctxId);
+    }
+
+    private static ServerDrawingContext getDrawingContextImpl(Integer ctxId) {
+        ServerDrawingContext ctx = id2DrawingContext.get(ctxId);
+        assert (ctx != null) : "Unknown or GCed drawing context id=" + ctxId + ", lastDrawingContextId=" + lastDrawingContextId;
+        return ctx;
+    }
+
+    private static void releaseDrawingContextImpl(Integer ctxId) {
+        ServerDrawingContext ctx = getDrawingContextImpl(ctxId);
+        if (ctx.decRefCount()) {
+            id2DrawingContext.remove(ctxId);
+            drawingContext2id.remove(ctx);
+        }
+    }
+
+    public static void main(String[] args) throws IOException {
+        server = HttpServer.create(new InetSocketAddress(RemoteDevice.SERVER_PORT), 0);
+        server.createContext(RemoteDevice.COMMAND_HANDLER, new CommandHandler());
+        server.createContext(STATUS_HANDLER, new StatusAndQuitHandler(false));
+        server.createContext(QUIT_HANDLER, new StatusAndQuitHandler(true));
+        server.setExecutor(null); // creates a default executor
+        server.start();
+    }
+
+    private static class CommandHandler implements HttpHandler {
+
+        private RemoteDeviceDataExchange resultEncoder = new RemoteDeviceDataExchange();
+
+        @Override
+        public void handle(HttpExchange exchange) throws IOException {
+            try {
+                handleImpl(exchange);
+            } catch (Throwable t) {
+                t.printStackTrace();
+                System.exit(1);
+            }
+        }
+
+        private void handleImpl(HttpExchange exchange) throws IOException {
+            totalRequestsServiced++;
+            InputStream is = exchange.getRequestBody();
+            byte[] isBuf = new byte[is.available() + 1];
+            int off = 0;
+            int len;
+            try {
+                while ((len = is.read(isBuf, off, isBuf.length - off)) != -1) {
+                    off += len;
+                    if (off == isBuf.length) {
+                        byte[] newBuf = new byte[isBuf.length + Math.max(isBuf.length, is.available() + 1)];
+                        System.arraycopy(isBuf, 0, newBuf, 0, off);
+                        isBuf = newBuf;
+                    }
+                }
+            } finally {
+                is.close();
+            }
+            if (log.isLoggable(Level.FINER)) {
+                log.finer(RemoteDeviceDataExchange.bytesToString("Server Input: ", isBuf, off));
+            }
+
+            if (off == 0) {
+                throw new IOException("Empty request to grid server");
+            }
+            totalBytesRead += off;
+            RemoteDeviceDataExchange paramsDecoder = new RemoteDeviceDataExchange(isBuf, off);
+            byte commandId = paramsDecoder.readByte();
+            // Optimistically write ok status for all ops (revert if necessary)
+            resultEncoder.writeByte(RemoteDevice.STATUS_OK);
+            boolean checkServerClose = false;
+            RuntimeException rExc = null;
+            try {
+                if (commandId == RemoteDevice.CREATE_IMAGE) {
+                    DeviceType type = DeviceType.values()[paramsDecoder.readInt()];
+                    String filename = paramsDecoder.readString();
+                    String fileType = paramsDecoder.readString();
+                    int width = paramsDecoder.readInt();
+                    int height = paramsDecoder.readInt();
+                    int deviceId;
+                    synchronized (RemoteDeviceServer.class) {
+                        deviceId = ++lastDeviceId;
+                    }
+                    GridDevice device;
+                    switch (type) {
+                        case BUFFERED_IMAGE:
+                            try {
+                                device = GridContext.openLocalOrRemoteDevice(filename, fileType, width, height);
+                            } catch (NotSupportedImageFormatException ex) {
+                                deviceId = -1;
+                                device = null;
+                            }
+                            break;
+                        case WINDOW:
+                            device = WindowDevice.createWindowDevice(true, width, height);
+                            break;
+                        default:
+                            throw new AssertionError();
+                    }
+                    if (deviceId != -1) {
+                        id2Device.put(deviceId, device);
+                    }
+                    resultEncoder.writeInt(deviceId);
+                } else if (commandId == RemoteDevice.CREATE_DRAWING_CONTEXT) {
+                    ServerDrawingContext ctx = new ServerDrawingContext(paramsDecoder);
+                    Integer ctxId = drawingContext2id.get(ctx);
+                    if (ctxId == null) {
+                        synchronized (RemoteDeviceServer.class) {
+                            ctxId = ++lastDrawingContextId;
+                            id2DrawingContext.put(ctxId, ctx);
+                            drawingContext2id.put(ctx, ctxId);
+                        }
+                    } else {
+                        synchronized (RemoteDeviceServer.class) {
+                            ctx = getDrawingContextImpl(ctxId);
+                            ctx.incRefCount();
+                        }
+                    }
+                    resultEncoder.writeInt(ctxId);
+                } else if (commandId == RemoteDevice.RELEASE_DRAWING_CONTEXT) {
+                    int ctxId = paramsDecoder.readInt();
+                    synchronized (RemoteDeviceServer.class) {
+                        releaseDrawingContextImpl(ctxId);
+                    }
+                } else {
+                    Integer deviceId = paramsDecoder.readInt();
+                    GridDevice device = id2Device.get(deviceId);
+                    if (device == null) {
+                        throw new IllegalStateException("Grid device for id=" + deviceId + " does not exist on server.");
+                    }
+                    switch (commandId) {
+                        case RemoteDevice.OPEN_NEW_PAGE: {
+                            device.openNewPage();
+                            break;
+                        }
+                        case RemoteDevice.HOLD: {
+                            device.hold();
+                            break;
+                        }
+                        case RemoteDevice.FLUSH: {
+                            device.flush();
+                            break;
+                        }
+                        case RemoteDevice.CLOSE: {
+                            int[] releaseDrawingContextIds = paramsDecoder.readIntArray();
+                            synchronized (RemoteDeviceServer.class) {
+                                for (int i = 0; i < releaseDrawingContextIds.length; i++) {
+                                    int ctxId = releaseDrawingContextIds[i];
+                                    if (ctxId != 0) {
+                                        releaseDrawingContextImpl(ctxId);
+                                    }
+                                }
+                            }
+                            id2Device.remove(deviceId);
+                            checkServerClose = true;
+                            String exMsg = null;
+                            try {
+                                device.close();
+                            } catch (DeviceCloseException ex) {
+                                exMsg = ex.getMessage();
+                            }
+                            resultEncoder.writeString(exMsg);
+                            break;
+                        }
+                        case RemoteDevice.DRAW_RECT: {
+                            DrawingContext ctx = getDrawingContext(paramsDecoder.readInt());
+                            double leftX = paramsDecoder.readDouble();
+                            double bottomY = paramsDecoder.readDouble();
+                            double width = paramsDecoder.readDouble();
+                            double height = paramsDecoder.readDouble();
+                            double rotationAnticlockWise = paramsDecoder.readDouble();
+                            device.drawRect(ctx, leftX, bottomY, width, height, rotationAnticlockWise);
+                            break;
+                        }
+                        case RemoteDevice.DRAW_POLY_LINES: {
+                            DrawingContext ctx = getDrawingContext(paramsDecoder.readInt());
+                            double[] x = paramsDecoder.readDoubleArray();
+                            double[] y = paramsDecoder.readDoubleArray();
+                            int startIndex = paramsDecoder.readInt();
+                            int length = paramsDecoder.readInt();
+                            device.drawPolyLines(ctx, x, y, startIndex, length);
+                            break;
+                        }
+                        case RemoteDevice.DRAW_POLYGON: {
+                            DrawingContext ctx = getDrawingContext(paramsDecoder.readInt());
+                            double[] x = paramsDecoder.readDoubleArray();
+                            double[] y = paramsDecoder.readDoubleArray();
+                            int startIndex = paramsDecoder.readInt();
+                            int length = paramsDecoder.readInt();
+                            device.drawPolygon(ctx, x, y, startIndex, length);
+                            break;
+                        }
+                        case RemoteDevice.DRAW_CIRCLE: {
+                            DrawingContext ctx = getDrawingContext(paramsDecoder.readInt());
+                            double centerX = paramsDecoder.readDouble();
+                            double centerY = paramsDecoder.readDouble();
+                            double radius = paramsDecoder.readDouble();
+                            device.drawCircle(ctx, centerX, centerY, radius);
+                            break;
+                        }
+                        case RemoteDevice.DRAW_RASTER: {
+                            double leftX = paramsDecoder.readDouble();
+                            double bottomY = paramsDecoder.readDouble();
+                            double width = paramsDecoder.readDouble();
+                            double height = paramsDecoder.readDouble();
+                            int[] pixels = paramsDecoder.readIntArray();
+                            int pixelsColumnsCount = paramsDecoder.readInt();
+                            ImageInterpolation interpolation = ImageInterpolation.values()[paramsDecoder.readInt()];
+                            device.drawRaster(leftX, bottomY, width, height, pixels, pixelsColumnsCount, interpolation);
+                            break;
+                        }
+                        case RemoteDevice.DRAW_STRING: {
+                            DrawingContext ctx = getDrawingContext(paramsDecoder.readInt());
+                            double leftX = paramsDecoder.readDouble();
+                            double bottomY = paramsDecoder.readDouble();
+                            double rotationAnticlockWise = paramsDecoder.readDouble();
+                            String text = paramsDecoder.readString();
+                            device.drawString(ctx, leftX, bottomY, rotationAnticlockWise, text);
+                            break;
+                        }
+                        case RemoteDevice.GET_WIDTH: {
+                            resultEncoder.writeDouble(device.getWidth());
+                            break;
+                        }
+                        case RemoteDevice.GET_HEIGHT: {
+                            resultEncoder.writeDouble(device.getHeight());
+                            break;
+                        }
+                        case RemoteDevice.GET_NATIVE_WIDTH: {
+                            resultEncoder.writeDouble(device.getNativeWidth());
+                            break;
+                        }
+                        case RemoteDevice.GET_NATIVE_HEIGHT: {
+                            resultEncoder.writeDouble(device.getNativeHeight());
+                            break;
+                        }
+                        case RemoteDevice.GET_STRING_WIDTH: {
+                            DrawingContext ctx = getDrawingContext(paramsDecoder.readInt());
+                            String text = paramsDecoder.readString();
+                            resultEncoder.writeDouble(device.getStringWidth(ctx, text));
+                            break;
+                        }
+                        case RemoteDevice.GET_STRING_HEIGHT: {
+                            DrawingContext ctx = getDrawingContext(paramsDecoder.readInt());
+                            String text = paramsDecoder.readString();
+                            resultEncoder.writeDouble(device.getStringHeight(ctx, text));
+                            break;
+                        }
+                        default:
+                            throw new IllegalStateException("Invalid requestId=" + commandId);
+                    }
+                }
+            } catch (RuntimeException ex) {
+                rExc = ex;
+            }
+            byte[] osBuf = resultEncoder.resetWrite();
+            if (rExc != null) {
+                resultEncoder.writeByte(RemoteDevice.STATUS_SERVER_ERROR);
+                osBuf = resultEncoder.resetWrite();
+            }
+            if (log.isLoggable(Level.FINER)) {
+                log.finer(RemoteDeviceDataExchange.bytesToString("Server Output: ", osBuf, osBuf.length));
+            }
+            exchange.sendResponseHeaders(200, osBuf.length);
+            OutputStream os = exchange.getResponseBody();
+            try {
+                os.write(osBuf);
+            } finally {
+                os.close();
+            }
+            totalBytesWritten += osBuf.length;
+            exchange.close();
+            if (checkServerClose && rExc == null && id2Device.isEmpty()) {
+                log.fine("Server closing automatically after last device was closed.");
+                server.stop(0);
+                System.exit(0);
+            }
+            if (rExc != null) {
+                throw rExc;
+            }
+        }
+
+    }
+
+    private static final class ServerDrawingContext implements DrawingContext {
+
+        private final byte[] lineType;
+        private final double lineWidth;
+        private final GridLineJoin lineJoin;
+        private final GridLineEnd lineEnd;
+        private final double lineMitre;
+        private final GridColor color;
+        private final double fontSize;
+        private final GridFontStyle fontStyle;
+        private final String fontFamily;
+        private final double lineHeight;
+        private final GridColor fillColor;
+
+        private int hash;
+        private int refCount = 1;
+
+        ServerDrawingContext(RemoteDeviceDataExchange paramsDecoder) {
+            byte[] lineTypeRead = paramsDecoder.readByteArray();
+            if (lineTypeRead == null) {
+                lineType = DrawingContext.GRID_LINE_BLANK;
+            } else if (lineTypeRead.length == 0) {
+                lineType = DrawingContext.GRID_LINE_SOLID;
+            } else {
+                lineType = lineTypeRead;
+            }
+            lineWidth = paramsDecoder.readDouble();
+            lineJoin = GridLineJoin.values()[paramsDecoder.readInt()];
+            lineEnd = GridLineEnd.values()[paramsDecoder.readInt()];
+            lineMitre = paramsDecoder.readDouble();
+            color = GridColor.fromRawValue(paramsDecoder.readInt());
+            fontSize = paramsDecoder.readDouble();
+            fontStyle = GridFontStyle.values()[paramsDecoder.readInt()];
+            fontFamily = paramsDecoder.readString();
+            lineHeight = paramsDecoder.readDouble();
+            fillColor = GridColor.fromRawValue(paramsDecoder.readInt());
+        }
+
+        @Override
+        public byte[] getLineType() {
+            return lineType;
+        }
+
+        @Override
+        public double getLineWidth() {
+            return lineWidth;
+        }
+
+        @Override
+        public GridLineJoin getLineJoin() {
+            return lineJoin;
+        }
+
+        @Override
+        public GridLineEnd getLineEnd() {
+            return lineEnd;
+        }
+
+        @Override
+        public double getLineMitre() {
+            return lineMitre;
+        }
+
+        @Override
+        public GridColor getColor() {
+            return color;
+        }
+
+        @Override
+        public double getFontSize() {
+            return fontSize;
+        }
+
+        @Override
+        public GridFontStyle getFontStyle() {
+            return fontStyle;
+        }
+
+        @Override
+        public String getFontFamily() {
+            return fontFamily;
+        }
+
+        @Override
+        public double getLineHeight() {
+            return lineHeight;
+        }
+
+        @Override
+        public GridColor getFillColor() {
+            return fillColor;
+        }
+
+        @Override
+        public int hashCode() {
+            int h = hash;
+            if (h == 0) {
+                h = lineType.length;
+                for (int i = 0; i < lineType.length; i++) {
+                    h = (h << 8) ^ lineType[i];
+                }
+                h ^= Double.hashCode(lineWidth);
+                h ^= lineJoin.ordinal();
+                h ^= lineEnd.ordinal();
+                h ^= Double.hashCode(lineMitre);
+                h ^= color.getRawValue();
+                h ^= Double.hashCode(fontSize);
+                h ^= fontStyle.ordinal();
+                h ^= (fontFamily != null) ? fontFamily.hashCode() : 0;
+                h ^= Double.hashCode(lineHeight);
+                h ^= fillColor.getRawValue();
+                hash = h;
+            }
+            return h;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof ServerDrawingContext) {
+                ServerDrawingContext ctx = (ServerDrawingContext) obj;
+                byte[] ctxLT = ctx.lineType;
+                if (lineType != ctxLT) {
+                    if (lineType != null && ctxLT != null && lineType.length == ctxLT.length) {
+                        for (int i = ctxLT.length - 1; i >= 0; i--) {
+                            if (lineType[i] != ctxLT[i]) {
+                                return false;
+                            }
+                        }
+                    } else {
+                        return false;
+                    }
+                }
+                if (lineWidth != ctx.lineWidth || lineJoin != ctx.lineJoin ||
+                                lineEnd != ctx.lineEnd || lineMitre != ctx.lineMitre ||
+                                !color.equals(ctx.color) || fontSize != ctx.fontSize ||
+                                fontStyle != ctx.fontStyle || lineHeight != ctx.lineHeight ||
+                                !fillColor.equals(ctx.fillColor)) {
+                    return false;
+                }
+                if (fontFamily != ctx.fontFamily) {
+                    return (fontFamily != null && fontFamily.equals(ctx.fontFamily));
+                }
+                return true;
+            }
+            return false;
+        }
+
+        void incRefCount() {
+            refCount++;
+        }
+
+        boolean decRefCount() {
+            return (--refCount == 0);
+        }
+
+    }
+
+    private static class StatusAndQuitHandler implements HttpHandler {
+
+        private final boolean handleQuit;
+
+        StatusAndQuitHandler(boolean handleQuitArg) {
+            this.handleQuit = handleQuitArg;
+        }
+
+        @Override
+        public void handle(HttpExchange exchange) throws IOException {
+            OutputStream os = exchange.getResponseBody();
+            String response = (handleQuit ? LocalTime.now().toString() + ": Grid server stopped. Exit.\n" : "") +
+                            "Total devices created: " + lastDeviceId + ", active: " + id2Device.size() +
+                            "\nTotal DrawingContexts created: " + lastDrawingContextId + ", active: " + id2DrawingContext.size() +
+                            "\nTotal requests serviced: " + totalRequestsServiced +
+                            "\nTotal bytes read: " + totalBytesRead + ", written: " + totalBytesWritten;
+            byte[] responseBytes = response.getBytes();
+            exchange.sendResponseHeaders(200, responseBytes.length);
+            os.write(responseBytes);
+            os.close();
+            if (handleQuit) {
+                server.stop(0);
+                System.exit(0);
+            }
+        }
+
+    }
+
+}
diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java
index 7776efd084..60d4bd8c91 100644
--- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java
+++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java
@@ -33,7 +33,8 @@ import com.oracle.truffle.r.library.fastrGrid.device.GridDevice;
 import com.oracle.truffle.r.library.fastrGrid.device.GridDevice.DeviceCloseException;
 import com.oracle.truffle.r.library.fastrGrid.device.SVGDevice;
 import com.oracle.truffle.r.library.fastrGrid.device.awt.BufferedImageDevice;
-import com.oracle.truffle.r.library.fastrGrid.device.awt.BufferedImageDevice.NotSupportedImageFormatException;
+import com.oracle.truffle.r.library.fastrGrid.device.NotSupportedImageFormatException;
+import com.oracle.truffle.r.library.fastrGrid.device.remote.RemoteDevice;
 import com.oracle.truffle.r.library.fastrGrid.grDevices.FileDevUtils;
 import com.oracle.truffle.r.library.fastrGrid.graphics.RGridGraphicsAdapter;
 import com.oracle.truffle.r.runtime.FastRConfig;
@@ -106,6 +107,14 @@ public final class GridContext {
         return getContext(RContext.getInstance());
     }
 
+    public static GridDevice openLocalOrRemoteDevice(String filename, String fileType, int width, int height) throws NotSupportedImageFormatException {
+        if (FastRConfig.UseRemoteGridAwtDevice) {
+            return RemoteDevice.open(filename, fileType, width, height);
+        } else {
+            return BufferedImageDevice.open(filename, fileType, width, height);
+        }
+    }
+
     @TruffleBoundary
     public GridState getGridState() {
         gridState.setDeviceState(devices.get(currentDeviceIdx).state);
@@ -153,7 +162,7 @@ public final class GridContext {
             if (!FastRConfig.InternalGridAwtSupport) {
                 throw awtNotSupported();
             }
-            setCurrentDevice(defaultDev, WindowDevice.createWindowDevice());
+            setCurrentDevice(defaultDev, WindowDevice.createWindowDevice(false, GridDevice.DEFAULT_WIDTH, GridDevice.DEFAULT_HEIGHT));
         } else if (defaultDev.equals("svg")) {
             String filename = "Rplot%03d.svg";
             SVGDevice svgDevice = new SVGDevice(FileDevUtils.formatInitialFilename(filename), GridDevice.DEFAULT_WIDTH, GridDevice.DEFAULT_HEIGHT);
@@ -200,12 +209,9 @@ public final class GridContext {
     }
 
     private void safeOpenImageDev(String filename, String formatName) {
-        if (!FastRConfig.InternalGridAwtSupport) {
-            throw awtNotSupported();
-        }
-        BufferedImageDevice dev = null;
+        GridDevice dev = null;
         try {
-            dev = BufferedImageDevice.open(FileDevUtils.formatInitialFilename(filename), formatName, GridDevice.DEFAULT_WIDTH, GridDevice.DEFAULT_HEIGHT);
+            dev = openLocalOrRemoteDevice(FileDevUtils.formatInitialFilename(filename), formatName, GridDevice.DEFAULT_WIDTH, GridDevice.DEFAULT_HEIGHT);
         } catch (NotSupportedImageFormatException e) {
             throw RInternalError.shouldNotReachHere("Device format " + formatName + " should be supported.");
         }
diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/WindowDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/WindowDevice.java
index 63e453ec48..da39c52da9 100644
--- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/WindowDevice.java
+++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/WindowDevice.java
@@ -24,7 +24,9 @@ package com.oracle.truffle.r.library.fastrGrid;
 
 import com.oracle.truffle.r.library.fastrGrid.device.GridDevice;
 import com.oracle.truffle.r.library.fastrGrid.device.awt.JFrameDevice;
+import com.oracle.truffle.r.library.fastrGrid.device.remote.RemoteDevice;
 import com.oracle.truffle.r.nodes.function.PromiseHelperNode;
+import com.oracle.truffle.r.runtime.FastRConfig;
 import com.oracle.truffle.r.runtime.RCaller;
 import com.oracle.truffle.r.runtime.RError;
 import com.oracle.truffle.r.runtime.RError.Message;
@@ -41,20 +43,23 @@ public final class WindowDevice {
         // only static members
     }
 
-    public static GridDevice createWindowDevice() {
-        return createWindowDevice(GridDevice.DEFAULT_WIDTH, GridDevice.DEFAULT_HEIGHT);
-    }
-
-    public static GridDevice createWindowDevice(int width, int height) {
-        JFrameDevice frameDevice = new JFrameDevice(width, height);
-        RContext ctx = RContext.getInstance();
-        if (ctx.hasExecutor()) {
-            frameDevice.setResizeListener(() -> redrawAll(ctx));
-            frameDevice.setCloseListener(() -> devOff(ctx));
-        } else {
+    public static GridDevice createWindowDevice(boolean byGridServer, int width, int height) {
+        if (FastRConfig.UseRemoteGridAwtDevice) {
             noSchedulingSupportWarning();
+            return RemoteDevice.createWindowDevice(width, height);
+        } else {
+            JFrameDevice frameDevice = new JFrameDevice(width, height);
+            RContext ctx;
+            if (!byGridServer && ((ctx = RContext.getInstance()) != null) && ctx.hasExecutor() && !FastRConfig.UseRemoteGridAwtDevice) {
+                frameDevice.setResizeListener(() -> redrawAll(ctx));
+                frameDevice.setCloseListener(() -> devOff(ctx));
+            } else {
+                if (!byGridServer) {
+                    noSchedulingSupportWarning();
+                }
+            }
+            return frameDevice;
         }
-        return frameDevice;
     }
 
     public static RError awtNotSupported() {
@@ -103,7 +108,8 @@ public final class WindowDevice {
     }
 
     private static void noSchedulingSupportWarning() {
-        // Note: the PolyglotEngine was not built with an Executor
-        RError.warning(RError.NO_CALLER, Message.GENERIC, "Grid cannot resize the drawings. If you resize the window, the content will be lost.");
+        // Note: the PolyglotEngine was not built with an Executor or we use remote grid device
+        RError.warning(RError.NO_CALLER, Message.GENERIC, "Grid cannot resize the drawings. If you resize the window, the content will be lost. " +
+                        "You can redraw the contents using: 'popViewport(0, recording = FALSE); grid:::draw.all()'.");
     }
 }
diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/NotSupportedImageFormatException.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/NotSupportedImageFormatException.java
new file mode 100644
index 0000000000..2396f7ad0f
--- /dev/null
+++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/NotSupportedImageFormatException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 3 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 3 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.truffle.r.library.fastrGrid.device;
+
+public class NotSupportedImageFormatException extends Exception {
+
+    private static final long serialVersionUID = 1182697755931636217L;
+
+    @Override
+    public synchronized Throwable fillInStackTrace() {
+        return this;
+    }
+
+}
diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedImageDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedImageDevice.java
index 9d9f175900..4c1aafb4fb 100644
--- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedImageDevice.java
+++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedImageDevice.java
@@ -22,6 +22,7 @@
  */
 package com.oracle.truffle.r.library.fastrGrid.device.awt;
 
+import com.oracle.truffle.r.library.fastrGrid.device.NotSupportedImageFormatException;
 import static java.awt.image.BufferedImage.TYPE_INT_RGB;
 
 import java.awt.Color;
@@ -102,12 +103,4 @@ public final class BufferedImageDevice extends Graphics2DDevice implements FileG
         return false;
     }
 
-    public static class NotSupportedImageFormatException extends Exception {
-        private static final long serialVersionUID = 1182697755931636217L;
-
-        @Override
-        public synchronized Throwable fillInStackTrace() {
-            return this;
-        }
-    }
 }
diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDevice.java
new file mode 100644
index 0000000000..6749450102
--- /dev/null
+++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDevice.java
@@ -0,0 +1,653 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 3 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 3 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.truffle.r.library.fastrGrid.device.remote;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
+import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext;
+import com.oracle.truffle.r.library.fastrGrid.device.GridDevice;
+import com.oracle.truffle.r.library.fastrGrid.device.NotSupportedImageFormatException;
+import com.oracle.truffle.r.runtime.REnvVars;
+import com.oracle.truffle.r.runtime.RInternalError;
+
+public final class RemoteDevice implements GridDevice {
+
+    private static final Logger log = Logger.getLogger(RemoteDevice.class.getName());
+
+    /** Marker bit for commands required to return a result from server. */
+    static final byte RESULT_MASK = 64;
+
+    /**
+     * Create new image with filename, fileType, width and height params. Server returns integer
+     * imageId of the new image. Other requests encode command-type byte followed by imagedId int
+     * followed by command-specific parameters.
+     */
+    public static final byte CREATE_IMAGE = RESULT_MASK | 1;
+    public static final byte OPEN_NEW_PAGE = 2;
+    public static final byte HOLD = 3;
+    public static final byte FLUSH = 4;
+    public static final byte CLOSE = RESULT_MASK | 5; // Result is exception msg or ""
+    public static final byte DRAW_RECT = 6;
+    public static final byte DRAW_POLY_LINES = 7;
+    public static final byte DRAW_POLYGON = 8;
+    public static final byte DRAW_CIRCLE = 9;
+    public static final byte DRAW_RASTER = 10;
+    public static final byte DRAW_STRING = 11;
+    public static final byte GET_WIDTH = RESULT_MASK | 12;
+    public static final byte GET_HEIGHT = RESULT_MASK | 13;
+    public static final byte GET_NATIVE_WIDTH = RESULT_MASK | 14;
+    public static final byte GET_NATIVE_HEIGHT = RESULT_MASK | 15;
+    public static final byte GET_STRING_WIDTH = RESULT_MASK | 16;
+    public static final byte GET_STRING_HEIGHT = RESULT_MASK | 17;
+    public static final byte CREATE_DRAWING_CONTEXT = RESULT_MASK | 18;
+    public static final byte RELEASE_DRAWING_CONTEXT = 19;
+
+    /** Status is sent back from server as first byte of the response stream. */
+    public static final byte STATUS_OK = 0;
+    public static final byte STATUS_SERVER_ERROR = 1;
+
+    public static final int SERVER_PORT = 8011;
+
+    public static final String COMMAND_HANDLER = "/command";
+
+    private static final int SERVER_CONNECT_RETRIES = 3;
+    private static final int SERVER_CONNECT_TIMEOUT = 2000; // in ms
+    private static final int SERVER_CONNECT_RETRY_DELAY = 1000; // in ms
+    private static final int SERVER_AFTER_RUN_DELAY = 500; // in ms
+
+    private static final String SERVER_JAR_NAME = "grid-device-remote-server.jar";
+
+    private static LinkedBlockingDeque<RemoteRequest> queue = new LinkedBlockingDeque<>();
+
+    private static Thread queueWorker;
+
+    private static ReferenceQueue<DrawingContext> drawingContextRefQueue = new ReferenceQueue<>();
+
+    private static Process serverProcess;
+
+    private static Path javaCmd;
+
+    private final int remoteDeviceId;
+
+    private boolean closed;
+
+    private final RemoteDeviceDataExchange paramsEncoder = new RemoteDeviceDataExchange();
+
+    private final Map<DrawingContext, DrawingContextWeakRef> drawingContext2Ref = new WeakHashMap<>();
+
+    public static RemoteDevice open(String filename, String fileType, int width, int height) throws NotSupportedImageFormatException {
+        return new RemoteDevice(DeviceType.BUFFERED_IMAGE, filename, fileType, width, height);
+    }
+
+    public static RemoteDevice createWindowDevice(int width, int height) {
+        try {
+            return new RemoteDevice(DeviceType.WINDOW, null, null, width, height);
+        } catch (NotSupportedImageFormatException ex) { // Should never happen for this device type
+            throw new AssertionError();
+        }
+    }
+
+    private static Path javaCmd() {
+        if (javaCmd == null) {
+            String javaHome = System.getenv("JAVA_HOME");
+            if (javaHome == null) {
+                throw new RInternalError("JAVA_HOME is null");
+            }
+            javaCmd = Paths.get(javaHome, "bin", "java");
+            if (!Files.exists(javaCmd)) {
+                throw new RInternalError("Non-existent path '" + javaCmd + "'.");
+            }
+        }
+        return javaCmd;
+    }
+
+    private static void checkQueueInited() {
+        if (queueWorker == null) {
+            Runnable queueWorkerRun = new Runnable() {
+                @Override
+                public void run() {
+                    while (true) {
+                        RemoteRequest request;
+                        try {
+                            request = queue.take();
+                        } catch (InterruptedException ex) {
+                            break;
+                        }
+
+                        do {
+                            try {
+                                String url = "http://localhost:" + SERVER_PORT + COMMAND_HANDLER;
+                                HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+                                sendRequest(request, conn);
+                            } catch (IOException ex) {
+                                if (!checkServerConnectable()) {
+                                    destroyServer();
+                                    request.finish(new byte[]{STATUS_SERVER_ERROR}, ex);
+                                }
+                            }
+                        } while (!request.isFinished());
+                    }
+                }
+            };
+            queueWorker = new Thread(queueWorkerRun, "Grid-Remote-Device-Queue-Worker");
+            // queueWorker.setDaemon(true);
+            queueWorker.setPriority(Thread.NORM_PRIORITY + 2);
+            queueWorker.start();
+
+            Runnable dcRefGC = new Runnable() {
+                RemoteDeviceDataExchange paramsEncoder = new RemoteDeviceDataExchange();
+
+                @Override
+                public void run() {
+                    while (true) {
+                        try {
+                            DrawingContextWeakRef ref = (DrawingContextWeakRef) drawingContextRefQueue.remove();
+                            assert (paramsEncoder.isEmpty());
+                            paramsEncoder.writeByte(RELEASE_DRAWING_CONTEXT);
+                            paramsEncoder.writeInt(ref.getContextId());
+                            addRequestImpl(paramsEncoder.resetWrite());
+                            if (log.isLoggable(Level.FINE)) {
+                                log.fine("Drawing context with contextRefIHC=" + System.identityHashCode(ref) + " and id=" + ref.getContextId() + " released.");
+                            }
+                        } catch (InterruptedException ex) {
+                        }
+                    }
+                }
+            };
+            Thread dcRefQueueWorker = new Thread(dcRefGC, "Grid-Drawing-Context-GC");
+            dcRefQueueWorker.setDaemon(true);
+            dcRefQueueWorker.start();
+        }
+    }
+
+    private static boolean checkServerConnectable() {
+        for (int i = SERVER_CONNECT_RETRIES - 1; i >= 0; i--) {
+            if (serverProcess != null && !serverProcess.isAlive()) {
+                serverProcess.destroy();
+                serverProcess = null;
+            }
+            if (serverProcess == null) {
+                String rHome = REnvVars.rHome();
+                Path serverJar = Paths.get(rHome, SERVER_JAR_NAME);
+                if (!Files.exists(serverJar)) {
+                    Path buildServerJar = Paths.get(rHome, "mxbuild", "dists", SERVER_JAR_NAME);
+                    if (!Files.exists(buildServerJar)) {
+                        RInternalError.shouldNotReachHere(
+                                        "Remote grid server jar " + serverJar + " nor " + buildServerJar + " not found.");
+                    }
+                    serverJar = buildServerJar;
+                }
+                ProcessBuilder pb = new ProcessBuilder(
+                                javaCmd().toAbsolutePath().toString(),
+                                "-Dsun.net.httpserver.nodelay=true",
+                                "-jar",
+                                serverJar.toAbsolutePath().toString());
+                pb.inheritIO();
+                try {
+                    serverProcess = pb.start();
+                } catch (IOException ex) {
+                    throw new RInternalError(ex, "Cannot start remote grid server process.");
+                }
+                try {
+                    // Wait for an estimate of how long it takes to the server process
+                    // to start listening on server port.
+                    Thread.sleep(SERVER_AFTER_RUN_DELAY);
+                } catch (InterruptedException ex) {
+                }
+            }
+
+            // Check that server is connectable
+            Socket socket = new Socket();
+            try {
+                socket.connect(new InetSocketAddress(SERVER_PORT), SERVER_CONNECT_TIMEOUT);
+                return true;
+            } catch (SocketTimeoutException ex) {
+            } catch (IOException ex) {
+            } finally {
+                try {
+                    socket.close();
+                } catch (IOException ex) {
+                }
+            }
+
+            if (serverProcess != null) {
+                // In case the server start delay timeout was
+                // insufficient this (repetitive) timeout should
+                // ensure that the client will finally connect.
+                try {
+                    Thread.sleep(SERVER_CONNECT_RETRY_DELAY);
+                } catch (InterruptedException ex) {
+                }
+            }
+        }
+        return false;
+    }
+
+    private static void destroyServer() {
+        if (serverProcess != null) {
+            serverProcess.destroy();
+            serverProcess = null;
+        }
+    }
+
+    private static RInternalError serverError() {
+        return new RInternalError("Grid Server communication error.");
+    }
+
+    private RemoteDevice(DeviceType type, String filename, String fileType, int width, int height) throws NotSupportedImageFormatException {
+        checkQueueInited();
+        paramsEncoder.writeByte(CREATE_IMAGE);
+        paramsEncoder.writeInt(type.ordinal());
+        paramsEncoder.writeString(filename);
+        paramsEncoder.writeString(fileType);
+        paramsEncoder.writeInt(width);
+        paramsEncoder.writeInt(height);
+        RemoteDeviceDataExchange resultDecoder = addResultRequest(true);
+        remoteDeviceId = resultDecoder.readInt();
+        if (remoteDeviceId == -1) {
+            throw new NotSupportedImageFormatException();
+        }
+        if (log.isLoggable(Level.FINE)) {
+            log.fine("New remote device id=" + remoteDeviceId + " created.");
+        }
+    }
+
+    private void encodeOp(byte opId) {
+        if (closed) {
+            throw new RInternalError("Operation opId=" + opId + " with closed remote grid device (id=" + remoteDeviceId + ") prohibited.");
+        }
+        assert (paramsEncoder.isEmpty());
+        paramsEncoder.writeByte(opId);
+        assert (remoteDeviceId != 0) : "Remote device not obtained yet.";
+        paramsEncoder.writeInt(remoteDeviceId);
+    }
+
+    private boolean encodeOpAndDrawingContext(byte opId, DrawingContext ctx) {
+        DrawingContextWeakRef ctxRef;
+        synchronized (drawingContext2Ref) {
+            ctxRef = drawingContext2Ref.get(ctx);
+        }
+        if (ctxRef == null) { // Precede op with a request for drawing context creation
+            assert (paramsEncoder.isEmpty());
+            paramsEncoder.writeByte(CREATE_DRAWING_CONTEXT);
+            paramsEncoder.writeByteArray(ctx.getLineType());
+            paramsEncoder.writeDouble(ctx.getLineWidth());
+            paramsEncoder.writeInt(ctx.getLineJoin().ordinal());
+            paramsEncoder.writeInt(ctx.getLineEnd().ordinal());
+            paramsEncoder.writeDouble(ctx.getLineMitre());
+            paramsEncoder.writeInt(ctx.getColor().getRawValue());
+            paramsEncoder.writeDouble(ctx.getFontSize());
+            paramsEncoder.writeInt(ctx.getFontStyle().ordinal());
+            paramsEncoder.writeString(ctx.getFontFamily());
+            paramsEncoder.writeDouble(ctx.getLineHeight());
+            paramsEncoder.writeInt(ctx.getFillColor().getRawValue());
+            RemoteDeviceDataExchange resultDecoder = addResultRequest(false);
+            if (resultDecoder.readByte() == STATUS_OK) {
+                ctxRef = new DrawingContextWeakRef(ctx, resultDecoder.readInt());
+                if (log.isLoggable(Level.FINE)) {
+                    log.fine("Drawing context id=" + ctxRef.getContextId() + " for contextRefIHC=" + System.identityHashCode(ctxRef));
+                }
+                assert resultDecoder.isReadFinished();
+                synchronized (drawingContext2Ref) {
+                    drawingContext2Ref.put(ctx, ctxRef);
+                }
+            } else {
+                return false;
+            }
+        }
+        encodeOp(opId);
+        paramsEncoder.writeInt(ctxRef.getContextId());
+        return true;
+    }
+
+    @TruffleBoundary
+    RemoteDeviceDataExchange addResultRequest(boolean decodeReturnStatus) {
+        RemoteRequest request = addRequestImpl(paramsEncoder.resetWrite());
+        assert (request.params[0] & RESULT_MASK) == RESULT_MASK : "Unexpected no-result command-id " + request.params[0];
+        while (true) {
+            synchronized (request) {
+                if (request.isFinished()) {
+                    RemoteDeviceDataExchange resultDecoder = request.resultDecoder();
+                    if (decodeReturnStatus) {
+                        if (resultDecoder.readByte() != STATUS_OK) {
+                            throw serverError();
+                        }
+                    }
+                    return resultDecoder;
+                }
+                try {
+                    request.wait();
+                    if (request.errorCause != null) {
+                        throw new RInternalError(request.errorCause, "Grid Server communication error");
+                    }
+                } catch (InterruptedException ex) {
+                    throw new RInternalError("Waiting for result interrupted");
+                }
+            }
+        }
+    }
+
+    @TruffleBoundary
+    void addNoResultRequest() {
+        RemoteRequest request = addRequestImpl(paramsEncoder.resetWrite());
+        assert (request.params[0] & RESULT_MASK) == 0 : "Unexpected result command-id " + request.params[0];
+    }
+
+    private static RemoteRequest addRequestImpl(byte[] params) {
+        RemoteRequest request = new RemoteRequest(params);
+        queue.add(request);
+        return request;
+    }
+
+    private static void sendRequest(RemoteRequest request, HttpURLConnection conn) throws IOException {
+        conn.setRequestMethod("GET");
+        conn.setRequestProperty("User-Agent", "R-Grid-Remote-Client");
+        conn.setDoOutput(true);
+        OutputStream os = conn.getOutputStream();
+        byte[] osBuf = request.params;
+        if (log.isLoggable(Level.FINER)) {
+            log.finer(RemoteDeviceDataExchange.bytesToString("Data to server:", osBuf, osBuf.length));
+        }
+        try {
+            os.write(osBuf);
+        } finally {
+            os.close();
+        }
+        int responseCode = conn.getResponseCode();
+        if (responseCode == 200) { // Status OK
+            InputStream is = conn.getInputStream();
+            byte[] isBuf = new byte[is.available() + 1];
+            int off = 0;
+            int len;
+            try {
+                while ((len = is.read(isBuf, off, isBuf.length - off)) != -1) {
+                    off += len;
+                    if (off == isBuf.length) {
+                        byte[] newBuf = new byte[isBuf.length + Math.max(isBuf.length, is.available() + 1)];
+                        System.arraycopy(isBuf, 0, newBuf, 0, off);
+                        isBuf = newBuf;
+                    }
+                }
+            } finally {
+                is.close();
+            }
+            if (log.isLoggable(Level.FINER)) {
+                log.finer(RemoteDeviceDataExchange.bytesToString("Response from server:", isBuf, off));
+            }
+            byte[] result;
+            if (off != isBuf.length) {
+                result = new byte[off];
+                System.arraycopy(isBuf, 0, result, 0, off);
+            } else {
+                result = isBuf;
+            }
+            request.finish(result, null);
+        }
+    }
+
+    @Override
+    public void openNewPage() {
+        encodeOp(OPEN_NEW_PAGE);
+        addNoResultRequest();
+    }
+
+    @Override
+    public void close() throws DeviceCloseException {
+        encodeOp(CLOSE);
+        int[] releaseDrawingContextIds;
+        synchronized (RemoteDevice.class) {
+            releaseDrawingContextIds = new int[drawingContext2Ref.size()];
+            int i = 0;
+            for (DrawingContextWeakRef ref : drawingContext2Ref.values()) {
+                int ctxId = ref.invalidateContextId();
+                assert (ctxId != -1);
+                releaseDrawingContextIds[i++] = ctxId;
+            }
+            drawingContext2Ref.clear();
+        }
+        paramsEncoder.writeIntArray(releaseDrawingContextIds); // Possible zero ids skipped on
+                                                               // server
+        RemoteDeviceDataExchange resultDecoder = addResultRequest(true);
+        String excMsg = resultDecoder.readString();
+        closed = true;
+        if (excMsg != null) {
+            throw new DeviceCloseException(new IOException(excMsg)); // wrap into IOException for
+                                                                     // now
+        }
+        if (log.isLoggable(Level.FINE)) {
+            log.fine("Remote device id=" + remoteDeviceId + " closed successfully.");
+        }
+    }
+
+    @Override
+    public void drawRect(DrawingContext ctx, double leftX, double bottomY, double width, double height, double rotationAnticlockWise) {
+        if (encodeOpAndDrawingContext(DRAW_RECT, ctx)) {
+            paramsEncoder.writeDouble(leftX);
+            paramsEncoder.writeDouble(bottomY);
+            paramsEncoder.writeDouble(width);
+            paramsEncoder.writeDouble(height);
+            paramsEncoder.writeDouble(rotationAnticlockWise);
+            addNoResultRequest();
+        } else {
+            throw serverError();
+        }
+    }
+
+    @Override
+    public void drawPolyLines(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) {
+        if (encodeOpAndDrawingContext(DRAW_POLY_LINES, ctx)) {
+            paramsEncoder.writeDoubleArray(x);
+            paramsEncoder.writeDoubleArray(y);
+            paramsEncoder.writeInt(startIndex);
+            paramsEncoder.writeInt(length);
+            addNoResultRequest();
+        } else {
+            throw serverError();
+        }
+    }
+
+    @Override
+    public void drawPolygon(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) {
+        if (encodeOpAndDrawingContext(DRAW_POLYGON, ctx)) {
+            paramsEncoder.writeDoubleArray(x);
+            paramsEncoder.writeDoubleArray(y);
+            paramsEncoder.writeInt(startIndex);
+            paramsEncoder.writeInt(length);
+            addNoResultRequest();
+        } else {
+            throw serverError();
+        }
+    }
+
+    @Override
+    public void drawCircle(DrawingContext ctx, double centerX, double centerY, double radius) {
+        if (encodeOpAndDrawingContext(DRAW_CIRCLE, ctx)) {
+            paramsEncoder.writeDouble(centerX);
+            paramsEncoder.writeDouble(centerY);
+            paramsEncoder.writeDouble(radius);
+            addNoResultRequest();
+        } else {
+            throw serverError();
+        }
+    }
+
+    @Override
+    public void drawRaster(double leftX, double bottomY, double width, double height, int[] pixels, int pixelsColumnsCount, ImageInterpolation interpolation) {
+        encodeOp(DRAW_RASTER);
+        paramsEncoder.writeDouble(leftX);
+        paramsEncoder.writeDouble(bottomY);
+        paramsEncoder.writeDouble(width);
+        paramsEncoder.writeDouble(height);
+        paramsEncoder.writeIntArray(pixels);
+        paramsEncoder.writeInt(pixelsColumnsCount);
+        paramsEncoder.writeInt(interpolation.ordinal());
+        addNoResultRequest();
+    }
+
+    @Override
+    public void drawString(DrawingContext ctx, double leftX, double bottomY, double rotationAnticlockWise, String text) {
+        if (encodeOpAndDrawingContext(DRAW_STRING, ctx)) {
+            paramsEncoder.writeDouble(leftX);
+            paramsEncoder.writeDouble(bottomY);
+            paramsEncoder.writeDouble(rotationAnticlockWise);
+            paramsEncoder.writeString(text);
+            addNoResultRequest();
+        } else {
+            throw serverError();
+        }
+    }
+
+    @Override
+    public double getWidth() {
+        encodeOp(GET_WIDTH);
+        RemoteDeviceDataExchange resultDecoder = addResultRequest(true);
+        return resultDecoder.readDouble();
+    }
+
+    @Override
+    public double getHeight() {
+        encodeOp(GET_HEIGHT);
+        RemoteDeviceDataExchange resultDecoder = addResultRequest(true);
+        return resultDecoder.readDouble();
+    }
+
+    @Override
+    public int getNativeWidth() {
+        encodeOp(GET_NATIVE_WIDTH);
+        RemoteDeviceDataExchange resultDecoder = addResultRequest(true);
+        return resultDecoder.readInt();
+    }
+
+    @Override
+    public int getNativeHeight() {
+        encodeOp(GET_NATIVE_HEIGHT);
+        RemoteDeviceDataExchange resultDecoder = addResultRequest(true);
+        return resultDecoder.readInt();
+    }
+
+    @Override
+    public double getStringWidth(DrawingContext ctx, String text) {
+        if (encodeOpAndDrawingContext(GET_STRING_WIDTH, ctx)) {
+            paramsEncoder.writeString(text);
+            RemoteDeviceDataExchange resultDecoder = addResultRequest(true);
+            return resultDecoder.readDouble();
+        } else {
+            throw serverError();
+        }
+    }
+
+    @Override
+    public double getStringHeight(DrawingContext ctx, String text) {
+        if (encodeOpAndDrawingContext(GET_STRING_HEIGHT, ctx)) {
+            paramsEncoder.writeString(text);
+            RemoteDeviceDataExchange resultDecoder = addResultRequest(true);
+            return resultDecoder.readDouble();
+        } else {
+            throw serverError();
+        }
+    }
+
+    static final class RemoteRequest {
+
+        static final byte[] EMPTY_RESULT = new byte[0];
+
+        final byte[] params;
+
+        byte[] result;
+
+        Exception errorCause;
+
+        RemoteRequest(byte[] params) {
+            this.params = params;
+        }
+
+        RemoteDeviceDataExchange resultDecoder() {
+            return (result != null) ? new RemoteDeviceDataExchange(result, result.length) : null;
+        }
+
+        void finish(byte[] resultArg, Exception errorCauseArg) {
+            assert (this.result == null) : "Result already assigned";
+            byte[] resultArg2 = resultArg;
+            if (resultArg2 != null) {
+                assert (params[0] | RESULT_MASK) != 0 : "Attempt to assign result to non-result request";
+            } else {
+                resultArg2 = EMPTY_RESULT;
+            }
+            synchronized (this) {
+                this.result = resultArg2;
+                this.errorCause = errorCauseArg;
+                notifyAll();
+            }
+        }
+
+        boolean isFinished() {
+            return (result != null);
+        }
+
+    }
+
+    public enum DeviceType {
+        WINDOW,
+        BUFFERED_IMAGE;
+    }
+
+    private static final class DrawingContextWeakRef extends WeakReference<DrawingContext> {
+
+        private int contextId;
+
+        DrawingContextWeakRef(DrawingContext drawingContext, int contextIdArg) {
+            super(drawingContext, drawingContextRefQueue);
+            this.contextId = contextIdArg;
+        }
+
+        int getContextId() {
+            return contextId;
+        }
+
+        int invalidateContextId() {
+            int id = contextId;
+            contextId = -1;
+            return id;
+        }
+
+    }
+
+}
diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDeviceDataExchange.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDeviceDataExchange.java
new file mode 100644
index 0000000000..5a6674094a
--- /dev/null
+++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDeviceDataExchange.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 3 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 3 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.truffle.r.library.fastrGrid.device.remote;
+
+import com.oracle.truffle.r.runtime.RInternalError;
+import java.nio.charset.StandardCharsets;
+
+public class RemoteDeviceDataExchange {
+
+    public static String bytesToString(String title, byte[] buf, int limit) {
+        StringBuilder sb = new StringBuilder(title.length() + (limit << 2));
+        sb.append(title).append("0x");
+        for (int i = 0; i < limit; i++) {
+            int b = buf[i] & 0xFF;
+            sb.append("0123456789ABCDEF".charAt(b >>> 4));
+            sb.append("0123456789ABCDEF".charAt(b & 0x0F));
+        }
+        return sb.toString();
+    }
+
+    private static final int DEFAULT_BUF_SIZE = 64;
+
+    private byte[] buf;
+
+    private int index;
+
+    private int limit;
+
+    public RemoteDeviceDataExchange() {
+        this.buf = new byte[DEFAULT_BUF_SIZE];
+    }
+
+    public RemoteDeviceDataExchange(byte[] readBuf, int limit) {
+        this.buf = readBuf;
+        this.limit = limit;
+    }
+
+    public void writeInt(int value) {
+        ensureCapacity(4);
+        buf[index++] = (byte) (value >>> 24);
+        buf[index++] = (byte) (value >> 16);
+        buf[index++] = (byte) (value >> 8);
+        buf[index++] = (byte) value;
+    }
+
+    public int readInt() {
+        ensureData(4);
+        return ((buf[index++] & 0xff) << 24 |
+                        (buf[index++] & 0xff) << 16 |
+                        (buf[index++] & 0xff) << 8 |
+                        (buf[index++] & 0xff));
+    }
+
+    public void writeIntArray(int[] value) {
+        if (value != null) {
+            int len = value.length;
+            ensureCapacity(4 + (len << 2));
+            writeInt(len);
+            for (int i = 0; i < len; i++) {
+                writeInt(value[i]);
+            }
+        } else {
+            writeInt(-1);
+        }
+    }
+
+    public int[] readIntArray() {
+        int len = readInt();
+        if (len == -1) {
+            return null;
+        }
+        ensureData(len << 2);
+        int[] arr = new int[len];
+        for (int i = 0; i < len; i++) {
+            arr[i] = readInt();
+        }
+        return arr;
+    }
+
+    public void writeByte(byte value) {
+        ensureCapacity(1);
+        buf[index++] = value;
+    }
+
+    public byte readByte() {
+        ensureData(1);
+        return buf[index++];
+    }
+
+    public void writeByteArray(byte[] value) {
+        if (value != null) {
+            int len = value.length;
+            ensureCapacity(4 + len);
+            writeInt(len);
+            System.arraycopy(value, 0, buf, index, len);
+            index += len;
+        } else {
+            writeInt(-1);
+        }
+    }
+
+    public byte[] readByteArray() {
+        int len = readInt();
+        if (len == -1) {
+            return null;
+        }
+        ensureData(len);
+        byte[] arr = new byte[len];
+        System.arraycopy(buf, index, arr, 0, len);
+        index += len;
+        return arr;
+    }
+
+    public void writeDouble(double value) {
+        ensureCapacity(8);
+        long valueBits = Double.doubleToRawLongBits(value);
+        buf[index++] = (byte) (valueBits >>> 56);
+        buf[index++] = (byte) ((valueBits >> 48) & 0xff);
+        buf[index++] = (byte) ((valueBits >> 40) & 0xff);
+        buf[index++] = (byte) ((valueBits >> 32) & 0xff);
+        buf[index++] = (byte) ((valueBits >> 24) & 0xff);
+        buf[index++] = (byte) ((valueBits >> 16) & 0xff);
+        buf[index++] = (byte) ((valueBits >> 8) & 0xff);
+        buf[index++] = (byte) (valueBits & 0xff);
+    }
+
+    public double readDouble() {
+        long bits = ((long) (buf[index++] & 0xff) << 56 | (long) (buf[index++] & 0xff) << 48 | (long) (buf[index++] & 0xff) << 40 | (long) (buf[index++] & 0xff) << 32 |
+                        (long) (buf[index++] & 0xff) << 24 | (long) (buf[index++] & 0xff) << 16 | (long) (buf[index++] & 0xff) << 8 | buf[index++] & 0xff);
+        return Double.longBitsToDouble(bits);
+    }
+
+    public void writeDoubleArray(double[] value) {
+        if (value != null) {
+            int len = value.length;
+            ensureCapacity(4 + (len << 3));
+            writeInt(len);
+            for (int i = 0; i < len; i++) {
+                writeDouble(value[i]);
+            }
+        } else {
+            writeInt(-1);
+        }
+    }
+
+    public double[] readDoubleArray() {
+        int len = readInt();
+        if (len == -1) {
+            return null;
+        }
+        ensureData(len << 3);
+        double[] arr = new double[len];
+        for (int i = 0; i < len; i++) {
+            arr[i] = readDouble();
+        }
+        return arr;
+    }
+
+    public void writeString(String value) {
+        if (value != null) {
+            boolean simple = true;
+            for (int i = 0; i < value.length(); i++) {
+                if (value.charAt(i) >= 0x80) {
+                    simple = false;
+                    break;
+                }
+            }
+            if (simple && value.length() <= buf.length) {
+                writeInt(value.length());
+                ensureCapacity(value.length());
+                for (int i = 0; i < value.length(); i++) {
+                    buf[index++] = (byte) value.charAt(i);
+                }
+            } else {
+                byte[] bytes = value.getBytes();
+                int bytesLen = bytes.length;
+                ensureCapacity(bytesLen + 4);
+                writeInt(bytesLen);
+                System.arraycopy(bytes, 0, buf, index, bytesLen);
+                index += bytesLen;
+            }
+        } else { // value == null
+            writeInt(-1);
+        }
+    }
+
+    public String readString() {
+        int strLen = readInt();
+        if (strLen == -1) {
+            return null;
+        }
+        boolean simple = true;
+        for (int i = 0; i < strLen; i++) {
+            byte b = buf[index + i];
+            if (b < 0) {
+                simple = false;
+                break;
+            }
+        }
+        String result;
+        if (simple) {
+            @SuppressWarnings("deprecation")
+            String s = new String(buf, 0, index, strLen);
+            result = s;
+        } else {
+            result = new String(buf, index, strLen, StandardCharsets.UTF_8);
+        }
+        index += strLen;
+        return result;
+    }
+
+    /**
+     * Grab all bytes written so far and return them as byte array and then reset write index to
+     * zero for fresh writing.
+     * 
+     * @return bytes written prior call to this method.
+     */
+    public byte[] resetWrite() {
+        byte[] result = new byte[index];
+        System.arraycopy(buf, 0, result, 0, index);
+        index = 0;
+        return result;
+    }
+
+    public boolean isEmpty() {
+        return (index == 0);
+    }
+
+    public boolean isReadFinished() {
+        return (index == buf.length);
+    }
+
+    private void ensureCapacity(int nBytes) {
+        int requireLen = index + nBytes;
+        if (requireLen > buf.length) {
+            byte[] newBuf = new byte[Math.max(requireLen, buf.length << 1)];
+            System.arraycopy(buf, 0, newBuf, 0, index);
+            buf = newBuf;
+        }
+    }
+
+    private void ensureData(int nBytes) {
+        if (index + nBytes > limit) {
+            throw RInternalError.unimplemented("Unexpected EOF: " + (nBytes - limit) + " more bytes expected.");
+        }
+    }
+
+}
diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/InitWindowedDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/InitWindowedDevice.java
index 73a6bf369a..a80d86a008 100644
--- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/InitWindowedDevice.java
+++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/InitWindowedDevice.java
@@ -31,8 +31,7 @@ import com.oracle.truffle.api.TruffleLanguage;
 import com.oracle.truffle.r.library.fastrGrid.GridContext;
 import com.oracle.truffle.r.library.fastrGrid.WindowDevice;
 import com.oracle.truffle.r.library.fastrGrid.device.GridDevice;
-import com.oracle.truffle.r.library.fastrGrid.device.awt.BufferedImageDevice;
-import com.oracle.truffle.r.library.fastrGrid.device.awt.BufferedImageDevice.NotSupportedImageFormatException;
+import com.oracle.truffle.r.library.fastrGrid.device.NotSupportedImageFormatException;
 import com.oracle.truffle.r.library.fastrGrid.device.awt.Graphics2DDevice;
 import com.oracle.truffle.r.nodes.builtin.RExternalBuiltinNode;
 import com.oracle.truffle.r.runtime.FastRConfig;
@@ -94,7 +93,7 @@ public final class InitWindowedDevice extends RExternalBuiltinNode {
         }
 
         // otherwise create the window ourselves
-        GridDevice device = WindowDevice.createWindowDevice(width, height);
+        GridDevice device = WindowDevice.createWindowDevice(false, width, height);
         String name = isFastRDevice ? "awt" : "X11cairo";
         GridContext.getContext().setCurrentDevice(name, device);
         return RNull.instance;
@@ -104,7 +103,7 @@ public final class InitWindowedDevice extends RExternalBuiltinNode {
         String formatName = name.substring(0, name.indexOf("::"));
         String filename = name.substring(name.lastIndexOf(':') + 1);
         try {
-            BufferedImageDevice device = BufferedImageDevice.open(FileDevUtils.formatInitialFilename(filename), formatName, width, height);
+            GridDevice device = GridContext.openLocalOrRemoteDevice(FileDevUtils.formatInitialFilename(filename), formatName, width, height);
             GridContext.getContext().setCurrentDevice(formatName.toUpperCase(), device, filename);
         } catch (NotSupportedImageFormatException e) {
             throw error(Message.GENERIC, String.format("Format '%s' is not supported.", formatName));
diff --git a/com.oracle.truffle.r.runtime/src/com/oracle/truffle/r/runtime/FastRConfig.java b/com.oracle.truffle.r.runtime/src/com/oracle/truffle/r/runtime/FastRConfig.java
index a91d29ca1c..955d4969f1 100644
--- a/com.oracle.truffle.r.runtime/src/com/oracle/truffle/r/runtime/FastRConfig.java
+++ b/com.oracle.truffle.r.runtime/src/com/oracle/truffle/r/runtime/FastRConfig.java
@@ -28,6 +28,8 @@ public final class FastRConfig {
      */
     public static final boolean InternalGridAwtSupport;
 
+    public static final boolean UseRemoteGridAwtDevice;
+
     /**
      * Umbrella option, which changes default values of other options in a way that FastR will not
      * invoke any native code directly and other potentially security sensitive operations are
@@ -59,10 +61,12 @@ public final class FastRConfig {
             InternalGridAwtSupport = false;
             UseMXBeans = false;
             UseNativeEventLoop = false;
+            UseRemoteGridAwtDevice = false;
         } else {
-            InternalGridAwtSupport = getBoolean("fastr.internal.grid.awt.support");
-            UseMXBeans = getBoolean("fastr.internal.usemxbeans");
-            UseNativeEventLoop = getBoolean("fastr.internal.usenativeeventloop");
+            InternalGridAwtSupport = getBooleanOrTrue("fastr.internal.grid.awt.support");
+            UseMXBeans = getBooleanOrTrue("fastr.internal.usemxbeans");
+            UseNativeEventLoop = getBooleanOrTrue("fastr.internal.usenativeeventloop");
+            UseRemoteGridAwtDevice = getBooleanOrFalse("fastr.use.remote.grid.awt.device");
         }
         DefaultDownloadMethod = System.getProperty("fastr.internal.defaultdownloadmethod");
     }
@@ -71,7 +75,12 @@ public final class FastRConfig {
         // only static fields
     }
 
-    private static boolean getBoolean(String propName) {
+    private static boolean getBooleanOrFalse(String propName) {
+        String val = System.getProperty(propName);
+        return val != null && val.equals("true");
+    }
+
+    private static boolean getBooleanOrTrue(String propName) {
         String val = System.getProperty(propName);
         return val == null || val.equals("true");
     }
diff --git a/mx.fastr/mx_fastr.py b/mx.fastr/mx_fastr.py
index beb02cb20e..70cce2fe7e 100644
--- a/mx.fastr/mx_fastr.py
+++ b/mx.fastr/mx_fastr.py
@@ -107,6 +107,11 @@ def do_run_r(args, command, extraVmArgs=None, jdk=None, **kwargs):
         vmArgs.extend(_command_class_dict[command.lower()])
     return mx.run_java(vmArgs + args, jdk=jdk, **kwargs)
 
+def run_grid_server(args, **kwargs):
+    vmArgs = mx.get_runtime_jvm_args(['GRID_DEVICE_REMOTE_SERVER'], jdk=get_default_jdk())
+    vmArgs.append('com.oracle.truffle.r.library.fastrGrid.device.remote.server.RemoteDeviceServer')
+    return mx.run_java(vmArgs + args, jdk=get_default_jdk(), **kwargs)
+
 def r_classpath(args):
     print mx.classpath('FASTR', jdk=mx.get_jdk())
 
@@ -594,6 +599,7 @@ _commands = {
     'R' : [rshell, '[options]'],
     'rscript' : [rscript, '[options]'],
     'Rscript' : [rscript, '[options]'],
+    'gridserver' : [run_grid_server, ''],
     'rtestgen' : [testgen, ''],
     'rgate' : [rgate, ''],
     'rutsimple' : [ut_simple, ['options']],
diff --git a/mx.fastr/native-image.properties b/mx.fastr/native-image.properties
index b0cc90d91a..9c953e3666 100644
--- a/mx.fastr/native-image.properties
+++ b/mx.fastr/native-image.properties
@@ -7,11 +7,11 @@ Requires = tool:truffle
 
 JavaArgs = \
     -Dfastr.resource.factory.class=com.oracle.truffle.r.nodes.builtin.EagerResourceHandlerFactory \
-    -Dfastr.internal.grid.awt.support=false \
     -Dfastr.internal.usemxbeans=false \
     -Dfastr.internal.usenativeeventloop=false \
     -Dfastr.internal.defaultdownloadmethod=wget \
     -Dfastr.internal.ignorejvmargs=true \
+    -Dfastr.use.remote.grid.awt.device=true \
     -Xmx6G
 
 LauncherClass = com.oracle.truffle.r.launcher.RMain
@@ -19,4 +19,5 @@ LauncherClassPath = lib/graalvm/launcher-common.jar:languages/R/fastr-launcher.j
 
 Args = -H:MaxRuntimeCompileMethods=8000 \
     -H:-TruffleCheckFrameImplementation \
-    -H:+TruffleCheckNeverPartOfCompilation
+    -H:+TruffleCheckNeverPartOfCompilation \
+    -H:EnableURLProtocols=http
diff --git a/mx.fastr/suite.py b/mx.fastr/suite.py
index 727d1f1a43..06ba950ab0 100644
--- a/mx.fastr/suite.py
+++ b/mx.fastr/suite.py
@@ -311,6 +311,20 @@ suite = {
 
     },
 
+    "com.oracle.truffle.r.library.fastrGrid.server" : {
+      "sourceDirs" : ["src"],
+      "dependencies" : [
+        "com.oracle.truffle.r.library",
+      ],
+      "annotationProcessors" : [
+      ],
+      "checkstyle" : "com.oracle.truffle.r.runtime",
+      "javaCompliance" : "1.8",
+      "workingSets" : "FastR",
+      "jacoco" : "include",
+
+    },
+
     "com.oracle.truffle.r.release" : {
       "sourceDirs" : ["src"],
       "buildDependencies" : ["com.oracle.truffle.r.native.recommended"],
@@ -396,6 +410,20 @@ suite = {
       ],
     },
 
+    "GRID_DEVICE_REMOTE_SERVER" : {
+      "description" : "remote server for grid device",
+      "dependencies" : [
+        "com.oracle.truffle.r.library.fastrGrid.server",
+      ],
+      "mainClass" : "com.oracle.truffle.r.library.fastrGrid.server.RemoteDeviceServer",
+      "exclude" : [
+        "truffle:JLINE",
+        "ANTLR-3.5",
+        "GNUR",
+        "XZ-1.6",
+      ],
+    },
+
     "FASTR_UNIT_TESTS" : {
       "description" : "unit tests",
       "dependencies" : [
-- 
GitLab