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 IEventListener listener;
private final VirtualBoxManager manager; private final VirtualBoxManager manager;
private MCFrameBuffer framebuffer; 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.plugin = plugin;
this.manager = manager; this.manager = manager;
session = manager.getSessionObject(); session = manager.getSessionObject();
this.vbox = vbox; this.vbox = vbox;
this.direct = direct;
if (instance != null) throw new IllegalStateException("A computer already exists!"); if (instance != null) throw new IllegalStateException("A computer already exists!");
instance = this; instance = this;
} }
@ -107,7 +109,7 @@ public final class Computer {
IProgress progress = console.powerUp(); // https://marc.info/?l=vbox-dev&m=142780789819967&w=2 IProgress progress = console.powerUp(); // https://marc.info/?l=vbox-dev&m=142780789819967&w=2
handler.setProgress(progress); handler.setProgress(progress);
handler.registerTo(progress.getEventSource()); //TODO: Show progress bar some way? 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()) if (plugin.runEmbedded.get())
fb.start(); fb.start();
String fbid = console.getDisplay().attachFramebuffer(0L, 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.events.VBoxEventHandler;
import sznp.virtualcomputer.renderer.BukkitRenderer; import sznp.virtualcomputer.renderer.BukkitRenderer;
import sznp.virtualcomputer.renderer.GPURenderer; import sznp.virtualcomputer.renderer.GPURenderer;
import sznp.virtualcomputer.renderer.IRenderer;
import sznp.virtualcomputer.util.Utils; import sznp.virtualcomputer.util.Utils;
import sznp.virtualcomputer.util.VBoxLib; import sznp.virtualcomputer.util.VBoxLib;
import java.io.File; import java.io.File;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -32,13 +29,6 @@ public class PluginMain extends ButtonPlugin {
private VirtualBoxManager manager; private VirtualBoxManager manager;
public static PluginMain Instance; 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 @Getter
private static boolean pluginEnabled; //The Bukkit plugin has to be enabled for the enable command to work 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. * 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); 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 @Override
public void pluginEnable() { public void pluginEnable() {
@ -83,7 +81,6 @@ public class PluginMain extends ButtonPlugin {
pluginEnabled = true; pluginEnabled = true;
try { try {
ConsoleCommandSender ccs = getServer().getConsoleSender(); ConsoleCommandSender ccs = getServer().getConsoleSender();
sendAll = getConfig().getBoolean("sendAll", true);
ccs.sendMessage("§bInitializing VirtualBox..."); ccs.sendMessage("§bInitializing VirtualBox...");
String osname = System.getProperty("os.name").toLowerCase(); String osname = System.getProperty("os.name").toLowerCase();
final boolean windows; final boolean windows;
@ -102,7 +99,7 @@ public class PluginMain extends ButtonPlugin {
if (notGoodDir.test(new File(vbpath))) if (notGoodDir.test(new File(vbpath)))
vbpath = "/usr/lib/virtualbox"; vbpath = "/usr/lib/virtualbox";
if (notGoodDir.test(new File(vbpath))) 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()) if (System.getProperty("vbox.home") == null || System.getProperty("vbox.home").isEmpty())
System.setProperty("vbox.home", vbpath); System.setProperty("vbox.home", vbpath);
if (System.getProperty("sun.boot.library.path") == null if (System.getProperty("sun.boot.library.path") == null
@ -118,49 +115,53 @@ public class PluginMain extends ButtonPlugin {
} }
IVirtualBox vbox = manager.getVBox(); IVirtualBox vbox = manager.getVBox();
(listener = new VBoxEventHandler()).registerTo(vbox.getEventSource()); (listener = new VBoxEventHandler()).registerTo(vbox.getEventSource());
new Computer(this, manager, vbox); //Saves itself
this.manager = manager; this.manager = manager;
ccs.sendMessage("§bLoading Screen..."); ccs.sendMessage("§bLoading Screen...");
boolean direct;
try { try {
if (useGPU.get()) if (useGPU.get()) {
setupDirectRendering(ccs); setupDirectRendering(ccs);
else direct = true;
} else {
setupBukkitRendering(ccs); setupBukkitRendering(ccs);
direct = false;
}
} catch (NoClassDefFoundError | Exception e) { } catch (NoClassDefFoundError | Exception e) {
e.printStackTrace(); e.printStackTrace();
setupBukkitRendering(ccs); setupBukkitRendering(ccs);
direct = false;
} }
new Computer(this, manager, vbox, direct); //Saves itself
ccs.sendMessage("§bLoaded!"); ccs.sendMessage("§bLoaded!");
val mlpl = new MouseLockerPlayerListener(); val mlpl = new MouseLockerPlayerListener();
mousetask = getServer().getScheduler().runTaskTimer(this, mlpl, 0, 0); mousetask = getServer().getScheduler().runTaskTimer(this, mlpl, 0, 0);
getServer().getPluginManager().registerEvents(mlpl, this); getServer().getPluginManager().registerEvents(mlpl, this);
} catch (final Exception e) { } catch (final Exception e) {
getLogger().severe("A fatal error occured, disabling plugin!"); error(null, e);
Bukkit.getPluginManager().disablePlugin(this);
throw new RuntimeException(e);
} }
} }
private void setupDirectRendering(CommandSender ccs) throws Exception { private void setupDirectRendering(CommandSender ccs) throws Exception {
for (short i = 0; i < MCX; i++) for (short i = 0; i < MCX; i++)
for (short j = 0; j < MCY; j++) for (short j = 0; j < MCY; j++)
renderers.add(new GPURenderer((short) (startID.get() + j * MCX + i), Bukkit.getWorlds().get(0), i, j)); new GPURenderer((short) (startID.get() + j * MCX + i), Bukkit.getWorlds().get(0), sendAll.get(), i, j);
direct = true;
ccs.sendMessage("§bUsing Direct Renderer, all good"); ccs.sendMessage("§bUsing Direct Renderer, all good");
} }
private void setupBukkitRendering(CommandSender ccs) { private void setupBukkitRendering(CommandSender ccs) {
for (short i = 0; i < MCX * MCY; i++) for (short i = 0; i < MCX * MCY; i++)
renderers.add(new BukkitRenderer((short) (startID.get() + i), Bukkit.getWorlds().get(0), i * 128 * 128 * 4)); new BukkitRenderer((short) (startID.get() + i), Bukkit.getWorlds().get(0), i * 128 * 128 * 4, getLogger());
direct = false;
ccs.sendMessage("§6Using Bukkit renderer"); 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!"); getLogger().severe("A fatal error occured, disabling plugin!");
Bukkit.getPluginManager().disablePlugin(this); Bukkit.getPluginManager().disablePlugin(this);
throw new RuntimeException(message); if (message != null)
throw new RuntimeException(message);
else
throw new RuntimeException(e);
} }
@Override @Override
@ -186,7 +187,6 @@ public class PluginMain extends ButtonPlugin {
Computer.getInstance().pluginDisable(ccs); Computer.getInstance().pluginDisable(ccs);
ccs.sendMessage("§aHuh."); ccs.sendMessage("§aHuh.");
saveConfig(); saveConfig();
renderers.clear();
manager.cleanup(); manager.cleanup();
} }

View file

@ -10,30 +10,33 @@ import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt; import java.awt.image.DataBufferInt;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
public class BukkitRenderer extends MapRenderer implements IRenderer { public class BukkitRenderer extends MapRenderer implements IRenderer {
private ByteBuffer allpixels; private static ByteBuffer allpixels;
private BufferedImage image; private final BufferedImage image;
private int startindex; 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) { public static void update(ByteBuffer allpixels, int x, int y, int width, int height) {
this.allpixels = allpixels; BukkitRenderer.allpixels = allpixels; //TODO: Only update what actually changes
} }
/** /**
* Generic implementation, should work on most versions * Generic implementation, should work on most versions
* *
* @param id * @param id The ID of the current map
* The ID of the current map * @param world The world to create new maps in
* @param world * @param startindex The index
* The world to create new maps in * @param logger The plugin's logger
* @param startindex
* The index to start from in allpixels
*/ */
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); MapView map = IRenderer.prepare(id, world);
map.addRenderer(this); map.addRenderer(this);
this.startindex = startindex; this.startindex = startindex;
@ -72,12 +75,12 @@ public class BukkitRenderer extends MapRenderer implements IRenderer {
long diff = System.nanoTime() - time; long diff = System.nanoTime() - time;
if (TimeUnit.NANOSECONDS.toMillis(diff) > 40) { 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) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
System.out.println("Progess: " + progress); logger.warning("Progess: " + progress);
System.out.println("UpdatePixels: " + updatepixels); logger.warning("UpdatePixels: " + updatepixels);
} }
} }
} }

