package greenfoot.vmcomm;

import bluej.utility.Debug;
import greenfoot.World;
import greenfoot.WorldVisitor;
import greenfoot.core.ShadowProjectProperties;
import greenfoot.core.Simulation;
import greenfoot.core.WorldHandler;
import greenfoot.gui.WorldRenderer;
import greenfoot.gui.input.KeyboardManager;
import greenfoot.gui.input.mouse.MousePollingManager;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import threadchecker.OnThread;
import threadchecker.Tag;

import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferOverflowException;
import java.nio.IntBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;


| Lives on the Simulation VM (aka debug VM), and handles communications with the server | (see {}link VMCommsMain}) | public class VMCommsSimulation { private final WorldRenderer worldRenderer; | Whether the image has been updated | private boolean updateImage;
| The world image (as most recently painted; double-buffered) | private BufferedImage[] worldImages = new BufferedImage[2];
| Index in worldImages of the most recently drawn world | private int drawnWorld;
| Whether the last drawn image is currently being transferred | private boolean transferringImage;
| The prompt for Greenfoot.ask() | private String pAskPrompt;
| The ask request identifier | private int pAskId;
| The answer received from an ask | private String askAnswer;
| The status of entering delay loop | private boolean delayLoopEntered; private final ShadowProjectProperties projectProperties;
| Shared memory documentation (this comment may get moved to somewhere more appropriate later). | | The shared memory consists of two successive lumps of memory. One is used by the server VM to | transmit data, and the other is used by the debug VM for the same purpose. File locks protect | both regions to prevent (in cases where it matters) either side from reading a potentially | incomplete data frame while the other side is still writing it. The locking protocol is | described in VMCommsMain. | | Its format is as follows, where each position is an integer position (i.e. bytes times four): | | Server area (16kb): | Pos 0: Reserved. Currently this region is locked independently; the "real" server area starts |* following this position. * Pos 1: When the number is negative, it indicates that the server VM has sent back * information to the debug VM to read. This includes keyboard and mouse events, | as shown below. | Pos 2: The last consumed image frame received from the debug VM. Note that the debug VM | should not update the image in the buffer until the current image is consumed | (otherwise there may be paint artifacts such as tearing). | Pos 3: Count of commands (C), can be zero | Pos 4 onwards: | Commands. Each command begins with an integer sequence ID, then has | an integer length (L), followed by L integers (L >= 1). | The first integer of the L integers is always the | command type, and the amount of other integers depend on the command. For example, | GreenfootStage.COMMAND_RUN just has the command type integer and no more, whereas | mouse events have four integers. | | Debug VM area (10M - 16kb): [Positions relative to beginning] | | Pos 0: Sequence index when the current (included) image was painted (the image is included | unchanged in subsequent frames). | Pos 1: Width of world image in pixels (W) | Pos 2: Height of world image in pixels (H) | Pos 3 incl to 3+(W*H) excl, if W and H are both greater than zero: | W * H pixels one row at a time with no gaps, each pixel is one | integer, in BGRA form, i.e. blue is highest 8 bits, alpha is lowest. | Pos 3+(W*H): Sequence ID of most recently processed command, or -1 if N/A. | Pos 4+(W*H): Stopped-with-error count. (If this goes up, server VM will bring terminal to front) | Pos 5+(W*H) and 6+(W*H): Two ints (highest bits first) with value of System.currentTimeMillis() | at the point when some execution that may contain user code last started on | the simulation thread, or 0L if user code is not currently running. | Pos 7+(W*H): The current simulation speed (1 to 100) | Pos 8+(W*H): world counter if a world is currently installed, or 0 if there is no world. | Pos 9+(W*H): The world cell size in pixels | Pos 10+(W*H): -1 if not currently awaiting a Greenfoot.ask() answer. | If awaiting, it is count (P) of following codepoints which make up prompt. | Pos 11+(W*H) to 11+(W*H)+P excl: codepoints making up ask prompt. | Pos 11+(W*H)+P: 1 if the the delay loop is currently running, or 0 otherwise. | private final IntBuffer sharedMemory; private int seq = 1; private final FileChannel shmFileChannel; private FileLock putLock; private long lastPaintNanos = System.nanoTime(); private int lastAckCommand = -1; private int lastPaintSeq = -1; private int lastPaintSize; private boolean paintScheduled = false; private int stoppedWithErrorCount = 0; private long startOfCurExecution = 0; private int worldCounter = 0; private World world; private final int fileSize;
| Construct a VMCommsSimulation. | | @param world The world which we are the canvas for. | @param shmFilePath The path to the shared-memory file to be mmap-ed for communication | @SuppressWarnings("resource") @OnThread(Tag.Any) public VMCommsSimulation(ShadowProjectProperties projectProperties, String shmFilePath, int fileSize) { this.projectProperties = projectProperties; worldRenderer = new WorldRenderer(); try { shmFileChannel = new RandomAccessFile(shmFilePath, "rw").getChannel(); this.fileSize = fileSize; MappedByteBuffer mbb = shmFileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize); sharedMemory = mbb.asIntBuffer(); putLock = shmFileChannel.lock(VMCommsMain.USER_AREA_OFFSET_BYTES, fileSize - VMCommsMain.USER_AREA_OFFSET_BYTES, false); new Thread() { @OnThread(value = Tag.Worker,ignoreParent = true) public void run() { while (true) { doInterVMComms(); } } }.start(); } catch (IOException e) { throw new RuntimeException(e); } }
| Sets the world that should be visualised by this canvas. | Can be called from any thread. | @OnThread(Tag.Any) public synchronized void setWorld(World world) { if (this.world != world) { this.worldCounter += 1; this.world = world; } } public static enum PaintWhen { FORCE, IF_DUE, NO_PAINT }
| Paints the current world into the shared memory buffer so that the server VM can | display it in the window there. | | @param paintWhen If IF_DUE, painting may be skipped if it's close to a recent paint. | FORCE always paints, NO_PAINT indicates that an actual image update | is not required but other information in the frame should be sent. | @return Answer from Greenfoot.ask() if available, null otherwise | @OnThread(Tag.Simulation) public String paintRemote(PaintWhen paintWhen) { long now = System.nanoTime(); if (paintWhen == PaintWhen.IF_DUE && now - lastPaintNanos <= 8_333_333L) { paintScheduled = (world != null); return null; } String[] answer = new String[] {null }; boolean sendImage = world != null && (paintWhen != PaintWhen.NO_PAINT || paintScheduled); if (sendImage) { lastPaintNanos = now; int imageWidth = WorldVisitor.getWidthInPixels(world); int imageHeight = WorldVisitor.getHeightInPixels(world); BufferedImage worldImage; synchronized (this) { int toDrawWorld = 1 - drawnWorld; worldImage = worldImages[toDrawWorld]; if (worldImage == null || worldImage.getHeight() != imageHeight || worldImage.getWidth() != imageWidth) { worldImage = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB); worldImages[toDrawWorld] = worldImage; } } worldRenderer.renderWorld(world, worldImage); synchronized (this) { if (! transferringImage) { drawnWorld = 1 - drawnWorld; } updateImage = true; } } return answer[0]; } @OnThread(Tag.Simulation) public synchronized String doAsk(int askId, String askPrompt) { pAskPrompt = askPrompt; pAskId = askId; askAnswer = null; try { do { wait(); } while (askAnswer == null){; } catch (InterruptedException ie){ return ""; } } return askAnswer; }
| Perform communications exchange with the other VM. | @OnThread(Tag.Worker) private void doInterVMComms() { String[] answer = new String[] {null }; FileLock fileLock = null; FileLock syncLock = null; try { fileLock = shmFileChannel.lock(VMCommsMain.SERVER_AREA_OFFSET_BYTES, VMCommsMain.SERVER_AREA_SIZE_BYTES, false); boolean doUpdateImage; World curWorld; int curWorldCounter; synchronized (this) { doUpdateImage = updateImage && world != null; curWorld = this.world; curWorldCounter = this.worldCounter; } sharedMemory.position(1); int recvSeq = sharedMemory.get(); if (recvSeq < 0 && Simulation.getInstance() != null) { int lastConsumedImg = sharedMemory.get(); doUpdateImage &= (lastConsumedImg >= lastPaintSeq); int latest = readCommands(answer); if (latest != -1) { lastAckCommand = latest; } } BufferedImage img; synchronized (this) { img = doUpdateImage ? worldImages[drawnWorld] : null; transferringImage = (img != null); if (img != null) { updateImage = false; } } int [] raw = (img == null) ? null : ((DataBufferInt) img.getData().getDataBuffer()).getData(); int imageWidth = 0; int imageHeight = 0; if (img != null) { imageWidth = img.getWidth(); imageHeight = img.getHeight(); } sharedMemory.position(VMCommsMain.USER_AREA_OFFSET); sharedMemory.put(this.seq++); if (img == null) { sharedMemory.put(lastPaintSeq); sharedMemory.get(); sharedMemory.get(); sharedMemory.position(sharedMemory.position() + lastPaintSize); } else { lastPaintSeq = (seq - 1); sharedMemory.put(lastPaintSeq); sharedMemory.put(imageWidth); sharedMemory.put(imageHeight); for (int i = 0; i < raw.length; i++) { sharedMemory.put(raw[i]); } lastPaintSize = raw.length; paintScheduled = false; synchronized (this) { transferringImage = false; if (updateImage) { drawnWorld = 1 - drawnWorld; } } } sharedMemory.put(lastAckCommand); sharedMemory.put(stoppedWithErrorCount); sharedMemory.put((int)(startOfCurExecution >> 32)); sharedMemory.put((int)(startOfCurExecution & 0xFFFFFFFFL)); if (Simulation.getInstance() != null) { sharedMemory.put(Simulation.getInstance().getSpeed()); } else { sharedMemory.put(0); } sharedMemory.put(curWorld == null ? 0 : curWorldCounter); sharedMemory.put(curWorld == null ? 0 : WorldVisitor.getCellSize(curWorld)); synchronized (this) { if (pAskPrompt == null || answer[0] != null) { sharedMemory.put(-1); } else { int[] codepoints = pAskPrompt.codePoints().toArray(); sharedMemory.put(pAskId); sharedMemory.put(codepoints.length); sharedMemory.put(codepoints); } } if (delayLoopEntered == true) { sharedMemory.put(1); } else { sharedMemory.put(0); } putLock.release(); syncLock = shmFileChannel.lock(VMCommsMain.SYNC_AREA_OFFSET_BYTES, VMCommsMain.SYNC_AREA_SIZE_BYTES, false); fileLock.release(); putLock = shmFileChannel.lock(VMCommsMain.USER_AREA_OFFSET_BYTES, fileSize - VMCommsMain.USER_AREA_OFFSET_BYTES, false); syncLock.release(); } catch (IOException ex) { try { putLock.release(); } catch (Exception e) { } Debug.reportError(ex); } catch (BufferOverflowException ex) { try { putLock.release(); if (fileLock != null) { fileLock.release(); } if (syncLock != null) { syncLock.release(); } } catch (Exception e) { } Debug.message("World size is too large. If your world contains more than around 2.5 million pixels you will need to do the following.\n" + "Close your project, then edit project.greenfoot in a text editor to add the following line:\n" + "shm.size=40000000\n" + "(The default is 20000000, keep increasing if needed.) Save the file and re-open the project in Greenfoot."); } if (answer[0] != null) { gotAskAnswer(answer[0]); } }
| An "ask" answer has been received from the other VM; record it and signal the simulation |* thread. */ private synchronized void gotAskAnswer(String answer) { askAnswer = answer; notifyAll(); | |} | |/** | Read commands from the server VM. Eventually, at the end of the Greenfoot | rewrite, this should live elsewhere (probably in WorldHandler or similar). | | @param answer A one-element array in which to store an ask-answer, if received | @return The command acknowledge to write back to the buffer | private int readCommands(String[] answer) { int lastSeqID = -1; int commandCount = sharedMemory.get(); for (int i = 0; i < commandCount; i++) { lastSeqID = sharedMemory.get(); int commandLength = sharedMemory.get(); int data[] = new int[commandLength]; sharedMemory.get(data); if (Command.isKeyEvent(data[0])) { KeyboardManager keyboardManager = WorldHandler.getInstance().getKeyboardManager(); KeyCode keyCode = KeyCode.values()[data[1]]; String keyText = new String(data, 2, data.length - 2); switch(data[0]) { case Command.KEY_DOWN: keyboardManager.keyPressed(keyCode, keyText); break; case Command.KEY_UP: keyboardManager.keyReleased(keyCode, keyText); break; case Command.KEY_TYPED: keyboardManager.keyTyped(keyCode, keyText); break; } } else if (Command.isMouseEvent(data[0])) { int x = data[1]; int y = data[2]; int button = data[3]; int clickCount = data[4]; MousePollingManager mouseManager = WorldHandler.getInstance().getMouseManager(); switch (data[0]) { case Command.MOUSE_CLICKED: mouseManager.mouseClicked(x, y, MouseButton.values()[button], clickCount); break; case Command.MOUSE_PRESSED: mouseManager.mousePressed(x, y, MouseButton.values()[button]); break; case Command.MOUSE_RELEASED: mouseManager.mouseReleased(x, y, MouseButton.values()[button]); break; case Command.MOUSE_DRAGGED: mouseManager.mouseDragged(x, y, MouseButton.values()[button]); break; case Command.MOUSE_MOVED: mouseManager.mouseMoved(x, y); break; case Command.MOUSE_EXITED: mouseManager.mouseExited(); break; } } else { switch (data[0]) { case Command.COMMAND_RUN: Simulation.getInstance().setPaused(false); break; case Command.COMMAND_PAUSE: Simulation.getInstance().setPaused(true); break; case Command.COMMAND_ACT: Simulation.getInstance().runOnce(); break; case Command.COMMAND_INSTANTIATE_WORLD: String className = new String(data, 1, data.length - 1); WorldHandler.getInstance().instantiateNewWorld(className); break; case Command.COMMAND_DISCARD_WORLD: WorldHandler.getInstance().discardWorld(); break; case Command.COMMAND_CONTINUE_DRAG: WorldHandler.getInstance().continueDragging(data[1], data[2], data[3]); break; case Command.COMMAND_END_DRAG: WorldHandler.getInstance().finishDrag(data[1]); break; case Command.COMMAND_ANSWERED: answer[0] = new String(data, 1, data.length - 1); break; case Command.COMMAND_PROPERTY_CHANGED: int keyLength = data[1]; String key = new String(data, 2, keyLength); int valueLength = data[2+keyLength]; String value = valueLength < 0 ? null : new String(data, 3 + keyLength, valueLength); projectProperties.propertyChangedOnServerVM(key, value); break; case Command.COMMAND_SET_SPEED: Simulation.getInstance().setSpeed(data[1]); break; case Command.COMMAND_WORLD_FOCUS_GAINED: WorldHandler.getInstance().worldFocusChanged(true); break; case Command.COMMAND_WORLD_FOCUS_LOST: WorldHandler.getInstance().worldFocusChanged(false); break; } } } return lastSeqID; }
| Gets a suitable ask ID. This needs to be a sequence number which will be | newer than the last answer, so we just use the last command we received. | public int getAskId() { return lastAckCommand; }
| The simulation thread has stopped with an error; need to let the server VM know. | @OnThread(Tag.Simulation) public void notifyStoppedWithError() { stoppedWithErrorCount += 1; paintRemote(PaintWhen.NO_PAINT); }
| The delay loop is entered; need to let the server VM know. | @OnThread(Tag.Simulation) public void notifyDelayLoopEntered() { delayLoopEntered = true; }
| The delay loop is completed; need to let the server VM know. | @OnThread(Tag.Simulation) public void notifyDelayLoopCompleted() { delayLoopEntered = false; }
| User code has begun executing. Note this in the shared memory area so that the server VM can know. | @OnThread(Tag.Simulation) public void userCodeStarting() { long now = System.currentTimeMillis(); boolean recentlyRunning = now - startOfCurExecution < 1000L; startOfCurExecution = now; if (!recentlyRunning) { paintRemote(PaintWhen.NO_PAINT); } }
| User code has finished executing. Note this in the shared memory area so that the server VM can know. | Each userCodeStopped() event should follow one call to userCodeStarting(). | @OnThread(Tag.Simulation) public void userCodeStopped() { startOfCurExecution = 0L; paintRemote(PaintWhen.NO_PAINT); } }
top, use, map, class VMCommsSimulation

.   VMCommsSimulation
.   run
.   setWorld
.   paintRemote
.   doAsk
.   doInterVMComms
.   readCommands
.   getAskId
.   notifyStoppedWithError
.   notifyDelayLoopEntered
.   notifyDelayLoopCompleted
.   userCodeStarting
.   userCodeStopped




743 neLoCode + 78 LoComm