changeset 11247:01da8b583966

8217605: Add support for e-paper displays Reviewed-by: kcr, jvos Contributed-by: john@status6.com
author kcr
date Tue, 16 Apr 2019 15:25:29 -0700
parents b1b8c9c65c2a
children 58afdd563c0d
files buildSrc/armv6hf.gradle modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDFrameBuffer.java modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDInputDeviceRegistry.java modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDPlatform.java modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDPlatformFactory.java modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDScreen.java modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDSettings.java modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDSystem.java modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/FramebufferY8.java modules/javafx.graphics/src/main/native-glass/monocle/epd/EPDSystem.c modules/javafx.graphics/src/shims/java/com/sun/glass/ui/monocle/EPDSettingsShim.java modules/javafx.graphics/src/shims/java/com/sun/glass/ui/monocle/FramebufferY8Shim.java modules/javafx.graphics/src/shims/java/com/sun/glass/ui/monocle/FramebufferY8SuperShim.java tests/system/src/test/java/test/com/sun/glass/ui/monocle/EPDSettingsTest.java tests/system/src/test/java/test/com/sun/glass/ui/monocle/FramebufferY8Test.java
diffstat 15 files changed, 3588 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/buildSrc/armv6hf.gradle	Tue Apr 16 12:02:22 2019 -0700
+++ b/buildSrc/armv6hf.gradle	Tue Apr 16 15:25:29 2019 -0700
@@ -227,14 +227,15 @@
     "com/sun/glass/ui/*"]
 ARMV6HF.glass.variants = [ ]
 if (ARMV6HF.includeMonocle) {
-    ARMV6HF.glass.variants.addAll("monocle", "monocle_x11");
+    ARMV6HF.glass.variants.addAll("monocle", "monocle_x11", "monocle_epd");
     ARMV6HF.glass.javahInclude.addAll(
         "com/sun/glass/ui/monocle/*",
         "com/sun/glass/ui/monocle/dispman/*",
         "com/sun/glass/ui/monocle/mx6/*",
         "com/sun/glass/ui/monocle/linux/*",
         "com/sun/glass/ui/monocle/util/*",
-        "com/sun/glass/ui/monocle/x11/*");
+        "com/sun/glass/ui/monocle/x11/*",
+        "com/sun/glass/ui/monocle/epd/*");
     ARMV6HF.javafxPlatformProperties = ARMV6HF.javafxPlatformProperties + monoclePlatformAdditions
 }
 if (ARMV6HF.includeGTK) {
@@ -268,6 +269,15 @@
 ARMV6HF.glass.monocle_x11.linkFlags = [ monocleLFlags, "-lX11" ].flatten()
 ARMV6HF.glass.monocle_x11.lib = "glass_monocle_x11"
 
+ARMV6HF.glass.monocle_epd = [:]
+ARMV6HF.glass.monocle_epd.nativeSource = [
+        file("${project("graphics").projectDir}/src/main/native-glass/monocle/epd") ]
+ARMV6HF.glass.monocle_epd.compiler = compiler
+ARMV6HF.glass.monocle_epd.ccFlags = monocleCFlags
+ARMV6HF.glass.monocle_epd.linker = linker
+ARMV6HF.glass.monocle_epd.linkFlags = monocleLFlags
+ARMV6HF.glass.monocle_epd.lib = "glass_monocle_epd"
+
 FileTree ft_gtk = fileTree("${project(":graphics").projectDir}/src/main/native-glass/gtk/") {
     exclude("**/launcher.c")
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDFrameBuffer.java	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,649 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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.sun.glass.ui.monocle;
+
+import com.sun.glass.ui.monocle.EPDSystem.FbVarScreenInfo;
+import com.sun.glass.ui.monocle.EPDSystem.IntStructure;
+import com.sun.glass.ui.monocle.EPDSystem.MxcfbUpdateData;
+import com.sun.glass.ui.monocle.EPDSystem.MxcfbWaveformModes;
+import com.sun.javafx.logging.PlatformLogger;
+import com.sun.javafx.logging.PlatformLogger.Level;
+import com.sun.javafx.util.Logging;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.text.MessageFormat;
+
+/**
+ * Represents the standard Linux frame buffer device interface plus the custom
+ * extensions to that interface provided by the Electrophoretic Display
+ * Controller (EPDC) frame buffer driver.
+ * <p>
+ * The Linux frame buffer device interface is documented in <cite>The Frame
+ * Buffer Device API</cite> found in the Ubuntu package called <i>linux-doc</i>
+ * (see <i>/usr/share/doc/linux-doc/fb/api.txt.gz</i>).</p>
+ * <p>
+ * The EPDC frame buffer driver extensions are documented in the <cite>i.MX
+ * Linux Reference Manual</cite> available on the
+ * <a href="https://www.nxp.com/">NXP website</a> (registration required). On
+ * the NXP home page, click Products, ARM Processors, i.MX Application
+ * Processors, and then i.MX 6 Processors, for example. Select the i.MX6SLL
+ * Product in the chart; then click the Documentation tab. Look for a download
+ * with a label for Linux documents, like L4.1.15_2.1.0_LINUX_DOCS, under the
+ * Supporting Information section. After downloading and expanding the archive,
+ * the reference manual is found in the <i>doc</i> directory as the file
+ * <i>i.MX_Linux_Reference_Manual.pdf</i>.</p>
+ */
+class EPDFrameBuffer {
+
+    /**
+     * The arithmetic right shift value to convert a bit depth to a byte depth.
+     */
+    private static final int BITS_TO_BYTES = 3;
+
+    /**
+     * The delay in milliseconds between the completion of all updates in the
+     * EPDC driver and when the driver powers down the EPDC and display power
+     * supplies.
+     */
+    private static final int POWERDOWN_DELAY = 1_000;
+
+    private final PlatformLogger logger = Logging.getJavaFXLogger();
+    private final EPDSettings settings;
+    private final LinuxSystem system;
+    private final EPDSystem driver;
+    private final long fd;
+
+    private final int xres;
+    private final int yres;
+    private final int xresVirtual;
+    private final int yresVirtual;
+    private final int xoffset;
+    private final int yoffset;
+    private final int bitsPerPixel;
+    private final int bytesPerPixel;
+    private final int byteOffset;
+    private final MxcfbUpdateData updateData;
+    private final MxcfbUpdateData syncUpdate;
+
+    private int updateMarker;
+    private int lastMarker;
+
+    /**
+     * Creates a new {@code EPDFrameBuffer} for the given frame buffer device.
+     * The geometry of the Linux frame buffer is shown below for various color
+     * depths and rotations on a sample system, as printed by the <i>fbset</i>
+     * command. The first three are for landscape mode, while the last three are
+     * for portrait.
+     * <pre>{@code
+     * geometry 800 600 800 640 32 (line length: 3200)
+     * geometry 800 600 800 1280 16 (line length: 1600)
+     * geometry 800 600 800 1280 8 (line length: 800)
+     *
+     * geometry 600 800 608 896 32 (line length: 2432)
+     * geometry 600 800 608 1792 16 (line length: 1216)
+     * geometry 600 800 608 1792 8 (line length: 608)
+     * }</pre>
+     *
+     * @implNote {@code MonocleApplication} creates a {@code Screen} which
+     * requires that the width be set to {@link #xresVirtual} even though only
+     * the first {@link #xres} pixels of each row are visible. The EPDC driver
+     * supports panning only in the y-direction, so it is not possible to center
+     * the visible resolution horizontally when these values differ. The JavaFX
+     * application should be left-aligned in this case and ignore the few extra
+     * pixels on the right of its screen.
+     *
+     * @param fbPath the frame buffer device path, such as <i>/dev/fb0</i>
+     * @throws IOException if an error occurs when opening the frame buffer
+     * device or when getting or setting the frame buffer configuration
+     * @throws IllegalArgumentException if the EPD settings specify an
+     * unsupported color depth
+     */
+    EPDFrameBuffer(String fbPath) throws IOException {
+        settings = EPDSettings.newInstance();
+        system = LinuxSystem.getLinuxSystem();
+        driver = EPDSystem.getEPDSystem();
+        fd = system.open(fbPath, LinuxSystem.O_RDWR);
+        if (fd == -1) {
+            throw new IOException(system.getErrorMessage());
+        }
+
+        /*
+         * Gets the current settings of the frame buffer device.
+         */
+        var screen = new FbVarScreenInfo();
+        getScreenInfo(screen);
+
+        /*
+         * Changes the settings of the frame buffer from the system properties.
+         *
+         * See the section, "Format configuration," in "The Frame Buffer Device
+         * API" for details. Note that xoffset is always zero, and yoffset can
+         * be modified only by panning in the y-direction with the IOCTL call to
+         * LinuxSystem.FBIOPAN_DISPLAY.
+         */
+        screen.setBitsPerPixel(screen.p, settings.bitsPerPixel);
+        screen.setGrayscale(screen.p, settings.grayscale);
+        switch (settings.bitsPerPixel) {
+            case Byte.SIZE:
+                // rgba 8/0,8/0,8/0,0/0 (set by driver when grayscale > 0)
+                screen.setRed(screen.p, 0, 0);
+                screen.setGreen(screen.p, 0, 0);
+                screen.setBlue(screen.p, 0, 0);
+                screen.setTransp(screen.p, 0, 0);
+                break;
+            case Short.SIZE:
+                // rgba 5/11,6/5,5/0,0/0
+                screen.setRed(screen.p, 5, 11);
+                screen.setGreen(screen.p, 6, 5);
+                screen.setBlue(screen.p, 5, 0);
+                screen.setTransp(screen.p, 0, 0);
+                break;
+            case Integer.SIZE:
+                // rgba 8/16,8/8,8/0,8/24
+                screen.setRed(screen.p, 8, 16);
+                screen.setGreen(screen.p, 8, 8);
+                screen.setBlue(screen.p, 8, 0);
+                screen.setTransp(screen.p, 8, 24);
+                break;
+            default:
+                String msg = MessageFormat.format("Unsupported color depth: {0} bpp", settings.bitsPerPixel);
+                logger.severe(msg);
+                throw new IllegalArgumentException(msg);
+        }
+        screen.setActivate(screen.p, EPDSystem.FB_ACTIVATE_FORCE);
+        screen.setRotate(screen.p, settings.rotate);
+        setScreenInfo(screen);
+
+        /*
+         * Gets and logs the new settings of the frame buffer device.
+         */
+        getScreenInfo(screen);
+        logScreenInfo(screen);
+        xres = screen.getXRes(screen.p);
+        yres = screen.getYRes(screen.p);
+        xresVirtual = screen.getXResVirtual(screen.p);
+        yresVirtual = screen.getYResVirtual(screen.p);
+        xoffset = screen.getOffsetX(screen.p);
+        yoffset = screen.getOffsetY(screen.p);
+        bitsPerPixel = screen.getBitsPerPixel(screen.p);
+        bytesPerPixel = bitsPerPixel >>> BITS_TO_BYTES;
+        byteOffset = (xoffset + yoffset * xresVirtual) * bytesPerPixel;
+
+        /*
+         * Allocates objects for reuse to avoid creating new direct byte buffers
+         * outside of the Java heap on each display update.
+         */
+        updateData = new MxcfbUpdateData();
+        syncUpdate = createDefaultUpdate(xres, yres);
+    }
+
+    /**
+     * Gets the variable screen information of the frame buffer. Run the
+     * <i>fbset</i> command as <i>root</i> to print the screen information.
+     *
+     * @param screen the object representing the variable screen information
+     * @throws IOException if an error occurs getting the information
+     */
+    private void getScreenInfo(FbVarScreenInfo screen) throws IOException {
+        int rc = system.ioctl(fd, LinuxSystem.FBIOGET_VSCREENINFO, screen.p);
+        if (rc != 0) {
+            system.close(fd);
+            throw new IOException(system.getErrorMessage());
+        }
+    }
+
+    /**
+     * Sets the variable screen information of the frame buffer.
+     * <p>
+     * "To ensure that the EPDC driver receives the initialization request, the
+     * {@code activate} field of the {@code fb_var_screeninfo} parameter should
+     * be set to {@code FB_ACTIVATE_FORCE}." [EPDC Panel Initialization,
+     * <cite>i.MX Linux Reference Manual</cite>]</p>
+     * <p>
+     * To request a change to 8-bit grayscale format, the bits per pixel must be
+     * set to 8 and the grayscale value must be set to one of the two valid
+     * grayscale format values: {@code GRAYSCALE_8BIT} or
+     * {@code GRAYSCALE_8BIT_INVERTED}. [Grayscale Framebuffer Selection,
+     * <cite>i.MX Linux Reference Manual</cite>]</p>
+     *
+     * @param screen the object representing the variable screen information
+     * @throws IOException if an error occurs setting the information
+     */
+    private void setScreenInfo(FbVarScreenInfo screen) throws IOException {
+        int rc = system.ioctl(fd, LinuxSystem.FBIOPUT_VSCREENINFO, screen.p);
+        if (rc != 0) {
+            system.close(fd);
+            throw new IOException(system.getErrorMessage());
+        }
+    }
+
+    /**
+     * Logs the variable screen information of the frame buffer, depending on
+     * the logging level.
+     *
+     * @param screen the object representing the variable screen information
+     */
+    private void logScreenInfo(FbVarScreenInfo screen) {
+        if (logger.isLoggable(Level.FINE)) {
+            logger.fine("Frame buffer geometry: {0} {1} {2} {3} {4}",
+                    screen.getXRes(screen.p), screen.getYRes(screen.p),
+                    screen.getXResVirtual(screen.p), screen.getYResVirtual(screen.p),
+                    screen.getBitsPerPixel(screen.p));
+            logger.fine("Frame buffer rgba: {0}/{1},{2}/{3},{4}/{5},{6}/{7}",
+                    screen.getRedLength(screen.p), screen.getRedOffset(screen.p),
+                    screen.getGreenLength(screen.p), screen.getGreenOffset(screen.p),
+                    screen.getBlueLength(screen.p), screen.getBlueOffset(screen.p),
+                    screen.getTranspLength(screen.p), screen.getTranspOffset(screen.p));
+            logger.fine("Frame buffer grayscale: {0}", screen.getGrayscale(screen.p));
+        }
+    }
+
+    /**
+     * Creates the default update data with values from the EPD system
+     * properties, setting all fields except for the update marker. Reusing the
+     * update data object avoids creating a new one for each update request.
+     *
+     * @implNote An update mode of {@link EPDSystem#UPDATE_MODE_FULL} would make
+     * the {@link EPDSettings#NO_WAIT} system property useless by changing all
+     * non-colliding updates into colliding ones, so this method sets the
+     * default update mode to {@link EPDSystem#UPDATE_MODE_PARTIAL}.
+     *
+     * @param width the width of the update region
+     * @param height the height of the update region
+     * @return the default update data with all fields set but the update marker
+     */
+    private MxcfbUpdateData createDefaultUpdate(int width, int height) {
+        var update = new MxcfbUpdateData();
+        update.setUpdateRegion(update.p, 0, 0, width, height);
+        update.setWaveformMode(update.p, settings.waveformMode);
+        update.setUpdateMode(update.p, EPDSystem.UPDATE_MODE_PARTIAL);
+        update.setTemp(update.p, EPDSystem.TEMP_USE_AMBIENT);
+        update.setFlags(update.p, settings.flags);
+        return update;
+    }
+
+    /**
+     * Defines a mapping for common waveform modes. This mapping must be
+     * configured for the automatic waveform mode selection to function
+     * properly. Each of the parameters should be set to one of the following:
+     * <ul>
+     * <li>{@link EPDSystem#WAVEFORM_MODE_INIT}</li>
+     * <li>{@link EPDSystem#WAVEFORM_MODE_DU}</li>
+     * <li>{@link EPDSystem#WAVEFORM_MODE_GC16}</li>
+     * <li>{@link EPDSystem#WAVEFORM_MODE_GC4}</li>
+     * <li>{@link EPDSystem#WAVEFORM_MODE_A2}</li>
+     * </ul>
+     *
+     * @param init the initialization mode for clearing the screen to all white
+     * @param du the direct update mode for changing any gray values to either
+     * all black or all white
+     * @param gc4 the mode for 4-level (2-bit) grayscale images and text
+     * @param gc8 the mode for 8-level (3-bit) grayscale images and text
+     * @param gc16 the mode for 16-level (4-bit) grayscale images and text
+     * @param gc32 the mode for 32-level (5-bit) grayscale images and text
+     */
+    private void setWaveformModes(int init, int du, int gc4, int gc8, int gc16, int gc32) {
+        var modes = new MxcfbWaveformModes();
+        modes.setModes(modes.p, init, du, gc4, gc8, gc16, gc32);
+        int rc = system.ioctl(fd, driver.MXCFB_SET_WAVEFORM_MODES, modes.p);
+        if (rc != 0) {
+            logger.severe("Failed setting waveform modes: {0} ({1})",
+                    system.getErrorMessage(), system.errno());
+        }
+    }
+
+    /**
+     * Sets the temperature to be used by the EPDC driver in subsequent panel
+     * updates. Note that this temperature setting may be overridden by setting
+     * the temperature in a specific update to anything other than
+     * {@link EPDSystem#TEMP_USE_AMBIENT}.
+     *
+     * @param temp the temperature in degrees Celsius
+     */
+    private void setTemperature(int temp) {
+        int rc = driver.ioctl(fd, driver.MXCFB_SET_TEMPERATURE, temp);
+        if (rc != 0) {
+            logger.severe("Failed setting temperature to {2} °C: {0} ({1})",
+                    system.getErrorMessage(), system.errno(), temp);
+        }
+    }
+
+    /**
+     * Selects between automatic and region update mode. In region update mode,
+     * updates must be submitted with an IOCTL call to
+     * {@link EPDSystem#MXCFB_SEND_UPDATE}. In automatic mode, updates are
+     * generated by the driver when it detects that pages in a frame buffer
+     * memory region have been modified.
+     * <p>
+     * Automatic mode is available only when it has been enabled in the Linux
+     * kernel by the option CONFIG_FB_MXC_EINK_AUTO_UPDATE_MODE. You can find
+     * the configuration options used to build the kernel in a file under
+     * <i>/proc</i> or <i>/boot</i>, such as <i>/proc/config.gz</i>.</p>
+     *
+     * @param mode the automatic update mode, one of:
+     * <ul>
+     * <li>{@link EPDSystem#AUTO_UPDATE_MODE_REGION_MODE}</li>
+     * <li>{@link EPDSystem#AUTO_UPDATE_MODE_AUTOMATIC_MODE}</li>
+     * </ul>
+     */
+    private void setAutoUpdateMode(int mode) {
+        int rc = driver.ioctl(fd, driver.MXCFB_SET_AUTO_UPDATE_MODE, mode);
+        if (rc != 0) {
+            logger.severe("Failed setting auto-update mode to {2}: {0} ({1})",
+                    system.getErrorMessage(), system.errno(), mode);
+        }
+    }
+
+    /**
+     * Requests the entire visible region of the frame buffer to be updated to
+     * the display.
+     *
+     * @param updateMode the update mode, one of:
+     * <ul>
+     * <li>{@link EPDSystem#UPDATE_MODE_PARTIAL}</li>
+     * <li>{@link EPDSystem#UPDATE_MODE_FULL}</li>
+     * </ul>
+     * @param waveformMode the waveform mode, one of:
+     * <ul>
+     * <li>{@link EPDSystem#WAVEFORM_MODE_INIT}</li>
+     * <li>{@link EPDSystem#WAVEFORM_MODE_DU}</li>
+     * <li>{@link EPDSystem#WAVEFORM_MODE_GC16}</li>
+     * <li>{@link EPDSystem#WAVEFORM_MODE_GC4}</li>
+     * <li>{@link EPDSystem#WAVEFORM_MODE_A2}</li>
+     * <li>{@link EPDSystem#WAVEFORM_MODE_AUTO}</li>
+     * </ul>
+     * @param flags a bit mask composed of the following flag values:
+     * <ul>
+     * <li>{@link EPDSystem#EPDC_FLAG_ENABLE_INVERSION}</li>
+     * <li>{@link EPDSystem#EPDC_FLAG_FORCE_MONOCHROME}</li>
+     * <li>{@link EPDSystem#EPDC_FLAG_USE_DITHERING_Y1}</li>
+     * <li>{@link EPDSystem#EPDC_FLAG_USE_DITHERING_Y4}</li>
+     * </ul>
+     * @return the marker to identify this update in a subsequence call to
+     * {@link #waitForUpdateComplete}
+     */
+    private int sendUpdate(int updateMode, int waveformMode, int flags) {
+        updateData.setUpdateRegion(updateData.p, 0, 0, xres, yres);
+        updateData.setUpdateMode(updateData.p, updateMode);
+        updateData.setTemp(updateData.p, EPDSystem.TEMP_USE_AMBIENT);
+        updateData.setFlags(updateData.p, flags);
+        return sendUpdate(updateData, waveformMode);
+    }
+
+    /**
+     * Requests an update to the display, allowing for the reuse of the update
+     * data object. The waveform mode is reset because the update data could
+     * have been used in a previous update. In that case, the waveform mode may
+     * have been modified by the EPDC driver with the actual mode selected. The
+     * update marker is overwritten with the next sequential marker.
+     *
+     * @param update the data describing the update; the waveform mode and
+     * update marker are overwritten
+     * @param waveformMode the waveform mode for this update
+     * @return the marker to identify this update in a subsequence call to
+     * {@link #waitForUpdateComplete}
+     */
+    private int sendUpdate(MxcfbUpdateData update, int waveformMode) {
+        /*
+         * The IOCTL call to MXCFB_WAIT_FOR_UPDATE_COMPLETE returns the error
+         * "Invalid argument (22)" when passed an update marker of zero.
+         */
+        updateMarker++;
+        if (updateMarker == 0) {
+            updateMarker++;
+        }
+        update.setWaveformMode(update.p, waveformMode);
+        update.setUpdateMarker(update.p, updateMarker);
+        int rc = system.ioctl(fd, driver.MXCFB_SEND_UPDATE, update.p);
+        if (rc != 0) {
+            logger.severe("Failed sending update {2}: {0} ({1})",
+                    system.getErrorMessage(), system.errno(), Integer.toUnsignedLong(updateMarker));
+        } else if (logger.isLoggable(Level.FINER)) {
+            logger.finer("Sent update: {0} × {1}, waveform {2}, selected {3}, flags 0x{4}, marker {5}",
+                    update.getUpdateRegionWidth(update.p), update.getUpdateRegionHeight(update.p),
+                    waveformMode, update.getWaveformMode(update.p),
+                    Integer.toHexString(update.getFlags(update.p)).toUpperCase(),
+                    Integer.toUnsignedLong(updateMarker));
+        }
+        return updateMarker;
+    }
+
+    /**
+     * Blocks and waits for a previous update request to complete.
+     *
+     * @param marker the marker to identify a particular update, returned by
+     * {@link #sendUpdate(MxcfbUpdateData, int)}
+     */
+    private void waitForUpdateComplete(int marker) {
+        /*
+         * This IOCTL call returns: 0 if the marker was not found because the
+         * update already completed or failed, negative (-1) with the error
+         * "Connection timed out (110)" if the wait timed out after 5 seconds,
+         * or positive if the wait occurred and completed (see
+         * "wait_for_completion_timeout" in "kernel/sched/completion.c").
+         */
+        int rc = driver.ioctl(fd, driver.MXCFB_WAIT_FOR_UPDATE_COMPLETE, marker);
+        if (rc < 0) {
+            logger.severe("Failed waiting for update {2}: {0} ({1})",
+                    system.getErrorMessage(), system.errno(), Integer.toUnsignedLong(marker));
+        } else if (rc == 0 && logger.isLoggable(Level.FINER)) {
+            logger.finer("Update completed before wait: marker {0}",
+                    Integer.toUnsignedLong(marker));
+        }
+    }
+
+    /**
+     * Sets the delay between the completion of all updates in the driver and
+     * when the driver should power down the EPDC and display power supplies. To
+     * disable powering down entirely, use the delay value
+     * {@link EPDSystem#FB_POWERDOWN_DISABLE}.
+     *
+     * @param delay the delay in milliseconds
+     */
+    private void setPowerdownDelay(int delay) {
+        int rc = driver.ioctl(fd, driver.MXCFB_SET_PWRDOWN_DELAY, delay);
+        if (rc != 0) {
+            logger.severe("Failed setting power-down delay to {2}: {0} ({1})",
+                    system.getErrorMessage(), system.errno(), delay);
+        }
+    }
+
+    /**
+     * Gets the current power-down delay from the EPDC driver.
+     *
+     * @return the delay in milliseconds
+     */
+    private int getPowerdownDelay() {
+        var integer = new IntStructure();
+        int rc = system.ioctl(fd, driver.MXCFB_GET_PWRDOWN_DELAY, integer.p);
+        if (rc != 0) {
+            logger.severe("Failed getting power-down delay: {0} ({1})",
+                    system.getErrorMessage(), system.errno());
+        }
+        return integer.getInteger(integer.p);
+    }
+
+    /**
+     * Selects a scheme for the flow of updates within the driver.
+     *
+     * @param scheme the update scheme, one of:
+     * <ul>
+     * <li>{@link EPDSystem#UPDATE_SCHEME_SNAPSHOT}</li>
+     * <li>{@link EPDSystem#UPDATE_SCHEME_QUEUE}</li>
+     * <li>{@link EPDSystem#UPDATE_SCHEME_QUEUE_AND_MERGE}</li>
+     * </ul>
+     */
+    private void setUpdateScheme(int scheme) {
+        int rc = driver.ioctl(fd, driver.MXCFB_SET_UPDATE_SCHEME, scheme);
+        if (rc != 0) {
+            logger.severe("Failed setting update scheme to {2}: {0} ({1})",
+                    system.getErrorMessage(), system.errno(), scheme);
+        }
+    }
+
+    /**
+     * Initializes the EPDC frame buffer device, setting the update scheme to
+     * {@link EPDSystem#UPDATE_SCHEME_SNAPSHOT}.
+     */
+    void init() {
+        setWaveformModes(EPDSystem.WAVEFORM_MODE_INIT, EPDSystem.WAVEFORM_MODE_DU,
+                EPDSystem.WAVEFORM_MODE_GC4, EPDSystem.WAVEFORM_MODE_GC16,
+                EPDSystem.WAVEFORM_MODE_GC16, EPDSystem.WAVEFORM_MODE_GC16);
+        setTemperature(EPDSystem.TEMP_USE_AMBIENT);
+        setAutoUpdateMode(EPDSystem.AUTO_UPDATE_MODE_REGION_MODE);
+        setPowerdownDelay(POWERDOWN_DELAY);
+        setUpdateScheme(EPDSystem.UPDATE_SCHEME_SNAPSHOT);
+    }
+
+    /**
+     * Clears the display panel. The visible frame buffer should be cleared with
+     * zeros when called. This method sends two direct updates (all black
+     * followed by all white) to refresh the screen and clear any ghosting
+     * effects, and returns when both updates are complete.
+     * <p>
+     * <strong>This method is not thread safe</strong>, but it is invoked only
+     * once from the Event Thread during initialization.</p>
+     */
+    void clear() {
+        lastMarker = sendUpdate(EPDSystem.UPDATE_MODE_FULL,
+                EPDSystem.WAVEFORM_MODE_DU, 0);
+        lastMarker = sendUpdate(EPDSystem.UPDATE_MODE_FULL,
+                EPDSystem.WAVEFORM_MODE_DU, EPDSystem.EPDC_FLAG_ENABLE_INVERSION);
+        waitForUpdateComplete(lastMarker);
+    }
+
+    /**
+     * Sends the updated contents of the Linux frame buffer to the EPDC driver,
+     * optionally synchronizing with the driver by first waiting for the
+     * previous update to complete.
+     * <p>
+     * <strong>This method is not thread safe</strong>, but it is invoked only
+     * from the JavaFX Application Thread.</p>
+     */
+    void sync() {
+        if (!settings.noWait) {
+            waitForUpdateComplete(lastMarker);
+        }
+        lastMarker = sendUpdate(syncUpdate, settings.waveformMode);
+    }
+
+    /**
+     * Gets the number of bytes from the beginning of the frame buffer to the
+     * start of its visible resolution.
+     *
+     * @return the offset in bytes
+     */
+    int getByteOffset() {
+        return byteOffset;
+    }
+
+    /**
+     * Creates an off-screen byte buffer equal in resolution to the virtual
+     * resolution of the frame buffer, but with 32 bits per pixel.
+     *
+     * @return a 32-bit pixel buffer matching the resolution of the frame buffer
+     */
+    ByteBuffer getOffscreenBuffer() {
+        /*
+         * Allocates a direct byte buffer to avoid bug JDK-8201567,
+         * "QuantumRenderer modifies buffer in use by JavaFX Application Thread"
+         * <https://bugs.openjdk.java.net/browse/JDK-8201567>.
+         */
+        int size = xresVirtual * yresVirtual * Integer.SIZE;
+        return ByteBuffer.allocateDirect(size);
+    }
+
+    /**
+     * Creates a new mapping of the Linux frame buffer device into memory.
+     *
+     * @return a byte buffer containing the mapping of the Linux frame buffer
+     * device
+     */
+    ByteBuffer getMappedBuffer() {
+        int size = xresVirtual * yresVirtual * bytesPerPixel;
+        long addr = system.mmap(0l, size, LinuxSystem.PROT_WRITE, LinuxSystem.MAP_SHARED, fd, 0);
+        return addr == LinuxSystem.MAP_FAILED ? null : C.getC().NewDirectByteBuffer(addr, size);
+    }
+
+    /**
+     * Deletes the mapping of the Linux frame buffer device.
+     *
+     * @param buffer the byte buffer containing the mapping of the Linux frame
+     * buffer device
+     */
+    void releaseMappedBuffer(ByteBuffer buffer) {
+        system.munmap(C.getC().GetDirectBufferAddress(buffer), buffer.capacity());
+    }
+
+    /**
+     * Closes the Linux frame buffer device.
+     */
+    void close() {
+        system.close(fd);
+    }
+
+    /**
+     * Gets the native handle to the Linux frame buffer device.
+     *
+     * @return the frame buffer device file descriptor
+     */
+    long getNativeHandle() {
+        return fd;
+    }
+
+    /**
+     * Gets the virtual horizontal resolution of the frame buffer. See the notes
+     * for the {@linkplain EPDFrameBuffer#EPDFrameBuffer constructor} above.
+     *
+     * @return the virtual width in pixels
+     */
+    int getWidth() {
+        return xresVirtual;
+    }
+
+    /**
+     * Gets the visible vertical resolution of the frame buffer.
+     *
+     * @return the visible height in pixels
+     */
+    int getHeight() {
+        return yres;
+    }
+
+    /**
+     * Gets the color depth of the frame buffer.
+     *
+     * @return the color depth in bits per pixel
+     */
+    int getBitDepth() {
+        return bitsPerPixel;
+    }
+
+    @Override
+    public String toString() {
+        return MessageFormat.format("{0}[width={1} height={2} bitDepth={3}]",
+                getClass().getName(), getWidth(), getHeight(), getBitDepth());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDInputDeviceRegistry.java	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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.sun.glass.ui.monocle;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Maintains an observable set of input devices. This class is responsible for
+ * detecting the attached input devices and generating their input events. Run
+ * the following commands as <i>root</i> to list the properties of the keypad
+ * (event0) and touch screen (event1) input devices on the system:
+ * <pre>{@code
+ * # udevadm info -q all -n /dev/input/event0
+ * # udevadm info -q all -n /dev/input/event1
+ * }</pre>
+ *
+ * @implNote {@code EPDPlatform} creates an instance of this class instead of
+ * {@code LinuxInputDeviceRegistry} because this class replaces two of its
+ * methods.
+ * <p>
+ * It replaces the {@link #createDevice} method to work around bug JDK-8201568
+ * by opening the device before creating its {@code LinuxInputDevice}. It also
+ * replaces the {@link #addDeviceInternal} method to work around older versions
+ * of <i>udev</i>, such as version 142, which do not provide the
+ * ID_INPUT_TOUCHSCREEN=1 property for the touch screen device.
+ * {@link LinuxInputDevice#isTouch} requires that property and value; otherwise
+ * the method returns {@code false}, and the touch screen is mistakenly assigned
+ * a keyboard input processor. Newer versions of <i>udev</i>, such as version
+ * 204, provide the correct property and value.</p>
+ * <p>
+ * Therefore, once JDK-8201568 is fixed and the old version of <i>udev</i> is no
+ * longer in use, this entire class can be removed and replaced by
+ * {@code LinuxInputDeviceRegistry}.</p>
+ */
+class EPDInputDeviceRegistry extends InputDeviceRegistry {
+
+    /**
+     * The file name of the keypad input device.
+     */
+    private static final String KEYPAD_FILENAME = "event0";
+
+    /**
+     * The file name of the touch screen input device.
+     */
+    private static final String TOUCH_FILENAME = "event1";
+
+    /**
+     * Creates a new observable set of input devices.
+     *
+     * @implNote This is a verbatim copy of the {@link LinuxInputDeviceRegistry}
+     * constructor.
+     *
+     * @param headless {@code true} if this environment cannot support a
+     * display, keyboard, and mouse; otherwise {@code false}
+     */
+    EPDInputDeviceRegistry(boolean headless) {
+        if (headless) {
+            // Keep the registry but do not bind it to udev.
+            return;
+        }
+        Map<File, LinuxInputDevice> deviceMap = new HashMap<>();
+        UdevListener udevListener = (action, event) -> {
+            String subsystem = event.get("SUBSYSTEM");
+            String devPath = event.get("DEVPATH");
+            String devName = event.get("DEVNAME");
+            if (subsystem != null && subsystem.equals("input")
+                    && devPath != null && devName != null) {
+                try {
+                    File sysPath = new File("/sys", devPath);
+                    if (action.equals("add")
+                            || (action.equals("change")
+                            && !deviceMap.containsKey(sysPath))) {
+                        File devNode = new File(devName);
+                        LinuxInputDevice device = createDevice(
+                                devNode, sysPath, event);
+                        if (device != null) {
+                            deviceMap.put(sysPath, device);
+                        }
+                    } else if (action.equals("remove")) {
+                        LinuxInputDevice device = deviceMap.get(sysPath);
+                        deviceMap.remove(sysPath);
+                        if (device != null) {
+                            devices.remove(device);
+                        }
+                    }
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        };
+        Udev.getInstance().addListener(udevListener);
+        // Request updates for existing devices
+        SysFS.triggerUdevNotification("input");
+    }
+
+    /**
+     * Creates a Linux input device with the given properties.
+     *
+     * @implNote Works around bug
+     * <a href="https://bugs.openjdk.java.net/browse/JDK-8201568">JDK-8201568</a>,
+     * "zForce touchscreen input device fails when closed and immediately
+     * reopened," by opening the device before creating its
+     * {@code LinuxInputDevice}.
+     *
+     * @param devNode the file representing the device name, such as
+     * <i>/dev/input/event1</i>
+     * @param sysPath the system path to the device, such as
+     * <i>/sys/devices/virtual/input/input1/event1</i>
+     * @param udevManifest the set of properties for the device
+     * @return the new Linux input device, or {@code null} if no processor is
+     * found for the device
+     * @throws IOException if an error occurs opening the device
+     */
+    private LinuxInputDevice createDevice(File devNode, File sysPath,
+            Map<String, String> udevManifest) throws IOException {
+        LinuxSystem system = LinuxSystem.getLinuxSystem();
+        system.open(devNode.getPath(), LinuxSystem.O_RDONLY);
+
+        var device = new LinuxInputDevice(devNode, sysPath, udevManifest);
+        return addDeviceInternal(device, "Linux input: " + devNode.toString());
+    }
+
+    /**
+     * Creates an input processor for the device which runs on a new daemon
+     * background thread. Run the following commands as <i>root</i> to display
+     * the events generated by the keypad (0) and touch screen (1) input devices
+     * when you press buttons or touch the screen:
+     * <pre>{@code
+     * # input-events 0
+     * # input-events 1
+     * }</pre>
+     *
+     * @implNote The "mxckpd" keypad device driver does not generate EV_SYN
+     * events, yet the {@link LinuxInputDevice#run} method schedules an event
+     * for processing only after receiving the EV_SYN event terminator (see the
+     * {@link LinuxEventBuffer#put} method). The events from this device,
+     * therefore, are never delivered to the JavaFX application. The "gpio-keys"
+     * keypad device driver on more recent systems, though, correctly generates
+     * the EV_SYN event terminator.
+     *
+     * @param device the Linux input device
+     * @param name the device name, such as <i>/dev/input/event0</i>
+     * @return the Linux input device, or {@code null} if no input processor is
+     * found for the device
+     */
+    private LinuxInputDevice addDeviceInternal(LinuxInputDevice device, String name) {
+        LinuxInputProcessor processor = null;
+        if (name.endsWith(KEYPAD_FILENAME)) {
+            processor = new LinuxKeyProcessor();
+        } else if (name.endsWith(TOUCH_FILENAME)) {
+            processor = new LinuxSimpleTouchProcessor(device);
+        }
+        if (processor == null) {
+            return null;
+        } else {
+            device.setInputProcessor(processor);
+            var thread = new Thread(device);
+            thread.setName(name);
+            thread.setDaemon(true);
+            thread.start();
+            devices.add(device);
+            return device;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return MessageFormat.format("{0}[devices={1}]", getClass().getName(), devices);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDPlatform.java	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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.sun.glass.ui.monocle;
+
+/**
+ * A native platform for a Linux system with an electrophoretic display, also
+ * called an e-paper display.
+ */
+class EPDPlatform extends LinuxPlatform {
+
+    /**
+     * Creates a new Monocle EPD Platform.
+     */
+    EPDPlatform() {
+        EPDSystem.getEPDSystem().loadLibrary();
+    }
+
+    @Override
+    protected InputDeviceRegistry createInputDeviceRegistry() {
+        return new EPDInputDeviceRegistry(false);
+    }
+
+    @Override
+    protected NativeScreen createScreen() {
+        try {
+            return new EPDScreen();
+        } catch (RuntimeException e) {
+            return new HeadlessScreen();
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDPlatformFactory.java	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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.sun.glass.ui.monocle;
+
+import com.sun.javafx.logging.PlatformLogger;
+import com.sun.javafx.util.Logging;
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.text.MessageFormat;
+
+/**
+ * A factory object for creating the native platform on a Linux system with an
+ * electrophoretic display, also called an e-paper display, found on e-readers
+ * such as the Amazon Kindle and Rakuten Kobo.
+ */
+class EPDPlatformFactory extends NativePlatformFactory {
+
+    /**
+     * The major version number of this platform factory.
+     */
+    private static final int MAJOR_VERSION = 1;
+
+    /**
+     * The minor version number of this platform factory.
+     */
+    private static final int MINOR_VERSION = 0;
+
+    /**
+     * The file that contains the name of the frame buffer device when CONFIG_FB
+     * is defined during kernel compilation.
+     */
+    private static final String FB_FILE = "/proc/fb";
+
+    /**
+     * The name of the Mobile Extreme Convergence Electrophoretic Display
+     * Controller Frame Buffer device.
+     */
+    private static final String FB_NAME = "mxc_epdc_fb";
+
+    private final PlatformLogger logger = Logging.getJavaFXLogger();
+
+    /**
+     * Creates a new factory object for the Monocle EPD Platform.
+     */
+    EPDPlatformFactory() {
+    }
+
+    @Override
+    protected boolean matches() {
+        String fbinfo = AccessController.doPrivileged((PrivilegedAction<String>) () -> {
+            String line = null;
+            try (var reader = new BufferedReader(new FileReader(FB_FILE))) {
+                line = reader.readLine();
+            } catch (IOException e) {
+                logger.severe("Failed reading " + FB_FILE, e);
+            }
+            return line;
+        });
+        return fbinfo != null && fbinfo.contains(FB_NAME);
+    }
+
+    @Override
+    protected NativePlatform createNativePlatform() {
+        return new EPDPlatform();
+    }
+
+    @Override
+    protected int getMajorVersion() {
+        return MAJOR_VERSION;
+    }
+
+    @Override
+    protected int getMinorVersion() {
+        return MINOR_VERSION;
+    }
+
+    @Override
+    public String toString() {
+        return MessageFormat.format("{0}[majorVersion={1} minorVersion={2} matches=\"{3} in {4}\"]",
+                getClass().getName(), getMajorVersion(), getMinorVersion(), FB_NAME, FB_FILE);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDScreen.java	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,255 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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.sun.glass.ui.monocle;
+
+import com.sun.glass.ui.Pixels;
+import com.sun.javafx.logging.PlatformLogger;
+import com.sun.javafx.util.Logging;
+import java.io.IOException;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.text.MessageFormat;
+
+/**
+ * A native screen for an electrophoretic display, also called an e-paper
+ * display. This class uploads pixels directly into the Linux frame buffer if it
+ * is configured with a color depth of 32 bits per pixel. Otherwise, this class
+ * uploads pixels into a 32-bit off-screen composition buffer and converts the
+ * pixels to the correct format when writing them to the Linux frame buffer.
+ */
+class EPDScreen implements NativeScreen {
+
+    /**
+     * The system property for setting the frame buffer device path.
+     */
+    private static final String FB_PATH_KEY = "monocle.screen.fb";
+
+    /**
+     * The default value for the frame buffer device path.
+     */
+    private static final String FB_PATH_DEFAULT = "/dev/fb0";
+
+    /**
+     * The density of this screen in pixels per inch. For now, the value is
+     * hard-coded to the density of a 6-inch display panel with 800 × 600 px at
+     * 167 ppi.
+     */
+    private static final int DPI = 167;
+
+    /**
+     * The ratio of physical pixels to logical pixels on this screen. For now,
+     * the value is hard-coded to a ratio of 1.0.
+     */
+    private static final float SCALE = 1.0f;
+
+    private final PlatformLogger logger = Logging.getJavaFXLogger();
+
+    private final String fbPath;
+    private final EPDFrameBuffer fbDevice;
+    private final ByteBuffer fbMapping;
+    private final FileChannel fbChannel;
+    private final Framebuffer pixels;
+    private final int width;
+    private final int height;
+    private final int bitDepth;
+
+    private boolean isShutdown;
+
+    /**
+     * Creates a native screen for the electrophoretic display.
+     *
+     * @throws IllegalStateException if an error occurs opening the frame buffer
+     */
+    EPDScreen() {
+        fbPath = AccessController.doPrivileged((PrivilegedAction<String>) ()
+                -> System.getProperty(FB_PATH_KEY, FB_PATH_DEFAULT));
+        try {
+            fbDevice = new EPDFrameBuffer(fbPath);
+            fbDevice.init();
+
+            width = fbDevice.getWidth();
+            height = fbDevice.getHeight();
+            bitDepth = fbDevice.getBitDepth();
+
+            /*
+             * If the Linux frame buffer is configured for 32-bit color, compose
+             * the pixels directly into it. Otherwise, compose the pixels into
+             * an off-screen buffer and write them to the frame buffer when
+             * swapping buffers.
+             *
+             * With an LCD display, there must be space for two full screens to
+             * be able to write directly into the frame buffer, displaying one
+             * while updating the other. The Snapshot update mode of an e-paper
+             * display, though, allows us to reuse the same frame buffer region
+             * immediately after sending an update.
+             */
+            if (bitDepth == Integer.SIZE) {
+                fbMapping = fbDevice.getMappedBuffer();
+                fbChannel = null;
+            } else {
+                Path path = FileSystems.getDefault().getPath(fbPath);
+                fbChannel = FileChannel.open(path, StandardOpenOption.WRITE);
+                fbMapping = null;
+            }
+        } catch (IOException e) {
+            String msg = MessageFormat.format("Failed opening frame buffer: {0}", fbPath);
+            logger.severe(msg, e);
+            throw new IllegalStateException(msg, e);
+        }
+
+        /*
+         * Note that pixels.clearBufferContents() throws a NullPointerException
+         * if the last parameter of its constructor ("clear") is false.
+         */
+        ByteBuffer buffer = fbMapping != null ? fbMapping : fbDevice.getOffscreenBuffer();
+        buffer.order(ByteOrder.nativeOrder());
+        pixels = new FramebufferY8(buffer, width, height, bitDepth, true);
+        clearScreen();
+    }
+
+    /**
+     * Closes the Linux frame buffer device and related resources. Called only
+     * from the {@link #shutdown} method, which is called only once.
+     */
+    private void close() {
+        try {
+            if (fbChannel != null) {
+                fbChannel.close();
+            }
+        } catch (IOException e) {
+            logger.severe("Failed closing frame buffer channel", e);
+        } finally {
+            if (fbMapping != null) {
+                fbDevice.releaseMappedBuffer(fbMapping);
+            }
+            fbDevice.close();
+        }
+    }
+
+    /**
+     * Writes the content of the off-screen buffer to the Linux frame buffer, if
+     * necessary. If the frame buffer is mapped, the content to display is
+     * already there, and this method does nothing.
+     */
+    private void writeBuffer() {
+        if (fbChannel != null) {
+            try {
+                fbChannel.position(fbDevice.getByteOffset());
+                pixels.write(fbChannel);
+            } catch (IOException e) {
+                logger.severe("Failed writing to frame buffer channel", e);
+            }
+        }
+    }
+
+    /**
+     * Clears the screen.
+     */
+    private void clearScreen() {
+        pixels.clearBufferContents();
+        writeBuffer();
+        fbDevice.clear();
+    }
+
+    @Override
+    public int getDepth() {
+        return bitDepth;
+    }
+
+    @Override
+    public int getNativeFormat() {
+        /*
+         * The native pixel format must be one of either
+         * Pixels.Format.BYTE_BGRA_PRE when the system byte order is
+         * ByteOrder.LITTLE_ENDIAN, or Pixels.Format.BYTE_ARGB when the system
+         * byte order is ByteOrder.BIG_ENDIAN. The ARMv7-A architecture is
+         * little endian by default.
+         */
+        return Pixels.Format.BYTE_BGRA_PRE;
+    }
+
+    @Override
+    public int getWidth() {
+        return width;
+    }
+
+    @Override
+    public int getHeight() {
+        return height;
+    }
+
+    @Override
+    public int getDPI() {
+        return DPI;
+    }
+
+    @Override
+    public long getNativeHandle() {
+        return fbDevice.getNativeHandle();
+    }
+
+    @Override
+    public synchronized void shutdown() {
+        close();
+        isShutdown = true;
+    }
+
+    @Override
+    public synchronized void uploadPixels(Buffer b, int x, int y, int width, int height, float alpha) {
+        pixels.composePixels(b, x, y, width, height, alpha);
+    }
+
+    @Override
+    public synchronized void swapBuffers() {
+        if (!isShutdown && pixels.hasReceivedData()) {
+            writeBuffer();
+            fbDevice.sync();
+            pixels.reset();
+        }
+    }
+
+    @Override
+    public synchronized ByteBuffer getScreenCapture() {
+        return pixels.getBuffer().asReadOnlyBuffer();
+    }
+
+    @Override
+    public float getScale() {
+        return SCALE;
+    }
+
+    @Override
+    public String toString() {
+        return MessageFormat.format("{0}[width={1} height={2} depth={3} DPI={4} scale={5,number,0.0#}]",
+                getClass().getName(), getWidth(), getHeight(), getDepth(), getDPI(), getScale());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDSettings.java	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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.sun.glass.ui.monocle;
+
+import com.sun.javafx.logging.PlatformLogger;
+import com.sun.javafx.logging.PlatformLogger.Level;
+import com.sun.javafx.util.Logging;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.HashMap;
+
+/**
+ * Provides the values of the EPD system properties.
+ */
+class EPDSettings {
+
+    private static final String BITS_PER_PIXEL = "monocle.epd.bitsPerPixel";
+    private static final String ROTATE = "monocle.epd.rotate";
+    private static final String Y8_INVERTED = "monocle.epd.y8inverted";
+    private static final String NO_WAIT = "monocle.epd.noWait";
+    private static final String WAVEFORM_MODE = "monocle.epd.waveformMode";
+    private static final String FLAG_ENABLE_INVERSION = "monocle.epd.enableInversion";
+    private static final String FLAG_FORCE_MONOCHROME = "monocle.epd.forceMonochrome";
+    private static final String FLAG_USE_DITHERING_Y1 = "monocle.epd.useDitheringY1";
+    private static final String FLAG_USE_DITHERING_Y4 = "monocle.epd.useDitheringY4";
+
+    private static final String[] EPD_PROPERTIES = {
+        BITS_PER_PIXEL,
+        ROTATE,
+        Y8_INVERTED,
+        NO_WAIT,
+        WAVEFORM_MODE,
+        FLAG_ENABLE_INVERSION,
+        FLAG_FORCE_MONOCHROME,
+        FLAG_USE_DITHERING_Y1,
+        FLAG_USE_DITHERING_Y4
+    };
+
+    private static final int BITS_PER_PIXEL_DEFAULT = Integer.SIZE;
+    private static final int ROTATE_DEFAULT = EPDSystem.FB_ROTATE_UR;
+    private static final int WAVEFORM_MODE_DEFAULT = EPDSystem.WAVEFORM_MODE_AUTO;
+
+    private static final int[] BITS_PER_PIXEL_PERMITTED = {
+        Byte.SIZE,
+        Short.SIZE,
+        Integer.SIZE
+    };
+
+    private static final int[] ROTATIONS_PERMITTED = {
+        EPDSystem.FB_ROTATE_UR,
+        EPDSystem.FB_ROTATE_CW,
+        EPDSystem.FB_ROTATE_UD,
+        EPDSystem.FB_ROTATE_CCW
+    };
+
+    private static final int[] WAVEFORM_MODES_PERMITTED = {
+        EPDSystem.WAVEFORM_MODE_DU,
+        EPDSystem.WAVEFORM_MODE_GC16,
+        EPDSystem.WAVEFORM_MODE_GC4,
+        EPDSystem.WAVEFORM_MODE_A2,
+        EPDSystem.WAVEFORM_MODE_AUTO
+    };
+
+    /**
+     * Obtains a new instance of this class with the current values of the EPD
+     * system properties.
+     *
+     * @return a new {@code EPDSettings} instance
+     */
+    static EPDSettings newInstance() {
+        return AccessController.doPrivileged(
+                (PrivilegedAction<EPDSettings>) () -> new EPDSettings());
+    }
+
+    private final PlatformLogger logger = Logging.getJavaFXLogger();
+
+    private final boolean y8inverted;
+    private final boolean flagEnableInversion;
+    private final boolean flagForceMonochrome;
+    private final boolean flagUseDitheringY1;
+    private final boolean flagUseDitheringY4;
+
+    final int bitsPerPixel;
+    final int rotate;
+    final boolean noWait;
+    final int waveformMode;
+    final int grayscale;
+    final int flags;
+
+    /**
+     * Creates a new EPDSettings, capturing the current values of the EPD system
+     * properties.
+     */
+    private EPDSettings() {
+        if (logger.isLoggable(Level.FINE)) {
+            var map = new HashMap();
+            for (String key : EPD_PROPERTIES) {
+                String value = System.getProperty(key);
+                if (value != null) {
+                    map.put(key, value);
+                }
+            }
+            logger.fine("EPD system properties: {0}", map);
+        }
+
+        bitsPerPixel = getInteger(BITS_PER_PIXEL, BITS_PER_PIXEL_DEFAULT, BITS_PER_PIXEL_PERMITTED);
+        rotate = getInteger(ROTATE, ROTATE_DEFAULT, ROTATIONS_PERMITTED);
+        noWait = Boolean.getBoolean(NO_WAIT);
+        waveformMode = getInteger(WAVEFORM_MODE, WAVEFORM_MODE_DEFAULT, WAVEFORM_MODES_PERMITTED);
+
+        y8inverted = Boolean.getBoolean(Y8_INVERTED);
+        if (bitsPerPixel == Byte.SIZE) {
+            if (y8inverted) {
+                grayscale = EPDSystem.GRAYSCALE_8BIT_INVERTED;
+            } else {
+                grayscale = EPDSystem.GRAYSCALE_8BIT;
+            }
+        } else {
+            grayscale = 0;
+        }
+
+        flagEnableInversion = Boolean.getBoolean(FLAG_ENABLE_INVERSION);
+        flagForceMonochrome = Boolean.getBoolean(FLAG_FORCE_MONOCHROME);
+        flagUseDitheringY1 = Boolean.getBoolean(FLAG_USE_DITHERING_Y1);
+        flagUseDitheringY4 = Boolean.getBoolean(FLAG_USE_DITHERING_Y4);
+        flags = (flagEnableInversion ? EPDSystem.EPDC_FLAG_ENABLE_INVERSION : 0)
+                | (flagForceMonochrome ? EPDSystem.EPDC_FLAG_FORCE_MONOCHROME : 0)
+                | (flagUseDitheringY1 ? EPDSystem.EPDC_FLAG_USE_DITHERING_Y1 : 0)
+                | (flagUseDitheringY4 ? EPDSystem.EPDC_FLAG_USE_DITHERING_Y4 : 0);
+    }
+
+    /**
+     * Gets an integer system property.
+     *
+     * @param key the property name
+     * @param def the default value
+     * @param list a list of the permitted values for the property
+     * @return the value provided for the property if it is equal to one of the
+     * permitted values; otherwise, the default value
+     */
+    private int getInteger(String key, int def, int... list) {
+        int value = Integer.getInteger(key, def);
+        boolean found = false;
+        for (int i = 0; i < list.length && !found; i++) {
+            found = value == list[i];
+        }
+        if (!found) {
+            logger.severe("Value of {0}={1} not in {2}; using default ({3})",
+                    key, value, Arrays.toString(list), def);
+            value = def;
+        }
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return MessageFormat.format("{0}[bitsPerPixel={1} rotate={2} "
+                + "noWait={3} waveformMode={4} grayscale={5} flags=0x{6}]",
+                getClass().getName(), bitsPerPixel, rotate,
+                noWait, waveformMode, grayscale, Integer.toHexString(flags));
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/EPDSystem.java	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,749 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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.sun.glass.ui.monocle;
+
+import com.sun.glass.utils.NativeLibLoader;
+import java.nio.ByteOrder;
+import java.nio.IntBuffer;
+import java.security.Permission;
+import java.text.MessageFormat;
+
+/**
+ * A Java-language interface to the device API of the Electrophoretic Display
+ * Controller (EPDC) frame buffer driver. {@code EPDSystem} is a singleton. Its
+ * instance is obtained by calling the {@link EPDSystem#getEPDSystem} method.
+ * This class also extends {@link LinuxSystem.FbVarScreenInfo} to provide all of
+ * the fields in {@code fb_var_screeninfo}, defined in <i>linux/fb.h</i>.
+ */
+class EPDSystem {
+
+    /**
+     * The value for {@link FbVarScreenInfo#setActivate} to ensure that the EPDC
+     * driver receives the initialization request.
+     */
+    static final int FB_ACTIVATE_FORCE = 128;
+
+    /**
+     * The value for {@link FbVarScreenInfo#setRotate} to set the frame buffer
+     * rotation to un-rotated (upright landscape mode).
+     */
+    static final int FB_ROTATE_UR = 0;
+
+    /**
+     * The value for {@link FbVarScreenInfo#setRotate} to set the frame buffer
+     * rotation to 90-degrees clockwise (upside-down portrait mode).
+     */
+    static final int FB_ROTATE_CW = 1;
+
+    /**
+     * The value for {@link FbVarScreenInfo#setRotate} to set the frame buffer
+     * rotation to 180-degrees upside-down (upside-down landscape mode).
+     */
+    static final int FB_ROTATE_UD = 2;
+
+    /**
+     * The value for {@link FbVarScreenInfo#setRotate} to set the frame buffer
+     * rotation to 90-degrees counter-clockwise (upright portrait mode).
+     */
+    static final int FB_ROTATE_CCW = 3;
+
+    /**
+     * The value for {@link FbVarScreenInfo#setGrayscale} to set the frame
+     * buffer to an 8-bit grayscale pixel format.
+     */
+    static final int GRAYSCALE_8BIT = 0x1;
+
+    /**
+     * The value for {@link FbVarScreenInfo#setGrayscale} to set the frame
+     * buffer to an inverted 8-bit grayscale pixel format.
+     */
+    static final int GRAYSCALE_8BIT_INVERTED = 0x2;
+
+    /**
+     * Region update mode, in which updates to the display must be submitted
+     * with an IOCTL call to {@link #MXCFB_SEND_UPDATE}.
+     */
+    static final int AUTO_UPDATE_MODE_REGION_MODE = 0;
+
+    /**
+     * Automatic mode, in which updates are generated automatically by the
+     * driver when it detects that pages in a frame buffer memory region have
+     * been modified.
+     */
+    static final int AUTO_UPDATE_MODE_AUTOMATIC_MODE = 1;
+
+    /**
+     * Snapshot update scheme, which processes the contents of the frame buffer
+     * immediately and stores the update in a memory buffer internal to the
+     * driver. When the IOCTL call to {@link #MXCFB_SEND_UPDATE} returns, the
+     * frame buffer region is free and can be modified without affecting the
+     * update.
+     */
+    static final int UPDATE_SCHEME_SNAPSHOT = 0;
+
+    /**
+     * Queue update scheme, which uses a work queue to handle the processing of
+     * updates asynchronously. When updates are submitted with an IOCTL call to
+     * {@link #MXCFB_SEND_UPDATE}, they are added to the queue and processed in
+     * order as the EPDC hardware resources become available. The frame buffer
+     * contents processed and displayed, therefore, may not reflect what was
+     * present in the frame buffer when the update was submitted.
+     */
+    static final int UPDATE_SCHEME_QUEUE = 1;
+
+    /**
+     * Queue and Merge update scheme, which adds a merging step to the Queue
+     * update scheme. Before an update is added to the work queue, it is
+     * compared with other pending updates. If a pending update matches the mode
+     * and flags of the current update and also overlaps the update region, it
+     * will be merged with the current update. After all such merges, the final
+     * merged update is submitted to the queue.
+     */
+    static final int UPDATE_SCHEME_QUEUE_AND_MERGE = 2;
+
+    /**
+     * Partial update mode, which applies the waveform to only the pixels that
+     * change in a given region.
+     */
+    static final int UPDATE_MODE_PARTIAL = 0x0;
+
+    /**
+     * Full update mode, which applies the waveform to all pixels in a given
+     * region.
+     */
+    static final int UPDATE_MODE_FULL = 0x1;
+
+    /**
+     * Auto waveform mode, which requests the driver to select the actual
+     * waveform mode automatically based on the contents of the updated region.
+     */
+    static final int WAVEFORM_MODE_AUTO = 257;
+
+    /**
+     * The temperature value that requests the driver to use the ambient
+     * temperature of the device.
+     */
+    static final int TEMP_USE_AMBIENT = 0x1000;
+
+    /**
+     * An update flag to enable inversion of all pixels in the updated region.
+     */
+    static final int EPDC_FLAG_ENABLE_INVERSION = 0x01;
+
+    /**
+     * An update flag to enable black-and-white posterization of all pixels in
+     * the updated region.
+     */
+    static final int EPDC_FLAG_FORCE_MONOCHROME = 0x02;
+
+    /**
+     * An update flag to enable dithering of an 8-bit grayscale frame buffer to
+     * 1-bit black and white, if supported by the driver or hardware.
+     */
+    static final int EPDC_FLAG_USE_DITHERING_Y1 = 0x2000;
+
+    /**
+     * An update flag to enable dithering of an 8-bit grayscale frame buffer to
+     * 4-bit grayscale, if supported by the driver or hardware.
+     */
+    static final int EPDC_FLAG_USE_DITHERING_Y4 = 0x4000;
+
+    /**
+     * The power-down delay value to disable the powering down of the EPDC and
+     * display power supplies.
+     */
+    static final int FB_POWERDOWN_DISABLE = -1;
+
+    /**
+     * Initialization waveform (0x0...0xF → 0xF in ~4000 ms). Clears the screen
+     * to all white.
+     * <p>
+     * "A first exemplary drive scheme provides waveforms that may be used to
+     * change the display state of a pixel from any initial display state to a
+     * new display state of white. The first drive scheme may be referred to as
+     * an initialization or 'INIT' drive scheme." [<cite>United States Patent
+     * 9,280,955</cite>]</p>
+     */
+    static final int WAVEFORM_MODE_INIT = 0;
+
+    /**
+     * Direct update waveform (0x0...0xF → 0x0 or 0xF in ~260 ms). Changes gray
+     * pixels to black or white.
+     * <p>
+     * "A second exemplary drive scheme provides waveforms that may be used to
+     * change the display state of a pixel from any initial display state to a
+     * new display state of either white or black. The second drive scheme may
+     * be referred to as a 'DU' drive scheme." [<cite>United States Patent
+     * 9,280,955</cite>]</p>
+     */
+    static final int WAVEFORM_MODE_DU = 1;
+
+    /**
+     * Gray 4-level waveform (0x0...0xF → 0x0, 0x5, 0xA, or 0xF in ~500 ms).
+     * Supports 2-bit grayscale images and text with lower quality.
+     * <p>
+     * "A third exemplary drive scheme provides waveforms that may be used to
+     * change the display state of a pixel from any initial display state to a
+     * new display state. The initial state may be any four-bit (16 gray states)
+     * value. The new display state may be any two-bit (4 gray states) value.
+     * The third drive scheme may be referred to as a 'GC4' drive scheme."
+     * [<cite>United States Patent 9,280,955</cite>]</p>
+     */
+    static final int WAVEFORM_MODE_GC4 = 3;
+
+    /**
+     * Gray 16-level waveform (0x0...0xF → 0x0...0xF in ~760 ms). Supports 4-bit
+     * grayscale images and text with high quality.
+     * <p>
+     * "A fourth exemplary drive scheme provides waveforms that may be used to
+     * change the display state of a pixel from any initial display state to a
+     * new display state. The initial state may be any four-bit (16 gray states)
+     * value. The new display state may be any four-bit (16 gray states) value.
+     * The fourth drive scheme may be referred to as a 'GC16' drive scheme."
+     * [<cite>United States Patent 9,280,955</cite>]</p>
+     */
+    static final int WAVEFORM_MODE_GC16 = 2;
+
+    /**
+     * Animation waveform (0x0 or 0xF → 0x0 or 0xF in ~120 ms). Provides a fast
+     * 1-bit black-and-white animation mode of up to eight frames per second.
+     * <p>
+     * "A fifth exemplary drive scheme provides waveforms that may be used to
+     * change the display state of a pixel from an initial display state to a
+     * new display state. The initial state must be white or black. The new
+     * display state may be black or white. The fifth drive scheme may be
+     * referred to as an 'A2' drive scheme. An advantage of A2 waveforms is that
+     * they have generally short waveform periods, providing rapid display
+     * updates. A disadvantage of A2 waveforms is that there use may result in
+     * ghosting artifacts." [<cite>United States Patent 9,280,955</cite>]</p>
+     */
+    static final int WAVEFORM_MODE_A2 = 4;
+
+    private static final Permission PERMISSION = new RuntimePermission("loadLibrary.*");
+    private static final EPDSystem INSTANCE = new EPDSystem();
+
+    /**
+     * Checks for permission to load native libraries if running under a
+     * security manager.
+     */
+    private static void checkPermissions() {
+        SecurityManager security = System.getSecurityManager();
+        if (security != null) {
+            security.checkPermission(PERMISSION);
+        }
+    }
+
+    /**
+     * Obtains the single instance of {@code EPDSystem}. Calling this method
+     * requires the "loadLibrary.*" {@code RuntimePermission}. The
+     * {@link #loadLibrary} method must be called on the EPDSystem instance
+     * before any system calls can be made using it.
+     *
+     * @return the {@code EPDSystem} instance
+     */
+    static EPDSystem getEPDSystem() {
+        checkPermissions();
+        return INSTANCE;
+    }
+
+    /**
+     * The IOCTL request code to define a mapping for common waveform modes.
+     */
+    final int MXCFB_SET_WAVEFORM_MODES;
+
+    /**
+     * The IOCTL request code to set the temperature used by the EPDC driver in
+     * subsequent panel updates.
+     */
+    final int MXCFB_SET_TEMPERATURE;
+
+    /**
+     * The IOCTL request code to select between automatic and region update
+     * mode.
+     */
+    final int MXCFB_SET_AUTO_UPDATE_MODE;
+
+    /**
+     * The IOCTL request code to update a region of the frame buffer to the
+     * display.
+     */
+    final int MXCFB_SEND_UPDATE;
+
+    /**
+     * The IOCTL request code to block and wait for a previous update to
+     * complete.
+     */
+    final int MXCFB_WAIT_FOR_UPDATE_COMPLETE;
+
+    /**
+     * The IOCTL request code to set the delay between the completion of all
+     * updates in the driver and when the driver should power down the EPDC and
+     * display power supplies.
+     */
+    final int MXCFB_SET_PWRDOWN_DELAY;
+
+    /**
+     * The IOCTL request code to get the current power-down delay value from the
+     * driver.
+     */
+    final int MXCFB_GET_PWRDOWN_DELAY;
+
+    /**
+     * The IOCTL request code to select a scheme for the flow of updates within
+     * the driver.
+     */
+    final int MXCFB_SET_UPDATE_SCHEME;
+
+    private final LinuxSystem system;
+
+    /**
+     * Creates the single instance of {@code EPDSystem}.
+     */
+    private EPDSystem() {
+        system = LinuxSystem.getLinuxSystem();
+
+        MXCFB_SET_WAVEFORM_MODES = system.IOW('F', 0x2B, MxcfbWaveformModes.BYTES);
+        MXCFB_SET_TEMPERATURE = system.IOW('F', 0x2C, Integer.BYTES);
+        MXCFB_SET_AUTO_UPDATE_MODE = system.IOW('F', 0x2D, Integer.BYTES);
+        MXCFB_SEND_UPDATE = system.IOW('F', 0x2E, MxcfbUpdateData.BYTES);
+        MXCFB_WAIT_FOR_UPDATE_COMPLETE = system.IOW('F', 0x2F, Integer.BYTES);
+        MXCFB_SET_PWRDOWN_DELAY = system.IOW('F', 0x30, Integer.BYTES);
+        MXCFB_GET_PWRDOWN_DELAY = system.IOR('F', 0x31, IntStructure.BYTES);
+        MXCFB_SET_UPDATE_SCHEME = system.IOW('F', 0x32, Integer.BYTES);
+    }
+
+    /**
+     * Loads the native libraries required to make system calls using this
+     * {@code EPDSystem} instance. This method must be called before any other
+     * instance methods of {@code EPDSystem}. If this method is called multiple
+     * times, it has no effect after the first call.
+     */
+    void loadLibrary() {
+        NativeLibLoader.loadLibrary("glass_monocle_epd");
+    }
+
+    /**
+     * Passes an integer parameter by value to the device driver through the
+     * IOCTL interface. ({@link LinuxSystem#ioctl}, instead, takes a pointer as
+     * its third parameter, passing its data by reference.)
+     *
+     * @param fd an open file descriptor
+     * @param request a device-dependent request code
+     * @param value the integer value
+     * @return 0 if successful; otherwise -1 with {@code errno} set
+     * appropriately
+     */
+    native int ioctl(long fd, int request, int value);
+
+    /**
+     * A structure for passing an integer by value in an IOCTL call.
+     */
+    static class IntStructure extends C.Structure {
+
+        private static final int VALUE = 0;
+
+        private static final int NUM_INTS = 1;
+        private static final int BYTES = NUM_INTS * Integer.BYTES;
+
+        private final IntBuffer data;
+
+        IntStructure() {
+            b.order(ByteOrder.nativeOrder());
+            data = b.asIntBuffer();
+        }
+
+        @Override
+        int sizeof() {
+            return BYTES;
+        }
+
+        int getInteger(long p) {
+            return data.get(VALUE);
+        }
+
+        void setInteger(long p, int value) {
+            data.put(VALUE, value);
+        }
+    }
+
+    /**
+     * Wraps the C structure {@code mxcfb_waveform_modes}, defined in
+     * <i>mxcfb.h</i>.
+     */
+    static class MxcfbWaveformModes extends C.Structure {
+
+        private static final int MODE_INIT = 0;
+        private static final int MODE_DU = 1;
+        private static final int MODE_GC4 = 2;
+        private static final int MODE_GC8 = 3;
+        private static final int MODE_GC16 = 4;
+        private static final int MODE_GC32 = 5;
+
+        private static final int NUM_INTS = 6;
+        private static final int BYTES = NUM_INTS * Integer.BYTES;
+
+        private final IntBuffer data;
+
+        MxcfbWaveformModes() {
+            b.order(ByteOrder.nativeOrder());
+            data = b.asIntBuffer();
+        }
+
+        @Override
+        int sizeof() {
+            return BYTES;
+        }
+
+        int getModeInit(long p) {
+            return data.get(MODE_INIT);
+        }
+
+        int getModeDu(long p) {
+            return data.get(MODE_DU);
+        }
+
+        int getModeGc4(long p) {
+            return data.get(MODE_GC4);
+        }
+
+        int getModeGc8(long p) {
+            return data.get(MODE_GC8);
+        }
+
+        int getModeGc16(long p) {
+            return data.get(MODE_GC16);
+        }
+
+        int getModeGc32(long p) {
+            return data.get(MODE_GC32);
+        }
+
+        void setModes(long p, int init, int du, int gc4, int gc8, int gc16, int gc32) {
+            data.put(MODE_INIT, init);
+            data.put(MODE_DU, du);
+            data.put(MODE_GC4, gc4);
+            data.put(MODE_GC8, gc8);
+            data.put(MODE_GC16, gc16);
+            data.put(MODE_GC32, gc32);
+        }
+
+        @Override
+        public String toString() {
+            return MessageFormat.format(
+                    "{0}[mode_init={1} mode_du={2} mode_gc4={3} mode_gc8={4} mode_gc16={5} mode_gc32={6}]",
+                    getClass().getName(), getModeInit(p), getModeDu(p), getModeGc4(p),
+                    getModeGc8(p), getModeGc16(p), getModeGc32(p));
+        }
+    }
+
+    /**
+     * Wraps the C structure {@code mxcfb_update_data}, defined in
+     * <i>mxcfb.h</i>.
+     */
+    static class MxcfbUpdateData extends C.Structure {
+
+        private static final int UPDATE_REGION_TOP = 0;
+        private static final int UPDATE_REGION_LEFT = 1;
+        private static final int UPDATE_REGION_WIDTH = 2;
+        private static final int UPDATE_REGION_HEIGHT = 3;
+
+        private static final int WAVEFORM_MODE = 4;
+        private static final int UPDATE_MODE = 5;
+        private static final int UPDATE_MARKER = 6;
+        private static final int TEMP = 7;
+        private static final int FLAGS = 8;
+
+        private static final int ALT_BUFFER_DATA_VIRT_ADDR = 9;
+        private static final int ALT_BUFFER_DATA_PHYS_ADDR = 10;
+        private static final int ALT_BUFFER_DATA_WIDTH = 11;
+        private static final int ALT_BUFFER_DATA_HEIGHT = 12;
+
+        private static final int ALT_BUFFER_DATA_ALT_UPDATE_REGION_TOP = 13;
+        private static final int ALT_BUFFER_DATA_ALT_UPDATE_REGION_LEFT = 14;
+        private static final int ALT_BUFFER_DATA_ALT_UPDATE_REGION_WIDTH = 15;
+        private static final int ALT_BUFFER_DATA_ALT_UPDATE_REGION_HEIGHT = 16;
+
+        private static final int NUM_INTS = 17;
+        private static final int BYTES = NUM_INTS * Integer.BYTES;
+
+        private final IntBuffer data;
+
+        MxcfbUpdateData() {
+            b.order(ByteOrder.nativeOrder());
+            data = b.asIntBuffer();
+        }
+
+        @Override
+        int sizeof() {
+            return BYTES;
+        }
+
+        int getUpdateRegionTop(long p) {
+            return data.get(UPDATE_REGION_TOP);
+        }
+
+        int getUpdateRegionLeft(long p) {
+            return data.get(UPDATE_REGION_LEFT);
+        }
+
+        int getUpdateRegionWidth(long p) {
+            return data.get(UPDATE_REGION_WIDTH);
+        }
+
+        int getUpdateRegionHeight(long p) {
+            return data.get(UPDATE_REGION_HEIGHT);
+        }
+
+        int getWaveformMode(long p) {
+            return data.get(WAVEFORM_MODE);
+        }
+
+        int getUpdateMode(long p) {
+            return data.get(UPDATE_MODE);
+        }
+
+        int getUpdateMarker(long p) {
+            return data.get(UPDATE_MARKER);
+        }
+
+        int getTemp(long p) {
+            return data.get(TEMP);
+        }
+
+        int getFlags(long p) {
+            return data.get(FLAGS);
+        }
+
+        long getAltBufferDataVirtAddr(long p) {
+            return data.get(ALT_BUFFER_DATA_VIRT_ADDR);
+        }
+
+        long getAltBufferDataPhysAddr(long p) {
+            return data.get(ALT_BUFFER_DATA_PHYS_ADDR);
+        }
+
+        int getAltBufferDataWidth(long p) {
+            return data.get(ALT_BUFFER_DATA_WIDTH);
+        }
+
+        int getAltBufferDataHeight(long p) {
+            return data.get(ALT_BUFFER_DATA_HEIGHT);
+        }
+
+        int getAltBufferDataAltUpdateRegionTop(long p) {
+            return data.get(ALT_BUFFER_DATA_ALT_UPDATE_REGION_TOP);
+        }
+
+        int getAltBufferDataAltUpdateRegionLeft(long p) {
+            return data.get(ALT_BUFFER_DATA_ALT_UPDATE_REGION_LEFT);
+        }
+
+        int getAltBufferDataAltUpdateRegionWidth(long p) {
+            return data.get(ALT_BUFFER_DATA_ALT_UPDATE_REGION_WIDTH);
+        }
+
+        int getAltBufferDataAltUpdateRegionHeight(long p) {
+            return data.get(ALT_BUFFER_DATA_ALT_UPDATE_REGION_HEIGHT);
+        }
+
+        void setUpdateRegion(long p, int top, int left, int width, int height) {
+            data.put(UPDATE_REGION_TOP, top);
+            data.put(UPDATE_REGION_LEFT, left);
+            data.put(UPDATE_REGION_WIDTH, width);
+            data.put(UPDATE_REGION_HEIGHT, height);
+        }
+
+        void setWaveformMode(long p, int mode) {
+            data.put(WAVEFORM_MODE, mode);
+        }
+
+        void setUpdateMode(long p, int mode) {
+            data.put(UPDATE_MODE, mode);
+        }
+
+        void setUpdateMarker(long p, int marker) {
+            data.put(UPDATE_MARKER, marker);
+        }
+
+        void setTemp(long p, int temp) {
+            data.put(TEMP, temp);
+        }
+
+        void setFlags(long p, int flags) {
+            data.put(FLAGS, flags);
+        }
+
+        void setAltBufferData(long p, long virtAddr, long physAddr, int width, int height,
+                int altUpdateRegionTop, int altUpdateRegionLeft, int altUpdateRegionWidth, int altUpdateRegionHeight) {
+            data.put(ALT_BUFFER_DATA_VIRT_ADDR, (int) virtAddr);
+            data.put(ALT_BUFFER_DATA_PHYS_ADDR, (int) physAddr);
+            data.put(ALT_BUFFER_DATA_WIDTH, width);
+            data.put(ALT_BUFFER_DATA_HEIGHT, height);
+            data.put(ALT_BUFFER_DATA_ALT_UPDATE_REGION_TOP, altUpdateRegionTop);
+            data.put(ALT_BUFFER_DATA_ALT_UPDATE_REGION_LEFT, altUpdateRegionLeft);
+            data.put(ALT_BUFFER_DATA_ALT_UPDATE_REGION_WIDTH, altUpdateRegionWidth);
+            data.put(ALT_BUFFER_DATA_ALT_UPDATE_REGION_HEIGHT, altUpdateRegionHeight);
+        }
+
+        @Override
+        public String toString() {
+            return MessageFormat.format(
+                    "{0}[update_region.top={1} update_region.left={2} update_region.width={3} update_region.height={4}"
+                    + " waveform_mode={5} update_mode={6} update_marker={7} temp={8} flags=0x{9}"
+                    + " alt_buffer_data.virt_addr=0x{10} alt_buffer_data.phys_addr=0x{11}"
+                    + " alt_buffer_data.width={12} alt_buffer_data.height={13}"
+                    + " alt_buffer_data.alt_update_region.top={14} alt_buffer_data.alt_update_region.left={15}"
+                    + " alt_buffer_data.alt_update_region.width={16} alt_buffer_data.alt_update_region.height={17}]",
+                    getClass().getName(),
+                    Integer.toUnsignedLong(getUpdateRegionTop(p)),
+                    Integer.toUnsignedLong(getUpdateRegionLeft(p)),
+                    Integer.toUnsignedLong(getUpdateRegionWidth(p)),
+                    Integer.toUnsignedLong(getUpdateRegionHeight(p)),
+                    Integer.toUnsignedLong(getWaveformMode(p)),
+                    Integer.toUnsignedLong(getUpdateMode(p)),
+                    Integer.toUnsignedLong(getUpdateMarker(p)),
+                    getTemp(p),
+                    Integer.toHexString(getFlags(p)),
+                    Long.toHexString(getAltBufferDataVirtAddr(p)),
+                    Long.toHexString(getAltBufferDataPhysAddr(p)),
+                    Integer.toUnsignedLong(getAltBufferDataWidth(p)),
+                    Integer.toUnsignedLong(getAltBufferDataHeight(p)),
+                    Integer.toUnsignedLong(getAltBufferDataAltUpdateRegionTop(p)),
+                    Integer.toUnsignedLong(getAltBufferDataAltUpdateRegionLeft(p)),
+                    Integer.toUnsignedLong(getAltBufferDataAltUpdateRegionWidth(p)),
+                    Integer.toUnsignedLong(getAltBufferDataAltUpdateRegionHeight(p)));
+        }
+    }
+
+    /**
+     * Wraps the entire C structure {@code fb_var_screeninfo}, defined in
+     * <i>linux/fb.h</i>.
+     */
+    static class FbVarScreenInfo extends LinuxSystem.FbVarScreenInfo {
+
+        native int getGrayscale(long p);
+
+        native int getRedOffset(long p);
+
+        native int getRedLength(long p);
+
+        native int getRedMsbRight(long p);
+
+        native int getGreenOffset(long p);
+
+        native int getGreenLength(long p);
+
+        native int getGreenMsbRight(long p);
+
+        native int getBlueOffset(long p);
+
+        native int getBlueLength(long p);
+
+        native int getBlueMsbRight(long p);
+
+        native int getTranspOffset(long p);
+
+        native int getTranspLength(long p);
+
+        native int getTranspMsbRight(long p);
+
+        native int getNonstd(long p);
+
+        native int getActivate(long p);
+
+        native int getHeight(long p);
+
+        native int getWidth(long p);
+
+        native int getAccelFlags(long p);
+
+        native int getPixclock(long p);
+
+        native int getLeftMargin(long p);
+
+        native int getRightMargin(long p);
+
+        native int getUpperMargin(long p);
+
+        native int getLowerMargin(long p);
+
+        native int getHsyncLen(long p);
+
+        native int getVsyncLen(long p);
+
+        native int getSync(long p);
+
+        native int getVmode(long p);
+
+        native int getRotate(long p);
+
+        native void setGrayscale(long p, int grayscale);
+
+        native void setNonstd(long p, int nonstd);
+
+        native void setHeight(long p, int height);
+
+        native void setWidth(long p, int width);
+
+        native void setAccelFlags(long p, int accelFlags);
+
+        native void setPixclock(long p, int pixclock);
+
+        native void setLeftMargin(long p, int leftMargin);
+
+        native void setRightMargin(long p, int rightMargin);
+
+        native void setUpperMargin(long p, int upperMargin);
+
+        native void setLowerMargin(long p, int lowerMargin);
+
+        native void setHsyncLen(long p, int hsyncLen);
+
+        native void setVsyncLen(long p, int vsyncLen);
+
+        native void setSync(long p, int sync);
+
+        native void setVmode(long p, int vmode);
+
+        native void setRotate(long p, int rotate);
+    }
+
+    @Override
+    public String toString() {
+        return MessageFormat.format("{0}[MXCFB_SET_WAVEFORM_MODES=0x{1} MXCFB_SET_TEMPERATURE=0x{2} "
+                + "MXCFB_SET_AUTO_UPDATE_MODE=0x{3} MXCFB_SEND_UPDATE=0x{4} MXCFB_WAIT_FOR_UPDATE_COMPLETE=0x{5} "
+                + "MXCFB_SET_PWRDOWN_DELAY=0x{6} MXCFB_GET_PWRDOWN_DELAY=0x{7} MXCFB_SET_UPDATE_SCHEME=0x{8}]",
+                getClass().getName(),
+                Integer.toHexString(MXCFB_SET_WAVEFORM_MODES),
+                Integer.toHexString(MXCFB_SET_TEMPERATURE),
+                Integer.toHexString(MXCFB_SET_AUTO_UPDATE_MODE),
+                Integer.toHexString(MXCFB_SEND_UPDATE),
+                Integer.toHexString(MXCFB_WAIT_FOR_UPDATE_COMPLETE),
+                Integer.toHexString(MXCFB_SET_PWRDOWN_DELAY),
+                Integer.toHexString(MXCFB_GET_PWRDOWN_DELAY),
+                Integer.toHexString(MXCFB_SET_UPDATE_SCHEME)
+        );
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/FramebufferY8.java	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,259 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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.sun.glass.ui.monocle;
+
+import com.sun.javafx.logging.PlatformLogger;
+import com.sun.javafx.util.Logging;
+import java.io.IOException;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+import java.nio.channels.WritableByteChannel;
+import java.text.MessageFormat;
+
+/**
+ * Provides a buffer for composing JavaFX scenes. This class is given a 32-bit
+ * composition buffer that is either the Linux frame buffer itself or an
+ * off-screen byte buffer. It can write the contents of this buffer to a target
+ * channel, or copy them to a buffer, in one of three pixel formats: 32-bit
+ * ARGB32 color, 16-bit RGB565 color, or 8-bit Y8 grayscale.
+ */
+class FramebufferY8 extends Framebuffer {
+
+    /**
+     * The arithmetic right shift value to convert a bit depth to a byte depth.
+     */
+    private static final int BITS_TO_BYTES = 3;
+
+    private final PlatformLogger logger = Logging.getJavaFXLogger();
+    private final ByteBuffer bb;
+    private final int width;
+    private final int height;
+    private final int bitDepth;
+    private final int byteDepth;
+
+    private ByteBuffer lineByteBuffer;
+    private Buffer linePixelBuffer;
+
+    /**
+     * Creates a new {@code FramebufferY8} with the given 32-bit composition
+     * buffer and target color depth.
+     *
+     * @param bb the 32-bit composition buffer
+     * @param width the width of the composition buffer in pixels
+     * @param height the height of the composition buffer in pixels
+     * @param depth the color depth of the target channel or buffer in bits per
+     * pixel
+     * @param clear {@code true} to clear the composition buffer on the first
+     * upload of each frame unless that upload already overwrites the entire
+     * buffer; otherwise {@code false}
+     */
+    FramebufferY8(ByteBuffer bb, int width, int height, int depth, boolean clear) {
+        super(bb, width, height, depth, clear);
+        this.bb = bb;
+        this.width = width;
+        this.height = height;
+        this.bitDepth = depth;
+        this.byteDepth = depth >>> BITS_TO_BYTES;
+        if (byteDepth != Integer.BYTES && byteDepth != Short.BYTES && byteDepth != Byte.BYTES) {
+            String msg = MessageFormat.format("Unsupported color depth: {0} bpp", bitDepth);
+            logger.severe(msg);
+            throw new IllegalArgumentException(msg);
+        }
+    }
+
+    /**
+     * Copies the next 32-bit ARGB32 pixel to a byte buffer with 8-bit Y8
+     * pixels. Luma Y' can be calculated from gamma-corrected R'G'B' using the
+     * following coefficients. This method uses the coefficients from Rec. 709,
+     * which defines the same primaries and white point as the sRGB color space.
+     * <pre>{@code
+     * Simple average:  Y' = (R' + G' + B') / 3
+     * Rec. 601 (SDTV): Y' = 0.299  × R' + 0.587  × G' + 0.114  × B'
+     * Rec. 709 (HDTV): Y' = 0.2126 × R' + 0.7152 × G' + 0.0722 × B'
+     * Rec. 2100 (HDR): Y' = 0.2627 × R' + 0.6780 × G' + 0.0593 × B'
+     * }</pre>
+     *
+     * @implNote Java rounds toward zero when converting a {@code float} to an
+     * {@code int}, so this method adds 0.5 before the type conversion to round
+     * to the nearest integer.
+     *
+     * @param source the source integer buffer in ARGB32 format
+     * @param target the target byte buffer in Y8 format
+     */
+    private void copyNextPixel(IntBuffer source, ByteBuffer target) {
+        int pixel32 = source.get();
+        int r = (pixel32 >> 16) & 0xFF;
+        int g = (pixel32 >> 8) & 0xFF;
+        int b = pixel32 & 0xFF;
+        int y = (int) (0.2126f * r + 0.7152f * g + 0.0722f * b + 0.5f);
+        target.put((byte) y);
+    }
+
+    /**
+     * Copies the next 32-bit ARGB32 pixel to a short buffer with 16-bit RGB565
+     * pixels. This method truncates the low-order bits of each color component.
+     *
+     * @param source the source integer buffer in ARGB32 format
+     * @param target the target short buffer in RGB565 format
+     */
+    private void copyNextPixel(IntBuffer source, ShortBuffer target) {
+        int pixel32 = source.get();
+        int r = (pixel32 >> 8) & 0xF800;
+        int g = (pixel32 >> 5) & 0x07E0;
+        int b = (pixel32 >> 3) & 0x001F;
+        int pixel16 = r | g | b;
+        target.put((short) pixel16);
+    }
+
+    /**
+     * Writes the contents of the composition buffer to the output channel,
+     * converting the pixel format as necessary.
+     *
+     * @param out the output channel
+     * @throws IOException if an error occurs writing to the channel
+     * @throws IllegalArgumentException if the channel has an unsupported color
+     * depth
+     */
+    @Override
+    void write(WritableByteChannel out) throws IOException {
+        bb.clear();
+        switch (byteDepth) {
+            case Byte.BYTES: {
+                if (lineByteBuffer == null) {
+                    lineByteBuffer = ByteBuffer.allocate(width * Byte.BYTES);
+                    lineByteBuffer.order(ByteOrder.nativeOrder());
+                    linePixelBuffer = lineByteBuffer.duplicate();
+                }
+                IntBuffer srcPixels = bb.asIntBuffer();
+                ByteBuffer byteBuffer = (ByteBuffer) linePixelBuffer;
+                for (int y = 0; y < height; y++) {
+                    byteBuffer.clear();
+                    for (int x = 0; x < width; x++) {
+                        copyNextPixel(srcPixels, byteBuffer);
+                    }
+                    lineByteBuffer.clear();
+                    out.write(lineByteBuffer);
+                }
+                break;
+            }
+            case Short.BYTES: {
+                if (lineByteBuffer == null) {
+                    lineByteBuffer = ByteBuffer.allocate(width * Short.BYTES);
+                    lineByteBuffer.order(ByteOrder.nativeOrder());
+                    linePixelBuffer = lineByteBuffer.asShortBuffer();
+                }
+                IntBuffer srcPixels = bb.asIntBuffer();
+                ShortBuffer shortBuffer = (ShortBuffer) linePixelBuffer;
+                for (int y = 0; y < height; y++) {
+                    shortBuffer.clear();
+                    for (int x = 0; x < width; x++) {
+                        copyNextPixel(srcPixels, shortBuffer);
+                    }
+                    lineByteBuffer.clear();
+                    out.write(lineByteBuffer);
+                }
+                break;
+            }
+            case Integer.BYTES: {
+                out.write(bb);
+                break;
+            }
+            default:
+                String msg = MessageFormat.format("byteDepth={0}", byteDepth);
+                logger.severe(msg);
+                throw new IllegalStateException(msg);
+
+        }
+    }
+
+    /**
+     * Copies the contents of the composition buffer to the output buffer,
+     * converting the pixel format as necessary.
+     *
+     * @param out the output buffer
+     * @throws IllegalArgumentException if the buffer has an unsupported color
+     * depth
+     */
+    @Override
+    void copyToBuffer(ByteBuffer out) {
+        bb.clear();
+        switch (byteDepth) {
+            case Byte.BYTES: {
+                if (lineByteBuffer == null) {
+                    lineByteBuffer = ByteBuffer.allocate(width * Byte.BYTES);
+                    lineByteBuffer.order(ByteOrder.nativeOrder());
+                    linePixelBuffer = lineByteBuffer.duplicate();
+                }
+                IntBuffer srcPixels = bb.asIntBuffer();
+                ByteBuffer byteBuffer = (ByteBuffer) linePixelBuffer;
+                for (int y = 0; y < height; y++) {
+                    byteBuffer.clear();
+                    for (int x = 0; x < width; x++) {
+                        copyNextPixel(srcPixels, byteBuffer);
+                    }
+                    lineByteBuffer.clear();
+                    out.put(lineByteBuffer);
+                }
+                break;
+            }
+            case Short.BYTES: {
+                if (lineByteBuffer == null) {
+                    lineByteBuffer = ByteBuffer.allocate(width * Short.BYTES);
+                    lineByteBuffer.order(ByteOrder.nativeOrder());
+                    linePixelBuffer = lineByteBuffer.asShortBuffer();
+                }
+                IntBuffer srcPixels = bb.asIntBuffer();
+                ShortBuffer shortBuffer = (ShortBuffer) linePixelBuffer;
+                for (int y = 0; y < height; y++) {
+                    shortBuffer.clear();
+                    for (int x = 0; x < width; x++) {
+                        copyNextPixel(srcPixels, shortBuffer);
+                    }
+                    lineByteBuffer.clear();
+                    out.put(lineByteBuffer);
+                }
+                break;
+            }
+            case Integer.BYTES: {
+                out.put(bb);
+                break;
+            }
+            default:
+                String msg = MessageFormat.format("byteDepth={0}", byteDepth);
+                logger.severe(msg);
+                throw new IllegalStateException(msg);
+
+        }
+    }
+
+    @Override
+    public String toString() {
+        return MessageFormat.format("{0}[width={1} height={2} depth={3} bb={4}]",
+                getClass().getName(), width, height, bitDepth, bb);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/javafx.graphics/src/main/native-glass/monocle/epd/EPDSystem.c	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,274 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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.
+ */
+
+// Implementation of native methods in EPDSystem.java
+
+#include <sys/ioctl.h>  // For ioctl
+#include <sys/types.h>  // For uint
+#include <linux/fb.h>   // For fb_var_screeninfo
+
+#include "com_sun_glass_ui_monocle_EPDSystem.h"
+#include "com_sun_glass_ui_monocle_EPDSystem_FbVarScreenInfo.h"
+
+#include "Monocle.h"
+
+// EPDSystem
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_ioctl
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong fd, jint request, jint value) {
+    return ioctl((int) fd, (int) request, (__u32 *) & value);
+}
+
+// EPDSystem.FbVarScreenInfo
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getGrayscale
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->grayscale;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getRedOffset
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->red.offset;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getRedLength
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->red.length;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getRedMsbRight
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->red.msb_right;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getGreenOffset
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->green.offset;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getGreenLength
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->green.length;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getGreenMsbRight
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->green.msb_right;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getBlueOffset
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->blue.offset;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getBlueLength
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->blue.length;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getBlueMsbRight
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->blue.msb_right;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getTranspOffset
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->transp.offset;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getTranspLength
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->transp.length;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getTranspMsbRight
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->transp.msb_right;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getNonstd
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->nonstd;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getActivate
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->activate;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getHeight
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->height;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getWidth
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->width;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getAccelFlags
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->accel_flags;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getPixclock
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->pixclock;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getLeftMargin
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->left_margin;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getRightMargin
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->right_margin;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getUpperMargin
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->upper_margin;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getLowerMargin
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->lower_margin;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getHsyncLen
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->hsync_len;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getVsyncLen
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->vsync_len;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getSync
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->sync;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getVmode
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->vmode;
+}
+
+JNIEXPORT jint JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_getRotate
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p) {
+    return (jint) ((struct fb_var_screeninfo *) asPtr(p))->rotate;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setGrayscale
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint grayscale) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->grayscale = grayscale;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setNonstd
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint nonstd) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->nonstd = nonstd;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setHeight
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint height) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->height = height;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setWidth
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint width) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->width = width;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setAccelFlags
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint accelFlags) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->accel_flags = accelFlags;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setPixclock
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint pixclock) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->pixclock = pixclock;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setLeftMargin
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint leftMargin) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->left_margin = leftMargin;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setRightMargin
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint rightMargin) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->right_margin = rightMargin;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setUpperMargin
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint upperMargin) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->upper_margin = upperMargin;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setLowerMargin
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint lowerMargin) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->lower_margin = lowerMargin;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setHsyncLen
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint hsyncLen) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->hsync_len = hsyncLen;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setVsyncLen
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint vsyncLen) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->vsync_len = vsyncLen;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setSync
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint sync) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->sync = sync;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setVmode
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint vmode) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->vmode = vmode;
+}
+
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_monocle_EPDSystem_00024FbVarScreenInfo_setRotate
+(JNIEnv *UNUSED(env), jobject UNUSED(object), jlong p, jint rotate) {
+    struct fb_var_screeninfo *ptr = (struct fb_var_screeninfo *) asPtr(p);
+    ptr->rotate = rotate;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/javafx.graphics/src/shims/java/com/sun/glass/ui/monocle/EPDSettingsShim.java	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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.sun.glass.ui.monocle;
+
+/**
+ * Provides access to the {@link EPDSettings} class by making its
+ * package-private fields and methods public for test cases in
+ * {@link test.com.sun.glass.ui.monocle.EPDSettingsTest EPDSettingsTest}.
+ */
+public class EPDSettingsShim {
+
+    public final int bitsPerPixel;
+    public final int rotate;
+    public final int waveformMode;
+    public final boolean noWait;
+    public final int grayscale;
+    public final int flags;
+
+    /**
+     * Obtains a new instance of this class with the current values of the EPD
+     * system properties.
+     *
+     * @return a new {@code EPDSettingsShim} instance
+     */
+    public static EPDSettingsShim newInstance() {
+        return new EPDSettingsShim(EPDSettings.newInstance());
+    }
+
+    /**
+     * Sets the public fields of this object to the corresponding
+     * package-private fields of the {@code EPDSettings} instance.
+     *
+     * @param settings an instance of {@code EPDSettings}
+     */
+    private EPDSettingsShim(EPDSettings settings) {
+        bitsPerPixel = settings.bitsPerPixel;
+        rotate = settings.rotate;
+        waveformMode = settings.waveformMode;
+        noWait = settings.noWait;
+        grayscale = settings.grayscale;
+        flags = settings.flags;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/javafx.graphics/src/shims/java/com/sun/glass/ui/monocle/FramebufferY8Shim.java	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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.sun.glass.ui.monocle;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
+
+/**
+ * Provides access to the {@link FramebufferY8} class by making its
+ * package-private methods public for test cases in
+ * {@link test.com.sun.glass.ui.monocle.FramebufferY8Test FramebufferY8Test}.
+ */
+public class FramebufferY8Shim extends FramebufferY8 {
+
+    /**
+     * Creates a new {@code FramebufferY8Shim}.
+     *
+     * @param bb the 32-bit composition buffer
+     * @param width the width of the composition buffer in pixels
+     * @param height the height of the composition buffer in pixels
+     * @param depth the color depth of the target channel or buffer in bits per
+     * pixel
+     * @param clear {@code true} to clear the composition buffer on the first
+     * upload of each frame unless that upload already overwrites the entire
+     * buffer; otherwise {@code false}
+     */
+    public FramebufferY8Shim(ByteBuffer bb, int width, int height, int depth, boolean clear) {
+        super(bb, width, height, depth, clear);
+    }
+
+    @Override
+    public void write(WritableByteChannel out) throws IOException {
+        super.write(out);
+    }
+
+    @Override
+    public void copyToBuffer(ByteBuffer out) {
+        super.copyToBuffer(out);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/javafx.graphics/src/shims/java/com/sun/glass/ui/monocle/FramebufferY8SuperShim.java	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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.sun.glass.ui.monocle;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
+
+/**
+ * Provides access to the {@link Framebuffer} class by making its
+ * package-private methods public for test cases in
+ * {@link test.com.sun.glass.ui.monocle.FramebufferY8Test FramebufferY8Test}.
+ */
+public class FramebufferY8SuperShim extends Framebuffer {
+
+    /**
+     * Creates a new {@code FramebufferY8SuperShim}.
+     *
+     * @param bb the 32-bit composition buffer
+     * @param width the width of the composition buffer in pixels
+     * @param height the height of the composition buffer in pixels
+     * @param depth the color depth of the target channel or buffer in bits per
+     * pixel
+     * @param clear {@code true} to clear the composition buffer on the first
+     * upload of each frame unless that upload already overwrites the entire
+     * buffer; otherwise {@code false}
+     */
+    public FramebufferY8SuperShim(ByteBuffer bb, int width, int height, int depth, boolean clear) {
+        super(bb, width, height, depth, clear);
+    }
+
+    @Override
+    public void write(WritableByteChannel out) throws IOException {
+        super.write(out);
+    }
+
+    @Override
+    public void copyToBuffer(ByteBuffer out) {
+        super.copyToBuffer(out);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/system/src/test/java/test/com/sun/glass/ui/monocle/EPDSettingsTest.java	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,262 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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 test.com.sun.glass.ui.monocle;
+
+import com.sun.glass.ui.monocle.EPDSettingsShim;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Provides test cases for the {@code EPDSettings} class.
+ */
+public class EPDSettingsTest {
+
+    private static final String BITS_PER_PIXEL = "monocle.epd.bitsPerPixel";
+    private static final String ROTATE = "monocle.epd.rotate";
+    private static final String Y8_INVERTED = "monocle.epd.y8inverted";
+    private static final String NO_WAIT = "monocle.epd.noWait";
+    private static final String WAVEFORM_MODE = "monocle.epd.waveformMode";
+    private static final String FLAG_ENABLE_INVERSION = "monocle.epd.enableInversion";
+    private static final String FLAG_FORCE_MONOCHROME = "monocle.epd.forceMonochrome";
+    private static final String FLAG_USE_DITHERING_Y1 = "monocle.epd.useDitheringY1";
+    private static final String FLAG_USE_DITHERING_Y4 = "monocle.epd.useDitheringY4";
+
+    private static final String VERIFY_ERROR = "Verify the error log message for %s=%d.";
+
+    private EPDSettingsShim settings;
+
+    /**
+     * Removes all of the EPD system properties. This method runs before each of
+     * the test cases.
+     */
+    @Before
+    public void initialize() {
+        System.clearProperty(BITS_PER_PIXEL);
+        System.clearProperty(ROTATE);
+        System.clearProperty(Y8_INVERTED);
+        System.clearProperty(NO_WAIT);
+        System.clearProperty(WAVEFORM_MODE);
+        System.clearProperty(FLAG_ENABLE_INVERSION);
+        System.clearProperty(FLAG_FORCE_MONOCHROME);
+        System.clearProperty(FLAG_USE_DITHERING_Y1);
+        System.clearProperty(FLAG_USE_DITHERING_Y4);
+    }
+
+    /**
+     * Tests the EPD system property for the frame buffer color depth.
+     */
+    @Test
+    public void testBitsPerPixel() {
+        System.setProperty(BITS_PER_PIXEL, "8");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(8, settings.bitsPerPixel);
+
+        System.setProperty(BITS_PER_PIXEL, "16");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(16, settings.bitsPerPixel);
+
+        System.setProperty(BITS_PER_PIXEL, "32");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(32, settings.bitsPerPixel);
+
+        System.err.println(String.format(VERIFY_ERROR, BITS_PER_PIXEL, 64));
+        System.setProperty(BITS_PER_PIXEL, "64");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(32, settings.bitsPerPixel);
+    }
+
+    /**
+     * Tests the EPD system property for the frame buffer rotation.
+     */
+    @Test
+    public void testRotate() {
+        System.setProperty(ROTATE, "0");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0, settings.rotate);
+
+        System.setProperty(ROTATE, "1");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(1, settings.rotate);
+
+        System.setProperty(ROTATE, "2");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(2, settings.rotate);
+
+        System.setProperty(ROTATE, "3");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(3, settings.rotate);
+
+        System.err.println(String.format(VERIFY_ERROR, ROTATE, 4));
+        System.setProperty(ROTATE, "4");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0, settings.rotate);
+    }
+
+    /**
+     * Tests the EPD system property for whether the 8-bit pixels of the frame
+     * buffer are inverted. This property is ignored if the frame buffer is not
+     * in the Y8 pixel format.
+     */
+    @Test
+    public void testY8Inverted() {
+        System.setProperty(Y8_INVERTED, "false");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0, settings.grayscale);
+
+        System.setProperty(Y8_INVERTED, "true");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0, settings.grayscale);
+
+        System.setProperty(BITS_PER_PIXEL, "8");
+
+        System.setProperty(Y8_INVERTED, "false");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0x1, settings.grayscale);
+
+        System.setProperty(Y8_INVERTED, "true");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0x2, settings.grayscale);
+    }
+
+    /**
+     * Tests the EPD system property for whether to wait for the previous update
+     * to complete before sending the next update.
+     */
+    @Test
+    public void testNoWait() {
+        System.setProperty(NO_WAIT, "false");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(false, settings.noWait);
+
+        System.setProperty(NO_WAIT, "true");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(true, settings.noWait);
+    }
+
+    /**
+     * Tests the EPD system property for the update waveform mode.
+     */
+    @Test
+    public void testWaveformMode() {
+        System.setProperty(WAVEFORM_MODE, "1");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(1, settings.waveformMode);
+
+        System.setProperty(WAVEFORM_MODE, "2");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(2, settings.waveformMode);
+
+        System.setProperty(WAVEFORM_MODE, "3");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(3, settings.waveformMode);
+
+        System.setProperty(WAVEFORM_MODE, "4");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(4, settings.waveformMode);
+
+        System.err.println(String.format(VERIFY_ERROR, WAVEFORM_MODE, 5));
+        System.setProperty(WAVEFORM_MODE, "5");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(257, settings.waveformMode);
+
+        System.setProperty(WAVEFORM_MODE, "257");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(257, settings.waveformMode);
+    }
+
+    /**
+     * Tests the EPD system property for whether to invert the pixels in
+     * updates.
+     */
+    @Test
+    public void testFlagEnableInversion() {
+        System.setProperty(FLAG_ENABLE_INVERSION, "false");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0, settings.flags);
+
+        System.setProperty(FLAG_ENABLE_INVERSION, "true");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0x01, settings.flags);
+    }
+
+    /**
+     * Tests the EPD system property for whether to convert all pixels to
+     * monochrome (black or white) in updates.
+     */
+    @Test
+    public void testFlagForceMonochrome() {
+        System.setProperty(FLAG_FORCE_MONOCHROME, "false");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0, settings.flags);
+
+        System.setProperty(FLAG_FORCE_MONOCHROME, "true");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0x02, settings.flags);
+    }
+
+    /**
+     * Tests the EPD system property for whether to dither the 8-bit grayscale
+     * contents of the frame buffer to 1-bit black and white.
+     */
+    @Test
+    public void testFlagUseDitheringY1() {
+        System.setProperty(FLAG_USE_DITHERING_Y1, "false");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0, settings.flags);
+
+        System.setProperty(FLAG_USE_DITHERING_Y1, "true");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0x2000, settings.flags);
+    }
+
+    /**
+     * Tests the EPD system property for whether to dither the 8-bit grayscale
+     * contents of the frame buffer to 4-bit grayscale.
+     */
+    @Test
+    public void testFlagUseDitheringY4() {
+        System.setProperty(FLAG_USE_DITHERING_Y4, "false");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0, settings.flags);
+
+        System.setProperty(FLAG_USE_DITHERING_Y4, "true");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0x4000, settings.flags);
+    }
+
+    /**
+     * Tests the EPD system properties to enable all EPD update flags.
+     */
+    @Test
+    public void testAllFlags() {
+        System.setProperty(FLAG_ENABLE_INVERSION, "true");
+        System.setProperty(FLAG_FORCE_MONOCHROME, "true");
+        System.setProperty(FLAG_USE_DITHERING_Y1, "true");
+        System.setProperty(FLAG_USE_DITHERING_Y4, "true");
+        settings = EPDSettingsShim.newInstance();
+        Assert.assertEquals(0x01 | 0x02 | 0x2000 | 0x4000, settings.flags);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/system/src/test/java/test/com/sun/glass/ui/monocle/FramebufferY8Test.java	Tue Apr 16 15:25:29 2019 -0700
@@ -0,0 +1,395 @@
+/*
+ * Copyright (c) 2019, 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 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 2 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
+ * 2 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 test.com.sun.glass.ui.monocle;
+
+import com.sun.glass.ui.monocle.FramebufferY8Shim;
+import com.sun.glass.ui.monocle.FramebufferY8SuperShim;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferByte;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.IntBuffer;
+import java.nio.channels.Channels;
+import java.util.stream.IntStream;
+import javax.imageio.ImageIO;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Provides test cases for the {@code FramebufferY8} class.
+ */
+public class FramebufferY8Test {
+
+    private static final String IMAGE_FORMAT = "png";
+    private static final String IMAGE_NAME = "allrgb";
+    private static final String IMAGE_PATH = IMAGE_NAME + "." + IMAGE_FORMAT;
+    private static final String IMAGE_PATH_Y8 = IMAGE_NAME + "Y8." + IMAGE_FORMAT;
+
+    /**
+     * The number of iterations for the performance tests. A value of 100 can
+     * provide more confidence in the results, but a low value of 10 allows the
+     * automated tests to run with less delay.
+     */
+    private static final int ITERATIONS = 10;
+
+    private static final int VALUES_4_BIT = 16;
+    private static final int VALUES_12_BIT = VALUES_4_BIT * VALUES_4_BIT * VALUES_4_BIT;
+    private static final int BITS_TO_BYTES = 3;
+
+    private static final int WIDTH = VALUES_12_BIT;
+    private static final int HEIGHT = VALUES_12_BIT;
+
+    private static ByteBuffer bb;
+    private static IntBuffer pixels;
+
+    /**
+     * Generates the test image in the composition buffer provided to the
+     * {@code Framebuffer} and {@code FramebufferY8} constructors through their
+     * shim subclasses. This method runs only once before all of the test cases.
+     */
+    @BeforeClass
+    public static void onlyOnce() {
+        bb = ByteBuffer.allocate(WIDTH * HEIGHT * Integer.BYTES);
+        bb.order(ByteOrder.nativeOrder());
+        pixels = bb.asIntBuffer();
+        IntStream.range(0, WIDTH * HEIGHT).forEachOrdered(pixels::put);
+        pixels.flip();
+    }
+
+    /**
+     * Copies the image into a byte buffer using the original method of the
+     * older {@code FramebufferY8} superclass, {@code Framebuffer}.
+     *
+     * @param bitsPerPixel the number of bits per pixel in the output buffer
+     * @return a byte buffer containing the copied pixels
+     */
+    private ByteBuffer copyOld(int bitsPerPixel) {
+        int bytesPerPixel = bitsPerPixel >>> BITS_TO_BYTES;
+        var source = new FramebufferY8SuperShim(bb, WIDTH, HEIGHT, bitsPerPixel, true);
+        var target = ByteBuffer.allocate(WIDTH * HEIGHT * bytesPerPixel);
+        source.copyToBuffer(target);
+        target.flip();
+        return target;
+    }
+
+    /**
+     * Copies the image into a byte buffer using the updated method of the newer
+     * {@code FramebufferY8} class.
+     *
+     * @param bitsPerPixel the number of bits per pixel in the output buffer
+     * @return a byte buffer containing the copied pixels
+     */
+    private ByteBuffer copyNew(int bitsPerPixel) {
+        int bytesPerPixel = bitsPerPixel >>> BITS_TO_BYTES;
+        var source = new FramebufferY8Shim(bb, WIDTH, HEIGHT, bitsPerPixel, true);
+        var target = ByteBuffer.allocate(WIDTH * HEIGHT * bytesPerPixel);
+        source.copyToBuffer(target);
+        target.flip();
+        return target;
+    }
+
+    /**
+     * Tests the {@code FramebufferY8.copyToBuffer} method by comparing its
+     * output to that of the original implementation in its superclass.
+     *
+     * @param bitsPerPixel the number of bits per pixel in the output buffer
+     */
+    private void copyTest(int bitsPerPixel) {
+        ByteBuffer oldBuffer = copyOld(bitsPerPixel);
+        ByteBuffer newBuffer = copyNew(bitsPerPixel);
+        if (oldBuffer.hasArray() && newBuffer.hasArray()) {
+            Assert.assertArrayEquals(oldBuffer.array(), newBuffer.array());
+        } else {
+            Assert.assertEquals(oldBuffer, newBuffer);
+        }
+    }
+
+    /**
+     * Writes the image into an output stream using the original method of the
+     * older {@code FramebufferY8} superclass, {@code Framebuffer}.
+     *
+     * @param bitsPerPixel the number of bits per pixel in the output stream
+     * @return an output stream containing the written pixels
+     * @throws IOException if an error occurs writing to the output stream
+     */
+    private ByteArrayOutputStream writeOld(int bitsPerPixel) throws IOException {
+        int bytesPerPixel = bitsPerPixel >>> BITS_TO_BYTES;
+        var source = new FramebufferY8SuperShim(bb, WIDTH, HEIGHT, bitsPerPixel, true);
+        try (var target = new ByteArrayOutputStream(WIDTH * HEIGHT * bytesPerPixel);
+                var channel = Channels.newChannel(target)) {
+            source.write(channel);
+            return target;
+        }
+    }
+
+    /**
+     * Writes the image into an output stream using the updated method of the
+     * newer {@code FramebufferY8} class.
+     *
+     * @param bitsPerPixel the number of bits per pixel in the output stream
+     * @return an output stream containing the written pixels
+     * @throws IOException if an error occurs writing to the output stream
+     */
+    private ByteArrayOutputStream writeNew(int bitsPerPixel) throws IOException {
+        int bytesPerPixel = bitsPerPixel >>> BITS_TO_BYTES;
+        var source = new FramebufferY8Shim(bb, WIDTH, HEIGHT, bitsPerPixel, true);
+        try (var target = new ByteArrayOutputStream(WIDTH * HEIGHT * bytesPerPixel);
+                var channel = Channels.newChannel(target)) {
+            source.write(channel);
+            return target;
+        }
+    }
+
+    /**
+     * Tests the {@code FramebufferY8.write} method by comparing its output to
+     * that of the original implementation in its superclass.
+     *
+     * @param bitsPerPixel the number of bits per pixel in the output stream
+     * @throws IOException if an error occurs writing to the output stream
+     */
+    private void writeTest(int bitsPerPixel) throws IOException {
+        ByteArrayOutputStream oldStream = writeOld(bitsPerPixel);
+        ByteArrayOutputStream newStream = writeNew(bitsPerPixel);
+        Assert.assertArrayEquals(oldStream.toByteArray(), newStream.toByteArray());
+    }
+
+    /**
+     * Prints the duration of a performance test.
+     *
+     * @param source the object containing the tested method
+     * @param method the name of the tested method
+     * @param duration the duration of the performance test
+     */
+    private void printTime(Object source, String method, long duration) {
+        float msPerFrame = (float) duration / ITERATIONS;
+        System.out.println(String.format(
+                "Converted %,d frames of %,d × %,d px to RGB565 in %,d ms (%,.0f ms/frame): %s.%s",
+                ITERATIONS, WIDTH, HEIGHT, duration, msPerFrame,
+                source.getClass().getSuperclass().getSimpleName(), method));
+    }
+
+    /**
+     * Measures the time for the original implementation to copy the test image
+     * to a 16-bit buffer in RGB565 format {@value #ITERATIONS} times.
+     */
+    private long timeOldCopyTo16() {
+        int bitsPerPixel = Short.SIZE;
+        int bytesPerPixel = bitsPerPixel >>> BITS_TO_BYTES;
+        var source = new FramebufferY8SuperShim(bb, WIDTH, HEIGHT, bitsPerPixel, true);
+        var target = ByteBuffer.allocate(WIDTH * HEIGHT * bytesPerPixel);
+        long begin = System.currentTimeMillis();
+        for (int i = 0; i < ITERATIONS; i++) {
+            source.copyToBuffer(target);
+            target.flip();
+        }
+        long end = System.currentTimeMillis();
+        long duration = end - begin;
+        printTime(source, "copyToBuffer", duration);
+        return duration;
+    }
+
+    /**
+     * Measures the time for the updated implementation to copy the test image
+     * to a 16-bit buffer in RGB565 format {@value #ITERATIONS} times.
+     */
+    private long timeNewCopyTo16() {
+        int bitsPerPixel = Short.SIZE;
+        int bytesPerPixel = bitsPerPixel >>> BITS_TO_BYTES;
+        var source = new FramebufferY8Shim(bb, WIDTH, HEIGHT, bitsPerPixel, true);
+        var target = ByteBuffer.allocate(WIDTH * HEIGHT * bytesPerPixel);
+        long begin = System.currentTimeMillis();
+        for (int i = 0; i < ITERATIONS; i++) {
+            source.copyToBuffer(target);
+            target.flip();
+        }
+        long end = System.currentTimeMillis();
+        long duration = end - begin;
+        printTime(source, "copyToBuffer", duration);
+        return duration;
+    }
+
+    /**
+     * Measures the time for the original implementation to write the test image
+     * to a 16-bit output stream in RGB565 format {@value #ITERATIONS} times.
+     *
+     * @throws IOException if an error occurs writing to the output stream
+     */
+    private long timeOldWriteTo16() throws IOException {
+        int bitsPerPixel = Short.SIZE;
+        int bytesPerPixel = bitsPerPixel >>> BITS_TO_BYTES;
+        var source = new FramebufferY8SuperShim(bb, WIDTH, HEIGHT, bitsPerPixel, true);
+        try (var target = new ByteArrayOutputStream(WIDTH * HEIGHT * bytesPerPixel);
+                var channel = Channels.newChannel(target)) {
+            long begin = System.currentTimeMillis();
+            for (int i = 0; i < ITERATIONS; i++) {
+                source.write(channel);
+                target.reset();
+            }
+            long end = System.currentTimeMillis();
+            long duration = end - begin;
+            printTime(source, "write", duration);
+            return duration;
+        }
+    }
+
+    /**
+     * Measures the time for the updated implementation to write the test image
+     * to a 16-bit output stream in RGB565 format {@value #ITERATIONS} times.
+     *
+     * @throws IOException if an error occurs writing to the output stream
+     */
+    private long timeNewWriteTo16() throws IOException {
+        int bitsPerPixel = Short.SIZE;
+        int bytesPerPixel = bitsPerPixel >>> BITS_TO_BYTES;
+        var source = new FramebufferY8Shim(bb, WIDTH, HEIGHT, bitsPerPixel, true);
+        try (var target = new ByteArrayOutputStream(WIDTH * HEIGHT * bytesPerPixel);
+                var channel = Channels.newChannel(target)) {
+            long begin = System.currentTimeMillis();
+            for (int i = 0; i < ITERATIONS; i++) {
+                source.write(channel);
+                target.reset();
+            }
+            long end = System.currentTimeMillis();
+            long duration = end - begin;
+            printTime(source, "write", duration);
+            return duration;
+        }
+    }
+
+    /**
+     * Tests copying the pixels to a 16-bit buffer in RGB565 format.
+     */
+    @Test
+    public void copyTo16() {
+        copyTest(Short.SIZE);
+    }
+
+    /**
+     * Tests copying the pixels to a 32-bit buffer in ARGB32 format.
+     */
+    @Test
+    public void copyTo32() {
+        copyTest(Integer.SIZE);
+    }
+
+    /**
+     * Tests writing the pixels to a 16-bit output stream in RGB565 format.
+     *
+     * @throws IOException if an error occurs writing to the output stream
+     */
+    @Test
+    public void writeTo16() throws IOException {
+        writeTest(Short.SIZE);
+    }
+
+    /**
+     * Tests writing the pixels to a 32-bit output stream in ARGB32 format.
+     *
+     * @throws IOException if an error occurs writing to the output stream
+     */
+    @Test
+    public void writeTo32() throws IOException {
+        writeTest(Integer.SIZE);
+    }
+
+    /**
+     * Measures the time for the original and updated methods to copy the test
+     * image to a 16-bit buffer in RGB565 format. This method prints a warning
+     * when the newer updated method is slower than the older original one.
+     */
+    @Test
+    public void timeCopyTo16() {
+        long oldTime = timeOldCopyTo16();
+        long newTime = timeNewCopyTo16();
+        if (newTime > oldTime) {
+            System.err.println("Warning: FramebufferY8.copyToBuffer with 16-bit target is slower");
+        }
+    }
+
+    /**
+     * Measures the time for the original and updated methods to write the test
+     * image to a 16-bit output stream in RGB565 format. This method prints a
+     * warning when the newer updated method is slower than the older original
+     * one.
+     *
+     * @throws IOException if an error occurs writing to the output stream
+     */
+    @Test
+    public void timeWriteTo16() throws IOException {
+        long oldTime = timeOldWriteTo16();
+        long newTime = timeNewWriteTo16();
+        if (newTime > oldTime) {
+            System.err.println("Warning: FramebufferY8.write with 16-bit target is slower");
+        }
+    }
+
+    /**
+     * Saves the source test image to a file in PNG format.
+     */
+    @Ignore("Saves the source ARGB32 buffer as a PNG image")
+    @Test
+    public void saveImage() {
+        var image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
+        for (int y = 0; y < HEIGHT; y++) {
+            for (int x = 0; x < WIDTH; x++) {
+                image.setRGB(x, y, pixels.get());
+            }
+        }
+        try {
+            ImageIO.write(image, IMAGE_FORMAT, new File(IMAGE_PATH));
+        } catch (IOException e) {
+            System.err.println(String.format("Error saving %s (%s)", IMAGE_PATH, e));
+        }
+    }
+
+    /**
+     * Copies the source test image to a target 8-bit buffer in Y8 grayscale
+     * format with the method {@code FramebufferY8.copyToBuffer} and saves the
+     * resulting image as a PNG file.
+     */
+    @Ignore("Saves the target Y8 buffer as a PNG image")
+    @Test
+    public void saveImageY8() {
+        int bitsPerPixel = Byte.SIZE;
+        int bytesPerPixel = bitsPerPixel >>> BITS_TO_BYTES;
+        var source = new FramebufferY8Shim(bb, WIDTH, HEIGHT, bitsPerPixel, true);
+        var target = ByteBuffer.allocate(WIDTH * HEIGHT * bytesPerPixel);
+        source.copyToBuffer(target);
+        target.flip();
+        var image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_BYTE_GRAY);
+        byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
+        System.arraycopy(target.array(), 0, data, 0, WIDTH * HEIGHT);
+        try {
+            ImageIO.write(image, IMAGE_FORMAT, new File(IMAGE_PATH_Y8));
+        } catch (IOException e) {
+            System.err.println(String.format("Error saving %s (%s)", IMAGE_PATH_Y8, e));
+        }
+    }
+}