View file

@ -22,6 +22,7 @@ import java.util.function.BiConsumer;
public class GPURenderer extends MapRenderer implements IRenderer { public class GPURenderer extends MapRenderer implements IRenderer {
private byte[] buffer; private byte[] buffer;
private final GPURendererInternal kernel; private final GPURendererInternal kernel;
private final boolean sendAll;
private int mapx, mapy; private int mapx, mapy;
//Store at central location after conversion //Store at central location after conversion
private static int[] colors_; private static int[] colors_;
@ -32,7 +33,8 @@ public class GPURenderer extends MapRenderer implements IRenderer {
private static boolean enabled = true; private static boolean enabled = true;
private static boolean warned = false; 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); MapView map = IRenderer.prepare(id, world);
if (map == null) { if (map == null) {
kernel = 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 PluginMain.Instance.getLogger().warning("Server performance may be affected"); //TODO: Index 0 out of range 0
} }
} }
if (!PluginMain.sendAll) { if (!sendAll) {
synchronized (kernel) { synchronized (kernel) {
if (changedX >= (mapx + 1) * 128 || changedY >= (mapy + 1) * 128 if (changedX >= (mapx + 1) * 128 || changedY >= (mapy + 1) * 128
|| changedX + changedWidth < mapx * 128 || changedY + changedHeight < mapy * 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) { public static void update(byte[] pixels, int width, int height, int changedX, int changedY, int changedWidth, int changedHeight) {
for (GPURenderer r : renderers) { for (GPURenderer r : renderers) {
synchronized (r.kernel) { synchronized (r.kernel) {
if (!PluginMain.sendAll) { if (!r.sendAll) {
if (changedX < r.changedX) if (changedX < r.changedX)
r.changedX = changedX; r.changedX = changedX;
if (changedY < r.changedY) if (changedY < r.changedY)

View file

@ -2,8 +2,8 @@ package sznp.virtualcomputer.renderer;
import com.sun.jna.Pointer; import com.sun.jna.Pointer;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask; import org.bukkit.scheduler.BukkitTask;
import org.virtualbox_6_1.Holder; import org.virtualbox_6_1.Holder;
@ -15,12 +15,18 @@ import sznp.virtualcomputer.util.COMUtils;
import sznp.virtualcomputer.util.IMCFrameBuffer; import sznp.virtualcomputer.util.IMCFrameBuffer;
import sznp.virtualcomputer.util.Timing; import sznp.virtualcomputer.util.Timing;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.logging.Logger;
@RequiredArgsConstructor
public class MCFrameBuffer implements IMCFrameBuffer { public class MCFrameBuffer implements IMCFrameBuffer {
private final IDisplay display; private final IDisplay display;
private final Holder<IDisplaySourceBitmap> holder = new Holder<>(); 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; private BukkitTask tt;
/** /**
* Used when running embedded * Used when running embedded
@ -29,45 +35,59 @@ public class MCFrameBuffer implements IMCFrameBuffer {
/** /**
* Used when not running embedded * 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 width;
private int height; private int height;
@Getter @Getter
@Setter @Setter
private String id; private String id;
private final AtomicBoolean shouldUpdate = new AtomicBoolean(); private final AtomicBoolean shouldUpdate = new AtomicBoolean();
private final AtomicIntegerArray updateParameters = new AtomicIntegerArray(4);
private boolean running; 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 @Override
public void notifyChange(long screenId, long xOrigin, long yOrigin, long width, long height) { public void notifyChange(long screenId, long xOrigin, long yOrigin, long width, long height) {
if (tt != null) if (tt != null)
tt.cancel(); tt.cancel();
tt = Bukkit.getScheduler().runTaskAsynchronously(PluginMain.Instance, () -> { tt = Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
synchronized (this) { //If a change occurs twice, then wait for it synchronized (this) { //If a change occurs twice, then wait for it
try { try {
if (!PluginMain.Instance.runEmbedded.get()) { //Running separately if (!embedded) { //Running separately
this.width = (int) width; this.width = (int) width;
this.height = (int) height; 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)]; screenImage = new byte[(int) (width * height * 4)];
updateScreen(screenImage); screenBuffer = ByteBuffer.wrap(screenImage);
}
updateScreen((int) xOrigin, (int) yOrigin, (int) width, (int) height);
return; return;
} }
display.querySourceBitmap(0L, holder); 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]; 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); COMUtils.queryBitmapInfo(holder.value, ptr, w, h, bpp, bpl, pf);
if (PluginMain.direct) { pointer = new Pointer(ptr[0]);
pointer = new Pointer(ptr[0]); this.width = (int) w[0];
this.width = (int) w[0]; this.height = (int) h[0];
this.height = (int) h[0]; updateScreen(0, 0, (int) width, (int) height);
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));
}
} catch (VBoxException e) { } catch (VBoxException e) {
if (e.getResultCode() == 0x80070005) if (e.getResultCode() == 0x80070005)
return; // Machine is being powered down 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) { public void notifyUpdate(long x, long y, long width, long height) {
/*if (this.width > 1024 || this.height > 768) /*if (this.width > 1024 || this.height > 768)
return;*/ 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 return; //Don't wait for lock, ignore update since we're updating everything anyway - TODO: Not always
synchronized (this) { synchronized (this) {
shouldUpdate.set(true); shouldUpdate.set(true);
updateParameters.set(0, (int) x);
updateParameters.set(1, (int) y);
updateParameters.set(2, (int) width);
updateParameters.set(3, (int) height);
notifyAll(); notifyAll();
} }
} }
@ -97,19 +121,19 @@ public class MCFrameBuffer implements IMCFrameBuffer {
public void notifyUpdateImage(long x, long y, long width, long height, byte[] image) { public void notifyUpdateImage(long x, long y, long width, long height, byte[] image) {
System.out.println("Update image!"); System.out.println("Update image!");
if (this.width == 0 || this.height == 0) { 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; return;
} }
for (int i = 0; i < height; i++) //Copy lines of the screen in a fast way 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); 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() { public void start() {
if (!PluginMain.direct) if (!direct)
return; return;
running = true; running = true;
Bukkit.getScheduler().runTaskAsynchronously(PluginMain.Instance, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try { try {
while (running) { while (running) {
synchronized (this) { synchronized (this) {
@ -121,7 +145,7 @@ public class MCFrameBuffer implements IMCFrameBuffer {
continue; continue;
} }
if (!running) return; 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); shouldUpdate.set(false);
} }
} }
@ -130,11 +154,37 @@ public class MCFrameBuffer implements IMCFrameBuffer {
}); });
} }
private void updateScreen(byte[] pixels) { private void updateScreenDirectInternal(byte[] pixels, int x, int y, int width, int height) {
Timing t = new Timing(); //TODO: Add support for only sending changed fragments Timing t = new Timing();
GPURenderer.update(pixels, this.width, this.height, (int) 0, (int) 0, (int) width, (int) height); GPURenderer.update(pixels, this.width, this.height, x, y, width, height);
if (t.elapsedMS() > 60) //Typically 1ms max 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() { public void stop() {