From 4a89e547a4afa3ccf0eb0ba2994b3a8d48074f60 Mon Sep 17 00:00:00 2001 From: NorbiPeti Date: Sat, 20 Feb 2021 16:51:43 +0100 Subject: [PATCH] Improve encapsulation and support of the rendering modes --- .../java/sznp/virtualcomputer/Computer.java | 6 +- .../java/sznp/virtualcomputer/PluginMain.java | 50 ++++---- .../renderer/BukkitRenderer.java | 37 +++--- .../virtualcomputer/renderer/GPURenderer.java | 8 +- .../renderer/MCFrameBuffer.java | 108 +++++++++++++----- 5 files changed, 133 insertions(+), 76 deletions(-) diff --git a/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/Computer.java b/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/Computer.java index a601633..e1d3d8b 100644 --- a/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/Computer.java +++ b/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/Computer.java @@ -32,12 +32,14 @@ public final class Computer { private IEventListener listener; private final VirtualBoxManager manager; private MCFrameBuffer framebuffer; + private final boolean direct; - public Computer(PluginMain plugin, VirtualBoxManager manager, IVirtualBox vbox) { + public Computer(PluginMain plugin, VirtualBoxManager manager, IVirtualBox vbox, boolean direct) { this.plugin = plugin; this.manager = manager; session = manager.getSessionObject(); this.vbox = vbox; + this.direct = direct; if (instance != null) throw new IllegalStateException("A computer already exists!"); instance = this; } @@ -107,7 +109,7 @@ public final class Computer { IProgress progress = console.powerUp(); // https://marc.info/?l=vbox-dev&m=142780789819967&w=2 handler.setProgress(progress); handler.registerTo(progress.getEventSource()); //TODO: Show progress bar some way? - val fb = new MCFrameBuffer(console.getDisplay()); + val fb = new MCFrameBuffer(console.getDisplay(), plugin, direct); if (plugin.runEmbedded.get()) fb.start(); String fbid = console.getDisplay().attachFramebuffer(0L, diff --git a/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/PluginMain.java b/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/PluginMain.java index ef17a84..6893885 100644 --- a/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/PluginMain.java +++ b/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/PluginMain.java @@ -14,13 +14,10 @@ import org.virtualbox_6_1.VirtualBoxManager; import sznp.virtualcomputer.events.VBoxEventHandler; import sznp.virtualcomputer.renderer.BukkitRenderer; import sznp.virtualcomputer.renderer.GPURenderer; -import sznp.virtualcomputer.renderer.IRenderer; import sznp.virtualcomputer.util.Utils; import sznp.virtualcomputer.util.VBoxLib; import java.io.File; -import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.Arrays; import java.util.function.Predicate; @@ -32,13 +29,6 @@ public class PluginMain extends ButtonPlugin { private VirtualBoxManager manager; public static PluginMain Instance; - /** - * Only used if {@link #direct} is false. - */ - public static ByteBuffer allpixels; // It's set on each change - private static final ArrayList renderers = new ArrayList<>(); - public static boolean direct; - public static boolean sendAll; @Getter private static boolean pluginEnabled; //The Bukkit plugin has to be enabled for the enable command to work @@ -66,6 +56,14 @@ public class PluginMain extends ButtonPlugin { * This can be useful to save resources as the plugin keeps the VirtualBox interface running while enabled. */ private final ConfigData autoEnable = getIConfig().getData("autoEnable", true); + /** + * The amount of rows to update if the slower, Bukkit renderer is being used. + */ + private final ConfigData updateRows = getIConfig().getData("updateRows", 15, o -> (int) o, i -> BukkitRenderer.updatepixels = i); + /** + * Experimental. Whether all of the image data should be sent even on small changes. Defaults to true. + */ + private final ConfigData sendAll = getIConfig().getData("sendAll", true); @Override public void pluginEnable() { @@ -83,7 +81,6 @@ public class PluginMain extends ButtonPlugin { pluginEnabled = true; try { ConsoleCommandSender ccs = getServer().getConsoleSender(); - sendAll = getConfig().getBoolean("sendAll", true); ccs.sendMessage("§bInitializing VirtualBox..."); String osname = System.getProperty("os.name").toLowerCase(); final boolean windows; @@ -102,7 +99,7 @@ public class PluginMain extends ButtonPlugin { if (notGoodDir.test(new File(vbpath))) vbpath = "/usr/lib/virtualbox"; if (notGoodDir.test(new File(vbpath))) - error("Could not find VirtualBox! Download from https://www.virtualbox.org/wiki/Downloads"); + error("Could not find VirtualBox! Download from https://www.virtualbox.org/wiki/Downloads", null); if (System.getProperty("vbox.home") == null || System.getProperty("vbox.home").isEmpty()) System.setProperty("vbox.home", vbpath); if (System.getProperty("sun.boot.library.path") == null @@ -118,49 +115,53 @@ public class PluginMain extends ButtonPlugin { } IVirtualBox vbox = manager.getVBox(); (listener = new VBoxEventHandler()).registerTo(vbox.getEventSource()); - new Computer(this, manager, vbox); //Saves itself this.manager = manager; ccs.sendMessage("§bLoading Screen..."); + boolean direct; try { - if (useGPU.get()) + if (useGPU.get()) { setupDirectRendering(ccs); - else + direct = true; + } else { setupBukkitRendering(ccs); + direct = false; + } } catch (NoClassDefFoundError | Exception e) { e.printStackTrace(); setupBukkitRendering(ccs); + direct = false; } + new Computer(this, manager, vbox, direct); //Saves itself ccs.sendMessage("§bLoaded!"); val mlpl = new MouseLockerPlayerListener(); mousetask = getServer().getScheduler().runTaskTimer(this, mlpl, 0, 0); getServer().getPluginManager().registerEvents(mlpl, this); } catch (final Exception e) { - getLogger().severe("A fatal error occured, disabling plugin!"); - Bukkit.getPluginManager().disablePlugin(this); - throw new RuntimeException(e); + error(null, e); } } private void setupDirectRendering(CommandSender ccs) throws Exception { for (short i = 0; i < MCX; i++) for (short j = 0; j < MCY; j++) - renderers.add(new GPURenderer((short) (startID.get() + j * MCX + i), Bukkit.getWorlds().get(0), i, j)); - direct = true; + new GPURenderer((short) (startID.get() + j * MCX + i), Bukkit.getWorlds().get(0), sendAll.get(), i, j); ccs.sendMessage("§bUsing Direct Renderer, all good"); } private void setupBukkitRendering(CommandSender ccs) { for (short i = 0; i < MCX * MCY; i++) - renderers.add(new BukkitRenderer((short) (startID.get() + i), Bukkit.getWorlds().get(0), i * 128 * 128 * 4)); - direct = false; + new BukkitRenderer((short) (startID.get() + i), Bukkit.getWorlds().get(0), i * 128 * 128 * 4, getLogger()); ccs.sendMessage("§6Using Bukkit renderer"); } - private void error(String message) { + private void error(String message, Exception e) { //Message OR exception getLogger().severe("A fatal error occured, disabling plugin!"); Bukkit.getPluginManager().disablePlugin(this); - throw new RuntimeException(message); + if (message != null) + throw new RuntimeException(message); + else + throw new RuntimeException(e); } @Override @@ -186,7 +187,6 @@ public class PluginMain extends ButtonPlugin { Computer.getInstance().pluginDisable(ccs); ccs.sendMessage("§aHuh."); saveConfig(); - renderers.clear(); manager.cleanup(); } diff --git a/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/renderer/BukkitRenderer.java b/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/renderer/BukkitRenderer.java index 65b1068..8b4eb98 100644 --- a/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/renderer/BukkitRenderer.java +++ b/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/renderer/BukkitRenderer.java @@ -10,30 +10,33 @@ import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; public class BukkitRenderer extends MapRenderer implements IRenderer { - private ByteBuffer allpixels; - private BufferedImage image; - private int startindex; + private static ByteBuffer allpixels; + private final BufferedImage image; + private final int startindex; + private final Logger logger; /** - * The raw pixel data from the machine in BGRA format + * Updates the screen. + * + * @param allpixels The raw pixel data from the machine in BGRA format */ - public void setAllPixels(ByteBuffer allpixels) { - this.allpixels = allpixels; + public static void update(ByteBuffer allpixels, int x, int y, int width, int height) { + BukkitRenderer.allpixels = allpixels; //TODO: Only update what actually changes } /** * Generic implementation, should work on most versions - * - * @param id - * The ID of the current map - * @param world - * The world to create new maps in - * @param startindex - * The index to start from in allpixels + * + * @param id The ID of the current map + * @param world The world to create new maps in + * @param startindex The index + * @param logger The plugin's logger */ - public BukkitRenderer(short id, World world, int startindex) { + public BukkitRenderer(short id, World world, int startindex, Logger logger) { + this.logger = logger; MapView map = IRenderer.prepare(id, world); map.addRenderer(this); this.startindex = startindex; @@ -72,12 +75,12 @@ public class BukkitRenderer extends MapRenderer implements IRenderer { long diff = System.nanoTime() - time; if (TimeUnit.NANOSECONDS.toMillis(diff) > 40) { - System.out.println("Map rendering took " + TimeUnit.NANOSECONDS.toMillis(diff) + " ms"); + logger.warning("Map rendering took " + TimeUnit.NANOSECONDS.toMillis(diff) + " ms"); } } catch (Exception e) { e.printStackTrace(); - System.out.println("Progess: " + progress); - System.out.println("UpdatePixels: " + updatepixels); + logger.warning("Progess: " + progress); + logger.warning("UpdatePixels: " + updatepixels); } } } diff --git a/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/renderer/GPURenderer.java b/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/renderer/GPURenderer.java index 31ad1c7..b9a2779 100644 --- a/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/renderer/GPURenderer.java +++ b/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/renderer/GPURenderer.java @@ -22,6 +22,7 @@ import java.util.function.BiConsumer; public class GPURenderer extends MapRenderer implements IRenderer { private byte[] buffer; private final GPURendererInternal kernel; + private final boolean sendAll; private int mapx, mapy; //Store at central location after conversion private static int[] colors_; @@ -32,7 +33,8 @@ public class GPURenderer extends MapRenderer implements IRenderer { private static boolean enabled = true; private static boolean warned = false; - public GPURenderer(short id, World world, int mapx, int mapy) throws Exception { + public GPURenderer(short id, World world, boolean sendAll, int mapx, int mapy) throws Exception { + this.sendAll = sendAll; MapView map = IRenderer.prepare(id, world); if (map == null) { kernel = null; @@ -90,7 +92,7 @@ public class GPURenderer extends MapRenderer implements IRenderer { PluginMain.Instance.getLogger().warning("Server performance may be affected"); //TODO: Index 0 out of range 0 } } - if (!PluginMain.sendAll) { + if (!sendAll) { synchronized (kernel) { if (changedX >= (mapx + 1) * 128 || changedY >= (mapy + 1) * 128 || changedX + changedWidth < mapx * 128 || changedY + changedHeight < mapy * 128) { @@ -134,7 +136,7 @@ public class GPURenderer extends MapRenderer implements IRenderer { public static void update(byte[] pixels, int width, int height, int changedX, int changedY, int changedWidth, int changedHeight) { for (GPURenderer r : renderers) { synchronized (r.kernel) { - if (!PluginMain.sendAll) { + if (!r.sendAll) { if (changedX < r.changedX) r.changedX = changedX; if (changedY < r.changedY) diff --git a/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/renderer/MCFrameBuffer.java b/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/renderer/MCFrameBuffer.java index 201aa52..3b599d1 100644 --- a/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/renderer/MCFrameBuffer.java +++ b/VirtualComputer-Core/src/main/java/sznp/virtualcomputer/renderer/MCFrameBuffer.java @@ -2,8 +2,8 @@ package sznp.virtualcomputer.renderer; import com.sun.jna.Pointer; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.Setter; +import lombok.val; import org.bukkit.Bukkit; import org.bukkit.scheduler.BukkitTask; import org.virtualbox_6_1.Holder; @@ -15,12 +15,18 @@ import sznp.virtualcomputer.util.COMUtils; import sznp.virtualcomputer.util.IMCFrameBuffer; import sznp.virtualcomputer.util.Timing; +import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.logging.Logger; -@RequiredArgsConstructor public class MCFrameBuffer implements IMCFrameBuffer { private final IDisplay display; private final Holder holder = new Holder<>(); + private final Logger logger; + private final PluginMain plugin; + private final boolean embedded; + private final boolean direct; private BukkitTask tt; /** * Used when running embedded @@ -29,45 +35,59 @@ public class MCFrameBuffer implements IMCFrameBuffer { /** * Used when not running embedded */ - private byte[] screenImage; //TODO: Remove PluginMain.allpixels (and other PluginMain references) + private byte[] screenImage; + /** + * Used when running in indirect mode, not embedded + */ + private ByteBuffer screenBuffer; private int width; private int height; @Getter @Setter private String id; private final AtomicBoolean shouldUpdate = new AtomicBoolean(); + private final AtomicIntegerArray updateParameters = new AtomicIntegerArray(4); private boolean running; + /** + * Creates a new framebuffer that receives images from the VM and sends the image data to Minecraft. + * + * @param display The VM display to use - TODO: Multiple monitors + * @param plugin The plugin + * @param direct Whether the GPU rendering is used + */ + public MCFrameBuffer(IDisplay display, PluginMain plugin, boolean direct) { + this.display = display; + this.plugin = plugin; + this.logger = plugin.getLogger(); + this.embedded = plugin.runEmbedded.get(); //Don't change even if the config got updated while running + this.direct = direct; + } + @Override public void notifyChange(long screenId, long xOrigin, long yOrigin, long width, long height) { if (tt != null) tt.cancel(); - tt = Bukkit.getScheduler().runTaskAsynchronously(PluginMain.Instance, () -> { + tt = Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { synchronized (this) { //If a change occurs twice, then wait for it try { - if (!PluginMain.Instance.runEmbedded.get()) { //Running separately + if (!embedded) { //Running separately this.width = (int) width; this.height = (int) height; - if (screenImage == null || screenImage.length != width * height * 4) + if (screenImage == null || screenImage.length != width * height * 4) { screenImage = new byte[(int) (width * height * 4)]; - updateScreen(screenImage); + screenBuffer = ByteBuffer.wrap(screenImage); + } + updateScreen((int) xOrigin, (int) yOrigin, (int) width, (int) height); return; } display.querySourceBitmap(0L, holder); long[] ptr = new long[1], w = new long[1], h = new long[1], bpp = new long[1], bpl = new long[1], pf = new long[1]; COMUtils.queryBitmapInfo(holder.value, ptr, w, h, bpp, bpl, pf); - if (PluginMain.direct) { - pointer = new Pointer(ptr[0]); - this.width = (int) w[0]; - this.height = (int) h[0]; - GPURenderer.update(pointer.getByteArray(0L, (int) (w[0] * h[0] * 4)), (int) w[0], (int) h[0], 0, 0, this.width, this.height); - } else { - PluginMain.allpixels = new Pointer(ptr[0]).getByteBuffer(0L, width * height * 4); - if (width * height > 640 * 480) - PluginMain.allpixels.limit(640 * 480 * 4); - else - PluginMain.allpixels.limit((int) (width * height * 4)); - } + pointer = new Pointer(ptr[0]); + this.width = (int) w[0]; + this.height = (int) h[0]; + updateScreen(0, 0, (int) width, (int) height); } catch (VBoxException e) { if (e.getResultCode() == 0x80070005) return; // Machine is being powered down @@ -85,10 +105,14 @@ public class MCFrameBuffer implements IMCFrameBuffer { public void notifyUpdate(long x, long y, long width, long height) { /*if (this.width > 1024 || this.height > 768) return;*/ - if (!PluginMain.direct || shouldUpdate.get()) + if (!direct || shouldUpdate.get()) return; //Don't wait for lock, ignore update since we're updating everything anyway - TODO: Not always synchronized (this) { shouldUpdate.set(true); + updateParameters.set(0, (int) x); + updateParameters.set(1, (int) y); + updateParameters.set(2, (int) width); + updateParameters.set(3, (int) height); notifyAll(); } } @@ -97,19 +121,19 @@ public class MCFrameBuffer implements IMCFrameBuffer { public void notifyUpdateImage(long x, long y, long width, long height, byte[] image) { System.out.println("Update image!"); if (this.width == 0 || this.height == 0) { - PluginMain.Instance.getLogger().warning("Received screen image before resolution change!"); + logger.warning("Received screen image before resolution change!"); return; } for (int i = 0; i < height; i++) //Copy lines of the screen in a fast way System.arraycopy(image, (int) (i * width * 4), screenImage, (int) (x + y * this.width * 4), (int) width * 4); - updateScreen(image); + updateScreen((int) x, (int) y, (int) width, (int) height); } public void start() { - if (!PluginMain.direct) + if (!direct) return; running = true; - Bukkit.getScheduler().runTaskAsynchronously(PluginMain.Instance, () -> { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { try { while (running) { synchronized (this) { @@ -121,7 +145,7 @@ public class MCFrameBuffer implements IMCFrameBuffer { continue; } if (!running) return; - updateScreen(pointer.getByteArray(0L, this.width * this.height * 4)); + updateScreen(updateParameters.get(0), updateParameters.get(1), updateParameters.get(2), updateParameters.get(3)); shouldUpdate.set(false); } } @@ -130,11 +154,37 @@ public class MCFrameBuffer implements IMCFrameBuffer { }); } - private void updateScreen(byte[] pixels) { - Timing t = new Timing(); //TODO: Add support for only sending changed fragments - GPURenderer.update(pixels, this.width, this.height, (int) 0, (int) 0, (int) width, (int) height); + private void updateScreenDirectInternal(byte[] pixels, int x, int y, int width, int height) { + Timing t = new Timing(); + GPURenderer.update(pixels, this.width, this.height, x, y, width, height); if (t.elapsedMS() > 60) //Typically 1ms max - PluginMain.Instance.getLogger().warning("Update took " + t.elapsedMS() + "ms"); + logger.warning("Update took " + t.elapsedMS() + "ms"); + } + + private void updateScreenIndirectInternal(ByteBuffer buffer, int x, int y, int width, int height) { + if (this.width * this.height > 640 * 480) + buffer.limit(640 * 480 * 4); + else + buffer.limit(this.width * this.height * 4); + BukkitRenderer.update(buffer, x, y, width, height); + } + + /** + * Updates the screen when the VM is embedded or when it isn't. + * + * @param x The x of change - passed along to the renderer to use + * @param y The y of change - passed along to the renderer to use + * @param width The width of change - passed along to the renderer to use + * @param height The height of change - passed along to the renderer to use + */ + private void updateScreen(int x, int y, int width, int height) { + if (direct) { + val arr = embedded ? pointer.getByteArray(0L, this.width * this.height * 4) : screenImage; + updateScreenDirectInternal(arr, x, y, width, height); + } else { + val bb = embedded ? pointer.getByteBuffer(0L, (long) this.width * this.height * 4) : screenBuffer; + updateScreenIndirectInternal(bb, x, y, width, height); + } } public void stop() {