Improve encapsulation and support of the rendering modes

This commit is contained in:
Norbi Peti 2021-02-20 16:51:43 +01:00
parent 61f3192760
commit 4a89e547a4
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
5 changed files with 133 additions and 76 deletions

View file

@ -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,

View file

@ -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<IRenderer> 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<Boolean> autoEnable = getIConfig().getData("autoEnable", true);
/**
* The amount of rows to update if the slower, Bukkit renderer is being used.
*/
private final ConfigData<Integer> 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<Boolean> 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();
}

View file

@ -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);
}
}
}

View file

@ -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)

View file

@ -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<IDisplaySourceBitmap> 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() {