diff --git a/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/EAGMinecraftServer.java b/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/EAGMinecraftServer.java index 681133d..bd2d085 100644 --- a/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/EAGMinecraftServer.java +++ b/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/EAGMinecraftServer.java @@ -59,15 +59,10 @@ public class EAGMinecraftServer extends MinecraftServer { this.tick(); lastTick = SysUtil.steadyTimeMillis(); } else { - boolean mustYield = false; - while (delta >= 50L) { - if(mustYield) { - SysUtil.sleep(1); // allow some async - } + if (delta >= 50l) { delta -= 50L; - lastTick = SysUtil.steadyTimeMillis(); + lastTick += 50l; this.tick(); - mustYield = true; } } diff --git a/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/IntegratedServer.java b/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/IntegratedServer.java index a493094..1d4e376 100644 --- a/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/IntegratedServer.java +++ b/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/IntegratedServer.java @@ -749,6 +749,8 @@ public class IntegratedServer { sendIPCPacket(new IPCPacketFFProcessKeepAlive(IPCPacket01StopServer.ID)); currentProcess = null; } + }else { + SysUtil.sleep(50); } } @@ -767,7 +769,7 @@ public class IntegratedServer { mainLoop(); - SysUtil.sleep(1); // allow some async to occur + SysUtil.immediateContinue(); } // yee diff --git a/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/MessageChannel.java b/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/MessageChannel.java new file mode 100644 index 0000000..2f024cc --- /dev/null +++ b/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/MessageChannel.java @@ -0,0 +1,36 @@ +package net.lax1dude.eaglercraft.sp; + +import org.teavm.jso.JSBody; +import org.teavm.jso.JSClass; +import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; +import org.teavm.jso.workers.MessagePort; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +@JSClass +public class MessageChannel implements JSObject { + + @JSBody(params = { }, script = "return (typeof MessageChannel !== \"undefined\");") + public static native boolean supported(); + + @JSProperty + public native MessagePort getPort1(); + + @JSProperty + public native MessagePort getPort2(); + +} diff --git a/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/SysUtil.java b/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/SysUtil.java index daaf099..d56cc28 100644 --- a/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/SysUtil.java +++ b/sp-server/src/main/java/net/lax1dude/eaglercraft/sp/SysUtil.java @@ -4,6 +4,10 @@ import org.teavm.interop.Async; import org.teavm.interop.AsyncCallback; import org.teavm.jso.JSBody; import org.teavm.jso.JSObject; +import org.teavm.jso.browser.Window; +import org.teavm.jso.core.JSString; +import org.teavm.jso.dom.events.EventListener; +import org.teavm.jso.dom.events.MessageEvent; import org.teavm.platform.Platform; import org.teavm.platform.PlatformRunnable; @@ -45,4 +49,110 @@ public class SysUtil { } } + private static boolean hasCheckedImmediateContinue = false; + private static MessageChannel immediateContinueChannel = null; + private static Runnable currentContinueHack = null; + private static final Object immediateContLock = new Object(); + private static final JSString emptyJSString = JSString.valueOf(""); + + public static void immediateContinue() { + if(!hasCheckedImmediateContinue) { + hasCheckedImmediateContinue = true; + checkImmediateContinueSupport(); + } + if(immediateContinueChannel != null) { + immediateContinueTeaVM(); + }else { + sleep(0); + } + } + + @Async + private static native void immediateContinueTeaVM(); + + private static void immediateContinueTeaVM(final AsyncCallback cb) { + synchronized(immediateContLock) { + if(currentContinueHack != null) { + cb.error(new IllegalStateException("Worker thread is already waiting for an immediate continue callback!")); + return; + } + currentContinueHack = () -> { + cb.complete(null); + }; + try { + immediateContinueChannel.getPort2().postMessage(emptyJSString); + }catch(Throwable t) { + System.err.println("Caught error posting immediate continue, using setTimeout instead"); + Window.setTimeout(() -> cb.complete(null), 0); + } + } + } + + private static void checkImmediateContinueSupport() { + try { + immediateContinueChannel = null; + if(!MessageChannel.supported()) { + System.err.println("Fast immediate continue will be disabled for server context due to MessageChannel being unsupported"); + return; + } + immediateContinueChannel = new MessageChannel(); + immediateContinueChannel.getPort1().addEventListener("message", new EventListener() { + @Override + public void handleEvent(MessageEvent evt) { + Runnable toRun; + synchronized(immediateContLock) { + toRun = currentContinueHack; + currentContinueHack = null; + } + if(toRun != null) { + toRun.run(); + } + } + }); + immediateContinueChannel.getPort1().start(); + immediateContinueChannel.getPort2().start(); + final boolean[] checkMe = new boolean[1]; + checkMe[0] = false; + currentContinueHack = () -> { + checkMe[0] = true; + }; + immediateContinueChannel.getPort2().postMessage(emptyJSString); + if(checkMe[0]) { + currentContinueHack = null; + if(immediateContinueChannel != null) { + safeShutdownChannel(immediateContinueChannel); + } + immediateContinueChannel = null; + System.err.println("Fast immediate continue will be disabled for server context due to actually continuing immediately"); + return; + } + sleep(10); + currentContinueHack = null; + if(!checkMe[0]) { + if(immediateContinueChannel != null) { + safeShutdownChannel(immediateContinueChannel); + } + immediateContinueChannel = null; + System.err.println("Fast immediate continue will be disabled for server context due to startup check failing"); + } + }catch(Throwable t) { + System.err.println("Fast immediate continue will be disabled for server context due to exceptions"); + if(immediateContinueChannel != null) { + safeShutdownChannel(immediateContinueChannel); + } + immediateContinueChannel = null; + } + } + + private static void safeShutdownChannel(MessageChannel chan) { + try { + chan.getPort1().close(); + }catch(Throwable tt) { + } + try { + chan.getPort2().close(); + }catch(Throwable tt) { + } + } + } diff --git a/src/lwjgl/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java b/src/lwjgl/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java index 0ae329a..dbd5864 100644 --- a/src/lwjgl/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java +++ b/src/lwjgl/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java @@ -81,8 +81,6 @@ import net.lax1dude.eaglercraft.RelayQuery.VersionMismatch; import net.lax1dude.eaglercraft.RelayWorldsQuery; import net.lax1dude.eaglercraft.ServerQuery; import net.lax1dude.eaglercraft.Voice; -import net.lax1dude.eaglercraft.adapter.EaglerAdapterImpl2.ProgramGL; -import net.lax1dude.eaglercraft.adapter.EaglerAdapterImpl2.RateLimit; import net.lax1dude.eaglercraft.adapter.lwjgl.GameWindowListener; import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket; import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket07LocalWorlds.LocalWorld; @@ -971,12 +969,16 @@ public class EaglerAdapterImpl2 { public static final boolean shouldShutdown() { return Display.isCloseRequested(); } - public static final void updateDisplay() { - Display.update(); + public static final void updateDisplay(int fpsLimit, boolean vsync) { + if(vsync) { + Display.setVSyncEnabled(true); + Display.update(); + }else { + Display.setVSyncEnabled(false); + Display.sync(fpsLimit); + Display.update(); + } } - public static final void setVSyncEnabled(boolean p1) { - Display.setVSyncEnabled(p1); - } public static final void enableRepeatEvents(boolean b) { Keyboard.enableRepeatEvents(b); } @@ -1005,17 +1007,14 @@ public class EaglerAdapterImpl2 { e.printStackTrace(); } } - public static final void syncDisplay(int performanceToFps) { - Display.sync(performanceToFps); - } - private static final Set rateLimitedAddresses = new HashSet(); - private static final Set blockedAddresses = new HashSet(); + private static final Set rateLimitedAddresses = new HashSet<>(); + private static final Set blockedAddresses = new HashSet<>(); private static WebSocketClient clientSocket = null; private static final Object socketSync = new Object(); - private static LinkedList readPackets = new LinkedList(); + private static LinkedList readPackets = new LinkedList<>(); private static class EaglerSocketClient extends WebSocketClient { @@ -1465,7 +1464,7 @@ public class EaglerAdapterImpl2 { public static final float getVoiceSpeakVolume() { return volumeSpeak; } - private static final Set emptySet = new HashSet(); + private static final Set emptySet = new HashSet<>(); public static final Set getVoiceListening() { return emptySet; } @@ -1478,7 +1477,7 @@ public class EaglerAdapterImpl2 { public static final Set getVoiceMuted() { return emptySet; } - private static final List emptyList = new ArrayList(); + private static final List emptyList = new ArrayList<>(); public static final List getVoiceRecent() { return emptyList; } @@ -1546,8 +1545,8 @@ public class EaglerAdapterImpl2 { private static class ServerQueryImpl extends WebSocketClient implements ServerQuery { - private final LinkedList queryResponses = new LinkedList(); - private final LinkedList queryResponsesBytes = new LinkedList(); + private final LinkedList queryResponses = new LinkedList<>(); + private final LinkedList queryResponsesBytes = new LinkedList<>(); private final String type; private boolean open; private boolean alive; diff --git a/src/main/java/net/lax1dude/eaglercraft/EarlyLoadScreen.java b/src/main/java/net/lax1dude/eaglercraft/EarlyLoadScreen.java index 96389a3..81fd018 100644 --- a/src/main/java/net/lax1dude/eaglercraft/EarlyLoadScreen.java +++ b/src/main/java/net/lax1dude/eaglercraft/EarlyLoadScreen.java @@ -99,7 +99,8 @@ public class EarlyLoadScreen { _wglDrawArrays(_wGL_TRIANGLES, 0, 6); _wglDisableVertexAttribArray(0); _wglFlush(); - updateDisplay(); + updateDisplay(0, false); + sleep(20); _wglUseProgram(null); _wglBindBuffer(_wGL_ARRAY_BUFFER, null); @@ -156,7 +157,8 @@ public class EarlyLoadScreen { _wglDrawArrays(_wGL_TRIANGLES, 0, 6); _wglDisableVertexAttribArray(0); _wglFlush(); - updateDisplay(); + updateDisplay(0, false); + sleep(20); _wglUseProgram(null); _wglBindBuffer(_wGL_ARRAY_BUFFER, null); diff --git a/src/main/java/net/lax1dude/eaglercraft/glemu/EaglerAdapterGL30.java b/src/main/java/net/lax1dude/eaglercraft/glemu/EaglerAdapterGL30.java index 0e830a4..1779ef6 100644 --- a/src/main/java/net/lax1dude/eaglercraft/glemu/EaglerAdapterGL30.java +++ b/src/main/java/net/lax1dude/eaglercraft/glemu/EaglerAdapterGL30.java @@ -1613,4 +1613,33 @@ public class EaglerAdapterGL30 extends EaglerAdapterImpl2 { return ret; } + public static boolean sync(int limitFramerate, long[] timerPtr) { + boolean limitFPS = limitFramerate > 0 && limitFramerate < 1000; + boolean blocked = false; + + if(limitFPS) { + if(timerPtr[0] == 0l) { + timerPtr[0] = steadyTimeMillis(); + }else { + long millis = steadyTimeMillis(); + long frameMillis = (1000l / limitFramerate); + long frameTime = millis - timerPtr[0]; + if(frameTime > 2000l || frameTime < 0l) { + frameTime = frameMillis; + timerPtr[0] = millis; + }else { + timerPtr[0] += frameMillis; + } + if(frameTime >= 0l && frameTime < frameMillis) { + sleep((int)(frameMillis - frameTime)); + blocked = true; + } + } + }else { + timerPtr[0] = 0l; + } + + return blocked; + } + } diff --git a/src/main/java/net/minecraft/client/Minecraft.java b/src/main/java/net/minecraft/client/Minecraft.java index 74c1d6e..28a8373 100644 --- a/src/main/java/net/minecraft/client/Minecraft.java +++ b/src/main/java/net/minecraft/client/Minecraft.java @@ -243,6 +243,7 @@ public class Minecraft implements Runnable { public boolean lanState = false; public boolean yeeState = false; + public boolean checkGLErrors = false; public Minecraft() { this.tempDisplayHeight = 480; @@ -404,7 +405,7 @@ public class Minecraft implements Runnable { EaglerAdapter.glPopMatrix(); EaglerAdapter.glFlush(); - EaglerAdapter.updateDisplay(); + updateDisplay(); long t = t1 + 17 + 17*i - EaglerAdapter.steadyTimeMillis(); if(t > 0) { @@ -437,7 +438,7 @@ public class Minecraft implements Runnable { EaglerAdapter.glPopMatrix(); EaglerAdapter.glFlush(); - EaglerAdapter.updateDisplay(); + updateDisplay(); long t = t1 + 17 + 17*i - EaglerAdapter.steadyTimeMillis(); if(t > 0) { @@ -468,7 +469,7 @@ public class Minecraft implements Runnable { EaglerAdapter.glPopMatrix(); EaglerAdapter.glFlush(); - EaglerAdapter.updateDisplay(); + updateDisplay(); long t = t1 + 17 + 17*i - EaglerAdapter.steadyTimeMillis(); if(t > 0) { @@ -478,7 +479,7 @@ public class Minecraft implements Runnable { EaglerAdapter.glClear(EaglerAdapter.GL_COLOR_BUFFER_BIT | EaglerAdapter.GL_DEPTH_BUFFER_BIT); EaglerAdapter.glFlush(); - EaglerAdapter.updateDisplay(); + updateDisplay(); EaglerAdapter.sleep(100); @@ -521,7 +522,7 @@ public class Minecraft implements Runnable { EaglerAdapter.glEnable(EaglerAdapter.GL_ALPHA_TEST); EaglerAdapter.glAlphaFunc(EaglerAdapter.GL_GREATER, 0.1F); EaglerAdapter.glFlush(); - EaglerAdapter.updateDisplay(); + updateDisplay(); EaglerAdapter.optimize(); } @@ -603,6 +604,7 @@ public class Minecraft implements Runnable { * string. */ public void checkGLError(String par1Str) { + if(!checkGLErrors) return; int var2; @@ -712,7 +714,7 @@ public class Minecraft implements Runnable { EaglerAdapter.glEnable(EaglerAdapter.GL_TEXTURE_2D); if (!EaglerAdapter.isKeyDown(65)) { - EaglerAdapter.updateDisplay(); + updateDisplay(); } if (this.thePlayer != null && this.thePlayer.isEntityInsideOpaqueBlock()) { @@ -781,11 +783,6 @@ public class Minecraft implements Runnable { chunkGeometryUpdates = 0; secondTimer = EaglerAdapter.steadyTimeMillis(); } - this.mcProfiler.startSection("syncDisplay"); - - if (this.func_90020_K() > 0) { - EaglerAdapter.syncDisplay(EntityRenderer.performanceToFps(this.func_90020_K())); - } if(isGonnaTakeDatScreenShot) { isGonnaTakeDatScreenShot = false; @@ -1104,6 +1101,11 @@ public class Minecraft implements Runnable { this.voiceOverlay.setResolution(var4, var5); } + public void updateDisplay() { + int i = this.func_90020_K(); + EaglerAdapter.updateDisplay(i > 0 ? EntityRenderer.performanceToFps(i) : 0, false); + } + private boolean wasPaused = false; /** diff --git a/src/main/java/net/minecraft/src/GuiConnecting.java b/src/main/java/net/minecraft/src/GuiConnecting.java index 8f3e8ea..845a441 100644 --- a/src/main/java/net/minecraft/src/GuiConnecting.java +++ b/src/main/java/net/minecraft/src/GuiConnecting.java @@ -101,13 +101,13 @@ public class GuiConnecting extends GuiScreen { private void showDisconnectScreen(String e) { RateLimit l = EaglerAdapter.getRateLimitStatus(); if(l == RateLimit.NOW_LOCKED) { - this.mc.displayGuiScreen(new GuiDisconnected(this.field_98098_c, "disconnect.ipNowLocked", "disconnect.endOfStream", null)); + this.mc.displayGuiScreen(new GuiDisconnected(this.field_98098_c, "disconnect.ipNowLocked", "disconnect.endOfStream")); }else if(l == RateLimit.LOCKED) { - this.mc.displayGuiScreen(new GuiDisconnected(this.field_98098_c, "disconnect.ipLocked", "disconnect.endOfStream", null)); + this.mc.displayGuiScreen(new GuiDisconnected(this.field_98098_c, "disconnect.ipLocked", "disconnect.endOfStream")); }else if(l == RateLimit.BLOCKED) { - this.mc.displayGuiScreen(new GuiDisconnected(this.field_98098_c, "disconnect.ipBlocked", "disconnect.endOfStream", null)); + this.mc.displayGuiScreen(new GuiDisconnected(this.field_98098_c, "disconnect.ipBlocked", "disconnect.endOfStream")); }else if(l == RateLimit.FAILED_POSSIBLY_LOCKED) { - this.mc.displayGuiScreen(new GuiDisconnected(this.field_98098_c, "disconnect.ipFailedPossiblyLocked", "disconnect.endOfStream", null)); + this.mc.displayGuiScreen(new GuiDisconnected(this.field_98098_c, "disconnect.ipFailedPossiblyLocked", "disconnect.endOfStream")); }else { this.mc.displayGuiScreen(new GuiDisconnected(this.field_98098_c, "connect.failed", "disconnect.genericReason", "could not connect to "+uri, e)); } diff --git a/src/main/java/net/minecraft/src/LoadingScreenRenderer.java b/src/main/java/net/minecraft/src/LoadingScreenRenderer.java index 8acf604..5e20f85 100644 --- a/src/main/java/net/minecraft/src/LoadingScreenRenderer.java +++ b/src/main/java/net/minecraft/src/LoadingScreenRenderer.java @@ -137,7 +137,7 @@ public class LoadingScreenRenderer implements IProgressUpdate { this.mc.fontRenderer.drawStringWithShadow(this.currentlyDisplayedText, (var5 - this.mc.fontRenderer.getStringWidth(this.currentlyDisplayedText)) / 2, var6 / 2 - 4 - 16, 16777215); this.mc.fontRenderer.drawStringWithShadow(this.field_73727_a, (var5 - this.mc.fontRenderer.getStringWidth(this.field_73727_a)) / 2, var6 / 2 - 4 + 8, 16777215); - EaglerAdapter.updateDisplay(); + this.mc.updateDisplay(); } } } diff --git a/src/main/java/net/minecraft/src/NetClientHandler.java b/src/main/java/net/minecraft/src/NetClientHandler.java index 3751645..ddf46fb 100644 --- a/src/main/java/net/minecraft/src/NetClientHandler.java +++ b/src/main/java/net/minecraft/src/NetClientHandler.java @@ -119,20 +119,20 @@ public class NetClientHandler extends NetHandler { RateLimit r = EaglerAdapter.getRateLimitStatus(); if(r != null) { if(r == RateLimit.NOW_LOCKED) { - this.mc.displayGuiScreen(new GuiDisconnected(backToMenu(), "disconnect.ratelimit.ipNowLocked", "disconnect.endOfStream", null)); + this.mc.displayGuiScreen(new GuiDisconnected(backToMenu(), "disconnect.ratelimit.ipNowLocked", "disconnect.endOfStream")); }else if(r == RateLimit.LOCKED) { - this.mc.displayGuiScreen(new GuiDisconnected(backToMenu(), "disconnect.ratelimit.ipLocked", "disconnect.endOfStream", null)); + this.mc.displayGuiScreen(new GuiDisconnected(backToMenu(), "disconnect.ratelimit.ipLocked", "disconnect.endOfStream")); }else if(r == RateLimit.BLOCKED) { - this.mc.displayGuiScreen(new GuiDisconnected(backToMenu(), "disconnect.ratelimit.ipBlocked", "disconnect.endOfStream", null)); + this.mc.displayGuiScreen(new GuiDisconnected(backToMenu(), "disconnect.ratelimit.ipBlocked", "disconnect.endOfStream")); }else if(r == RateLimit.FAILED_POSSIBLY_LOCKED) { - this.mc.displayGuiScreen(new GuiDisconnected(backToMenu(), "disconnect.ratelimit.ipFailedPossiblyLocked", "disconnect.endOfStream", null)); + this.mc.displayGuiScreen(new GuiDisconnected(backToMenu(), "disconnect.ratelimit.ipFailedPossiblyLocked", "disconnect.endOfStream")); }else { - this.mc.displayGuiScreen(new GuiDisconnected(backToMenu(), "disconnect.disconnected", "RateLimit." + r.name(), null)); + this.mc.displayGuiScreen(new GuiDisconnected(backToMenu(), "disconnect.disconnected", "RateLimit." + r.name())); } }else { if(!(this.mc.currentScreen instanceof GuiDisconnected) && !(this.mc.currentScreen instanceof GuiScreenSingleplayerException) && !(this.mc.currentScreen instanceof GuiScreenSingleplayerLoading)) { - this.mc.stopServerAndDisplayGuiScreen(new GuiDisconnected(backToMenu(), "disconnect.disconnected", "disconnect.endOfStream", null)); + this.mc.stopServerAndDisplayGuiScreen(new GuiDisconnected(backToMenu(), "disconnect.disconnected", "disconnect.endOfStream")); } } this.disconnected = true; diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java index fd22d50..75505dd 100644 --- a/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java +++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java @@ -32,6 +32,7 @@ import org.teavm.interop.AsyncCallback; import org.teavm.jso.JSBody; import org.teavm.jso.JSFunctor; import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; import org.teavm.jso.ajax.ReadyStateChangeHandler; import org.teavm.jso.ajax.XMLHttpRequest; import org.teavm.jso.browser.Storage; @@ -39,6 +40,7 @@ import org.teavm.jso.browser.TimerHandler; import org.teavm.jso.browser.Window; import org.teavm.jso.canvas.CanvasRenderingContext2D; import org.teavm.jso.canvas.ImageData; +import org.teavm.jso.core.JSString; import org.teavm.jso.dom.css.CSSStyleDeclaration; import org.teavm.jso.dom.events.ErrorEvent; import org.teavm.jso.dom.events.Event; @@ -108,10 +110,12 @@ import net.lax1dude.eaglercraft.Voice; import net.lax1dude.eaglercraft.adapter.teavm.EaglercraftLANClient; import net.lax1dude.eaglercraft.adapter.teavm.EaglercraftLANServer; import net.lax1dude.eaglercraft.adapter.teavm.EaglercraftVoiceClient; +import net.lax1dude.eaglercraft.adapter.teavm.MessageChannel; import net.lax1dude.eaglercraft.adapter.teavm.SelfDefence; import net.lax1dude.eaglercraft.adapter.teavm.WebGL2RenderingContext; import net.lax1dude.eaglercraft.adapter.teavm.WebGLQuery; import net.lax1dude.eaglercraft.adapter.teavm.WebGLVertexArray; +import net.lax1dude.eaglercraft.glemu.EaglerAdapterGL30; import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket; import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket00Handshake; import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket07LocalWorlds; @@ -243,6 +247,13 @@ public class EaglerAdapterImpl2 { private static String[] identifier = new String[0]; private static String integratedServerScript = "worker_bootstrap.js"; private static boolean anisotropicFilteringSupported = false; + private static boolean vsyncSupport = false; + private static int vsyncTimeout = -1; + private static boolean useDelayOnSwap = false; + private static MessageChannel immediateContinueChannel = null; + private static Runnable currentMsgChannelContinueHack = null; + + private static final EagsFileChooser fileChooser = initFileChooser(); public static final String[] getIdentifier() { return identifier; @@ -395,6 +406,18 @@ public class EaglerAdapterImpl2 { }); onBeforeCloseRegister(); + checkImmediateContinueSupport(); + + vsyncTimeout = -1; + vsyncSupport = false; + + try { + asyncRequestAnimationFrame(); + vsyncSupport = true; + }catch(Throwable t) { + System.err.println("VSync is not supported on this browser!"); + } + initFileChooser(); EarlyLoadScreen.paintScreen(); @@ -452,37 +475,58 @@ public class EaglerAdapterImpl2 { } }, 5000); } - + @JSBody(params = { }, script = "return window.startVoiceClient();") private static native EaglercraftVoiceClient startVoiceClient(); - + + private static interface EagsFileChooser extends JSObject { + + @JSProperty + HTMLElement getInputElement(); + + void openFileChooser(String ext, String mime); + + @JSProperty + ArrayBuffer getGetFileChooserResult(); + + @JSProperty + void setGetFileChooserResult(ArrayBuffer val); + + @JSProperty + String getGetFileChooserResultName(); + + @JSProperty + void setGetFileChooserResultName(String str); + + } + @JSBody(params = { }, script = - "window.eagsFileChooser = {\r\n" + + "var ret = {\r\n" + "inputElement: null,\r\n" + "openFileChooser: function(ext, mime){\r\n" + - "var el = window.eagsFileChooser.inputElement = document.createElement(\"input\");\r\n" + + "var el = ret.inputElement = document.createElement(\"input\");\r\n" + "el.type = \"file\";\r\n" + "el.multiple = false;\r\n" + "el.addEventListener(\"change\", function(evt){\r\n" + - "var f = window.eagsFileChooser.inputElement.files;\r\n" + + "var f = ret.inputElement.files;\r\n" + "if(f.length == 0){\r\n" + - "window.eagsFileChooser.getFileChooserResult = null;\r\n" + + "ret.getFileChooserResult = null;\r\n" + "}else{\r\n" + - "(async function(){\r\n" + - "window.eagsFileChooser.getFileChooserResult = await f[0].arrayBuffer();\r\n" + - "window.eagsFileChooser.getFileChooserResultName = f[0].name;\r\n" + - "})();\r\n" + + "f[0].arrayBuffer().then(function(res) {\r\n" + + "ret.getFileChooserResult = res;\r\n" + + "ret.getFileChooserResultName = f[0].name;\r\n" + + "});\r\n" + "}\r\n" + "});\r\n" + - "window.eagsFileChooser.getFileChooserResult = null;\r\n" + - "window.eagsFileChooser.getFileChooserResultName = null;\r\n" + + "ret.getFileChooserResult = null;\r\n" + + "ret.getFileChooserResultName = null;\r\n" + "el.accept = \".\" + ext;\r\n" + "el.click();\r\n" + "},\r\n" + "getFileChooserResult: null,\r\n" + "getFileChooserResultName: null\r\n" + - "};") - private static native void initFileChooser(); + "}; return ret;") + private static native EagsFileChooser initFileChooser(); public static final void destroyContext() { @@ -1695,7 +1739,13 @@ public class EaglerAdapterImpl2 { public static final boolean shouldShutdown() { return false; } - public static final void updateDisplay() { + public static final boolean isVSyncSupported() { + return vsyncSupport; + } + @JSBody(params = { "doc" }, script = "return (typeof doc.visibilityState !== \"string\") || (doc.visibilityState === \"visible\");") + private static native boolean getVisibilityState(JSObject doc); + private static final long[] syncTimer = new long[1]; + public static final void updateDisplay(int fpsLimit, boolean vsync) { double r = win.getDevicePixelRatio(); int w = parent.getClientWidth(); int h = parent.getClientHeight(); @@ -1713,7 +1763,172 @@ public class EaglerAdapterImpl2 { webgl.blitFramebuffer(0, 0, backBufferWidth, backBufferHeight, 0, 0, w2, h2, COLOR_BUFFER_BIT, NEAREST); webgl.bindFramebuffer(FRAMEBUFFER, backBuffer.obj); resizeBackBuffer(w2, h2); - sleep(1); + + if(getVisibilityState(win.getDocument())) { + if(vsyncSupport && vsync) { + syncTimer[0] = 0l; + asyncRequestAnimationFrame(); + }else { + if(fpsLimit <= 0) { + syncTimer[0] = 0l; + swapDelayTeaVM(); + }else { + if(!EaglerAdapterGL30.sync(fpsLimit, syncTimer)) { + swapDelayTeaVM(); + } + } + } + }else { + syncTimer[0] = 0l; + sleep(50); + } + } + @Async + private static native void asyncRequestAnimationFrame(); + private static void asyncRequestAnimationFrame(AsyncCallback cb) { + if(vsyncTimeout != -1) { + cb.error(new IllegalStateException("Already waiting for vsync!")); + return; + } + final boolean[] hasTimedOut = new boolean[] { false }; + final int[] timeout = new int[] { -1 }; + Window.requestAnimationFrame((d) -> { + if(!hasTimedOut[0]) { + hasTimedOut[0] = true; + if(vsyncTimeout != -1) { + if(vsyncTimeout == timeout[0]) { + try { + Window.clearTimeout(vsyncTimeout); + }catch(Throwable t) { + } + vsyncTimeout = -1; + } + cb.complete(null); + } + } + }); + vsyncTimeout = timeout[0] = Window.setTimeout(() -> { + if(!hasTimedOut[0]) { + hasTimedOut[0] = true; + if(vsyncTimeout != -1) { + vsyncTimeout = -1; + cb.complete(null); + } + } + }, 50); + } + private static final void swapDelayTeaVM() { + if(!useDelayOnSwap && immediateContinueChannel != null) { + immediateContinueTeaVM0(); + }else { + sleep(0); + } + } + public static final void immediateContinue() { + if(immediateContinueChannel != null) { + immediateContinueTeaVM0(); + }else { + sleep(0); + } + } + private static final JSString emptyJSString = JSString.valueOf(""); + @Async + private static native void immediateContinueTeaVM0(); + private static void immediateContinueTeaVM0(final AsyncCallback cb) { + if(currentMsgChannelContinueHack != null) { + cb.error(new IllegalStateException("Main thread is already waiting for an immediate continue callback!")); + return; + } + currentMsgChannelContinueHack = () -> { + cb.complete(null); + }; + try { + immediateContinueChannel.getPort2().postMessage(emptyJSString); + }catch(Throwable t) { + currentMsgChannelContinueHack = null; + System.err.println("Caught error posting immediate continue, using setTimeout instead"); + Window.setTimeout(() -> cb.complete(null), 0); + } + } + private static final int IMMEDIATE_CONT_SUPPORTED = 0; + private static final int IMMEDIATE_CONT_FAILED_NOT_ASYNC = 1; + private static final int IMMEDIATE_CONT_FAILED_NOT_CONT = 2; + private static final int IMMEDIATE_CONT_FAILED_EXCEPTIONS = 3; + private static void checkImmediateContinueSupport() { + immediateContinueChannel = null; + int stat = checkImmediateContinueSupport0(); + if(stat == IMMEDIATE_CONT_SUPPORTED) { + return; + }else if(stat == IMMEDIATE_CONT_FAILED_NOT_ASYNC) { + System.err.println("MessageChannel fast immediate continue hack is incompatible with this browser due to actually continuing immediately!"); + }else if(stat == IMMEDIATE_CONT_FAILED_NOT_CONT) { + System.err.println("MessageChannel fast immediate continue hack is incompatible with this browser due to startup check failing!"); + }else if(stat == IMMEDIATE_CONT_FAILED_EXCEPTIONS) { + System.err.println("MessageChannel fast immediate continue hack is incompatible with this browser due to exceptions!"); + } + immediateContinueChannel = null; + } + private static int checkImmediateContinueSupport0() { + try { + if(!MessageChannel.supported()) { + return IMMEDIATE_CONT_SUPPORTED; + } + immediateContinueChannel = new MessageChannel(); + immediateContinueChannel.getPort1().addEventListener("message", new EventListener() { + @Override + public void handleEvent(MessageEvent evt) { + Runnable toRun = currentMsgChannelContinueHack; + currentMsgChannelContinueHack = null; + if(toRun != null) { + toRun.run(); + } + } + }); + immediateContinueChannel.getPort1().start(); + immediateContinueChannel.getPort2().start(); + final boolean[] checkMe = new boolean[1]; + checkMe[0] = false; + currentMsgChannelContinueHack = () -> { + checkMe[0] = true; + }; + immediateContinueChannel.getPort2().postMessage(emptyJSString); + if(checkMe[0]) { + currentMsgChannelContinueHack = null; + if(immediateContinueChannel != null) { + safeShutdownChannel(immediateContinueChannel); + } + immediateContinueChannel = null; + return IMMEDIATE_CONT_FAILED_NOT_ASYNC; + } + sleep(10); + currentMsgChannelContinueHack = null; + if(!checkMe[0]) { + if(immediateContinueChannel != null) { + safeShutdownChannel(immediateContinueChannel); + } + immediateContinueChannel = null; + return IMMEDIATE_CONT_FAILED_NOT_CONT; + }else { + return IMMEDIATE_CONT_SUPPORTED; + } + }catch(Throwable t) { + currentMsgChannelContinueHack = null; + if(immediateContinueChannel != null) { + safeShutdownChannel(immediateContinueChannel); + } + immediateContinueChannel = null; + return IMMEDIATE_CONT_FAILED_EXCEPTIONS; + } + } + private static void safeShutdownChannel(MessageChannel chan) { + try { + chan.getPort1().close(); + }catch(Throwable tt) { + } + try { + chan.getPort2().close(); + }catch(Throwable tt) { + } } public static final void setupBackBuffer() { backBuffer = _wglCreateFramebuffer(); @@ -1740,9 +1955,6 @@ public class EaglerAdapterImpl2 { public static final float getContentScaling() { return (float)win.getDevicePixelRatio(); } - public static final void setVSyncEnabled(boolean p1) { - - } public static final void enableRepeatEvents(boolean b) { enableRepeatEvents = b; } @@ -1776,9 +1988,6 @@ public class EaglerAdapterImpl2 { } public static final void setDisplaySize(int x, int y) { - } - public static final void syncDisplay(int performanceToFps) { - } private static final DateFormat dateFormatSS = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss"); @@ -2001,11 +2210,13 @@ public class EaglerAdapterImpl2 { @JSBody(params = { }, script = "window.onbeforeunload = function(){javaMethods.get('net.lax1dude.eaglercraft.adapter.EaglerAdapterImpl2.onWindowUnload()V').invoke();return false;};") private static native void onBeforeCloseRegister(); - @JSBody(params = { "ext", "mime" }, script = "window.eagsFileChooser.openFileChooser(ext, mime);") - public static native void openFileChooser(String ext, String mime); + public static final void openFileChooser(String ext, String mime) { + fileChooser.openFileChooser(ext, mime); + } - @JSBody(params = { }, script = "return window.eagsFileChooser.getFileChooserResult != null;") - public static final native boolean getFileChooserResultAvailable(); + public static final boolean getFileChooserResultAvailable() { + return fileChooser.getGetFileChooserResult() != null; + } public static final byte[] getFileChooserResult() { ArrayBuffer b = getFileChooserResult0(); @@ -2022,12 +2233,16 @@ public class EaglerAdapterImpl2 { getFileChooserResult0(); } - @JSBody(params = { }, script = "var ret = window.eagsFileChooser.getFileChooserResult; window.eagsFileChooser.getFileChooserResult = null; return ret;") - private static native ArrayBuffer getFileChooserResult0(); + private static final ArrayBuffer getFileChooserResult0() { + ArrayBuffer ret = fileChooser.getGetFileChooserResult(); + fileChooser.setGetFileChooserResultName(null); + return ret; + } + + public static final String getFileChooserResultName() { + return fileChooser.getGetFileChooserResultName(); + } - @JSBody(params = { }, script = "var ret = window.eagsFileChooser.getFileChooserResultName; window.eagsFileChooser.getFileChooserResultName = null; return ret;") - public static native String getFileChooserResultName(); - public static final void setListenerPos(float x, float y, float z, float vx, float vy, float vz, float pitch, float yaw) { float var2 = MathHelper.cos(-yaw * 0.017453292F); float var3 = MathHelper.sin(-yaw * 0.017453292F); diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/MessageChannel.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/MessageChannel.java new file mode 100644 index 0000000..193c0cb --- /dev/null +++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/MessageChannel.java @@ -0,0 +1,36 @@ +package net.lax1dude.eaglercraft.adapter.teavm; + +import org.teavm.jso.JSBody; +import org.teavm.jso.JSClass; +import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; +import org.teavm.jso.workers.MessagePort; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +@JSClass +public class MessageChannel implements JSObject { + + @JSBody(params = { }, script = "return (typeof MessageChannel !== \"undefined\");") + public static native boolean supported(); + + @JSProperty + public native MessagePort getPort1(); + + @JSProperty + public native MessagePort getPort2(); + +}