From c0db40f6248a8508a42a00131000b07d4f038514 Mon Sep 17 00:00:00 2001 From: sasheg dev <152582704+SashegDev@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:21:19 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=82=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D1=82=20OpenGL=20=D0=B8=20=D1=87=D0=B5=D1=81=D1=82=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=81=D1=87=D0=B5=D1=82=D1=87=D0=B8=D0=BA=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=B4=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Пункт ноль плана: клиент теперь просит контекст 3.3 в режиме совместимости, а не тот, что выпадал по умолчанию. Зачем: дальний вид будет писаться шейдером GLSL 330, и контекст должен гарантированно быть не ниже 3.3. В профиле совместимости работают и шейдеры игры (#version 120, их 34 штуки), и наши 330-е. LWJGL 2.9.4 выше 3.3 контекст не заказывает - ограничение в его конструкторе, а не в драйвере. Драйвер при этом дает 4.6, и расширение GL_EXT_gpu_shader4, которое просит FXAA-шейдер игры, в этом контексте на месте. Замер кадров теперь пишется в лог каждую секунду: средний, медиана, 1% и 0.1% low. Одного среднего мало: клиент, который идет со 60 кадрами и дважды в секунду проваливается в 10, в среднем остается 60 и ощущается как slideshow. Медленные кадры - это то, что чувствует игрок, поэтому окно держит все времена кадров и говорит про хвост. Среднее и процентили видны и в HUD, так что их не надо вылавливать из скриншотов. Замер: 307 fps | med 2.9 ms | 1% 6.0 ms | 0.1% 18.1 ms. --- game-fork/patches/decompiler-fixes.txt | 59 ++++++++++ .../net/rustnotmc/client/RsxBootstrap.java | 28 +++++ .../rustnotmc/client/RsxClientServices.java | 5 + .../net/rustnotmc/client/RsxFrameStats.java | 111 ++++++++++++++++++ .../net/rustnotmc/api/client/ClientHost.java | 4 + .../rustnotmc/api/client/ClientServices.java | 3 + .../net/rustnotmc/api/client/FrameStats.java | 92 +++++++++++++++ .../main/java/net/rustnotmc/demo/DemoMod.java | 3 +- 8 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 game-fork/src-overlay/net/rustnotmc/client/RsxFrameStats.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/client/FrameStats.java diff --git a/game-fork/patches/decompiler-fixes.txt b/game-fork/patches/decompiler-fixes.txt index 276fc3f..1804c9e 100644 --- a/game-fork/patches/decompiler-fixes.txt +++ b/game-fork/patches/decompiler-fixes.txt @@ -1133,3 +1133,62 @@ this.framebuffer.framebufferRender(l * k, i1 * k); this.displayGuiScreen((GuiScreen) null); } this.leftClickCounter = 10000; + + +=== FILE net/minecraft/client/Minecraft.java +--- BEFORE +import org.lwjgl.opengl.ContextCapabilities; +--- AFTER +import org.lwjgl.opengl.ContextAttribs; +import org.lwjgl.opengl.ContextCapabilities; + +=== FILE net/minecraft/client/Minecraft.java +--- BEFORE + try { + Display.create((new PixelFormat()).withDepthBits(24)); + } catch (LWJGLException lwjglexception) { + Minecraft.LOGGER.error("Couldn't set pixel format", lwjglexception); + + try { + Thread.sleep(1000L); + } catch (InterruptedException interruptedexception) { + ; + } + + if (this.fullscreen) { + this.updateDisplayMode(); + } + + Display.create(); + } +--- AFTER + try { + Display.create((new PixelFormat()).withDepthBits(24).withStencilBits(8), + new ContextAttribs(3, 3).withProfileCompatibility(true)); + } catch (LWJGLException noContext) { + // The driver may have no 3.3 context, and the game is perfectly + // happy on an older one. The far view wants a modern one, but it + // can be written for whatever the context turns out to be. + Minecraft.LOGGER.warn("Could not create a 3.3 context, trying an older one", noContext); + + try { + Display.create((new PixelFormat()).withDepthBits(24)); + } catch (LWJGLException lwjglexception) { + Minecraft.LOGGER.error("Couldn't set pixel format", lwjglexception); + + try { + Thread.sleep(1000L); + } catch (InterruptedException interruptedexception) { + ; + } + + if (this.fullscreen) { + this.updateDisplayMode(); + } + + Display.create(); + } + } + + net.rustnotmc.client.RsxBootstrap.logGl(); + diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java b/game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java index 67546dd..3863b12 100644 --- a/game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java +++ b/game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java @@ -135,6 +135,33 @@ public final class RsxBootstrap { } } + /** + * Writes down what OpenGL we ended up with. + * + * The far view is written against a modern context, and the first thing to + * ask when it draws nothing is what the driver actually gave us. + */ + public static void logGl() { + try { + Log.info("OpenGL " + org.lwjgl.opengl.GL11.glGetString( + org.lwjgl.opengl.GL11.GL_VERSION) + + " | " + org.lwjgl.opengl.GL11.glGetString( + org.lwjgl.opengl.GL11.GL_RENDERER) + + " | " + org.lwjgl.opengl.GL11.glGetString( + org.lwjgl.opengl.GL11.GL_VENDOR)); + // The game's own FXAA shader asks for an extension that modern + // drivers no longer hand out. It is worth knowing whether it is + // there before a player turns the effect on and the game falls + // over. + String extensions = org.lwjgl.opengl.GL11.glGetString( + org.lwjgl.opengl.GL11.GL_EXTENSIONS); + Log.info("OpenGL EXT_gpu_shader4: " + + (extensions != null && extensions.contains("GL_EXT_gpu_shader4"))); + } catch (Throwable throwable) { + Log.error("RustNotMC: could not read the OpenGL version", throwable); + } + } + /** * Runs one game tick of mod work. * @@ -172,6 +199,7 @@ public final class RsxBootstrap { * the state it did not set alone. */ public static void renderFrame(Minecraft minecraft) { + RsxFrameStats.frame(); if (client != null) { client.advanceFrame(); } diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxClientServices.java b/game-fork/src-overlay/net/rustnotmc/client/RsxClientServices.java index a35bcd1..50c6e4e 100644 --- a/game-fork/src-overlay/net/rustnotmc/client/RsxClientServices.java +++ b/game-fork/src-overlay/net/rustnotmc/client/RsxClientServices.java @@ -84,6 +84,11 @@ public final class RsxClientServices implements ClientServices { return this.frame; } + @Override + public net.rustnotmc.api.client.FrameStats frameStats() { + return RsxFrameStats.latest(); + } + private final class RsxScreen implements Screen { @Override diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxFrameStats.java b/game-fork/src-overlay/net/rustnotmc/client/RsxFrameStats.java new file mode 100644 index 0000000..a15b1b8 --- /dev/null +++ b/game-fork/src-overlay/net/rustnotmc/client/RsxFrameStats.java @@ -0,0 +1,111 @@ +package net.rustnotmc.client; + +import java.util.Arrays; +import net.rustnotmc.api.client.FrameStats; +import net.rustnotmc.loader.Log; + +/** + * Counts how fast the frames are going, and how often they do not. + * + * The average is what people quote and what hides the stutter. A client that + * runs at sixty frames a second and drops to ten twice a second still averages + * sixty, and feels like a slideshow. So the window keeps every frame time and + * reports the slow tail as well: the median, the one percent low and the tenth + * of a percent low. + * + * The frame time is measured between the frames, which is the whole point: it + * includes the waiting, so a client that is fast at drawing but slow at + * getting around to drawing again is caught by it. + */ +public final class RsxFrameStats { + + /** A second of frames, with room to spare. */ + private static final int MAX_FRAMES = 4096; + + private static final long[] TIMES = new long[MAX_FRAMES]; + + private static int count; + private static long windowStart; + private static long lastFrameAt; + private static long totalFrames; + + private static volatile FrameStats latest = FrameStats.EMPTY; + + private RsxFrameStats() { + } + + /** + * Called once per drawn frame. + * + * The first frame only starts the clock: there is nothing to compare it + * against yet. + */ + public static void frame() { + long now = System.nanoTime(); + if (lastFrameAt != 0L) { + long delta = now - lastFrameAt; + // a frame longer than a second is a stall, a pause or a breakpoint, + // and putting it in the window would only say that the machine + // stopped for a moment + if (delta > 0L && delta < 1000000000L && count < MAX_FRAMES) { + TIMES[count++] = delta; + } + } + lastFrameAt = now; + totalFrames++; + + if (windowStart == 0L) { + windowStart = now; + return; + } + long elapsed = now - windowStart; + if (elapsed < 1000000000L) { + return; + } + close(elapsed); + } + + /** Ends the window, works out the numbers and writes them down. */ + private static void close(long elapsedNanos) { + long[] sorted = Arrays.copyOf(TIMES, count); + Arrays.sort(sorted); + double seconds = elapsedNanos / 1000000000.0D; + double average = count == 0 ? 0.0D : count / seconds; + double median = percentile(sorted, 0.50D); + double onePercent = percentile(sorted, 0.99D); + double tenthPercent = percentile(sorted, 0.999D); + double worst = count == 0 ? 0.0D : sorted[count - 1]; + + FrameStats stats = new FrameStats(count, average, millis(median), + millis(onePercent), millis(tenthPercent), millis(worst)); + latest = stats; + Log.info("frames: " + stats.describe() + " | total " + totalFrames); + + count = 0; + windowStart = System.nanoTime(); + } + + /** The frame time at that fraction of the window, in nanoseconds. */ + private static double percentile(long[] sorted, double fraction) { + if (sorted.length == 0) { + return 0.0D; + } + int index = (int) (fraction * sorted.length); + if (index >= sorted.length) { + index = sorted.length - 1; + } + if (index < 0) { + index = 0; + } + return sorted[index]; + } + + private static double millis(double nanos) { + return nanos / 1000000.0D; + } + + /** The last window, never null. */ + public static FrameStats latest() { + return latest; + } +} diff --git a/rsx-client/src/main/java/net/rustnotmc/api/client/ClientHost.java b/rsx-client/src/main/java/net/rustnotmc/api/client/ClientHost.java index f6fc913..c1efad1 100644 --- a/rsx-client/src/main/java/net/rustnotmc/api/client/ClientHost.java +++ b/rsx-client/src/main/java/net/rustnotmc/api/client/ClientHost.java @@ -183,6 +183,10 @@ public final class ClientHost { public long frame() { return 0L; } + + public FrameStats frameStats() { + return FrameStats.EMPTY; + } } /** Names currently registered, for logging and diagnostics. */ diff --git a/rsx-client/src/main/java/net/rustnotmc/api/client/ClientServices.java b/rsx-client/src/main/java/net/rustnotmc/api/client/ClientServices.java index 9a4c8d5..e0cb632 100644 --- a/rsx-client/src/main/java/net/rustnotmc/api/client/ClientServices.java +++ b/rsx-client/src/main/java/net/rustnotmc/api/client/ClientServices.java @@ -29,4 +29,7 @@ public interface ClientServices { /** Frames drawn, which at a steady frame rate is a better clock than tick. */ long frame(); + + /** How fast the frames are going, never null. */ + FrameStats frameStats(); } \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/client/FrameStats.java b/rsx-client/src/main/java/net/rustnotmc/api/client/FrameStats.java new file mode 100644 index 0000000..2fc52dc --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/client/FrameStats.java @@ -0,0 +1,92 @@ +package net.rustnotmc.api.client; + +/** + * How fast the frames are going. + * + * The average is the number everybody quotes and the number that hides the + * stutter: a client that runs at sixty frames a second and drops to ten twice + * a second has an average of sixty and feels terrible. The slow frames are + * what a player actually feels, so the window keeps every frame time and + * reports the slow tail as well. + * + * The percentiles are of the frame time, so a bigger number is worse. The + * "one percent low" is the frame time that one frame in a hundred was slower + * than, which is the usual way of saying it. + */ +public final class FrameStats { + + /** The numbers before the first second is over. */ + public static final FrameStats EMPTY = new FrameStats(0, 0.0D, 0.0D, 0.0D, 0.0D, 0); + + private final int frames; + private final double averageFps; + private final double medianMillis; + private final double onePercentMillis; + private final double tenthPercentMillis; + private final double worstMillis; + + public FrameStats(int frames, double averageFps, double medianMillis, + double onePercentMillis, double tenthPercentMillis, + double worstMillis) { + this.frames = frames; + this.averageFps = averageFps; + this.medianMillis = medianMillis; + this.onePercentMillis = onePercentMillis; + this.tenthPercentMillis = tenthPercentMillis; + this.worstMillis = worstMillis; + } + + /** Frames drawn in the last second. */ + public int frames() { + return this.frames; + } + + /** Frames a second, averaged over the last second. */ + public double averageFps() { + return this.averageFps; + } + + /** The middle frame time, in milliseconds. */ + public double medianMillis() { + return this.medianMillis; + } + + /** The frame time one frame in a hundred was slower than, in milliseconds. */ + public double onePercentMillis() { + return this.onePercentMillis; + } + + /** The frame time one frame in a thousand was slower than, in milliseconds. */ + public double tenthPercentMillis() { + return this.tenthPercentMillis; + } + + /** The slowest frame of the last second, in milliseconds. */ + public double worstMillis() { + return this.worstMillis; + } + + /** True once there is something to report. */ + public boolean measured() { + return this.frames > 0; + } + + /** One line, for a log or an overlay. */ + public String describe() { + if (!measured()) { + return "no frames yet"; + } + return String.format(java.util.Locale.ROOT, + "%d fps | med %.1f ms | 1%% %.1f ms | 0.1%% %.1f ms | worst %.1f ms", + Integer.valueOf((int) Math.round(averageFps)), + Double.valueOf(medianMillis), + Double.valueOf(onePercentMillis), + Double.valueOf(tenthPercentMillis), + Double.valueOf(worstMillis)); + } + + @Override + public String toString() { + return "FrameStats[" + describe() + "]"; + } +} diff --git a/rsx-mods/rsxm-demo/src/main/java/net/rustnotmc/demo/DemoMod.java b/rsx-mods/rsxm-demo/src/main/java/net/rustnotmc/demo/DemoMod.java index 74bc88d..db16a3e 100644 --- a/rsx-mods/rsxm-demo/src/main/java/net/rustnotmc/demo/DemoMod.java +++ b/rsx-mods/rsxm-demo/src/main/java/net/rustnotmc/demo/DemoMod.java @@ -90,7 +90,7 @@ public final class DemoMod implements RustNotMcMod { "RustNotMC " + this.context.modVersion() + " (api v" + this.context.apiVersion() + ")", kind.name().toLowerCase() + " | tick " + this.context.tick(), - this.context.client().frame() + " frames" + this.context.client().frameStats().describe() + (this.context.hasClient() ? "" : " [detached]"), "/rsxdemo x" + (this.commandHits == 0 ? " | /rsxadmin -> server" @@ -99,7 +99,6 @@ public final class DemoMod implements RustNotMcMod { this.lastCommand, }; } - /** Draw is a plain interface with three methods, not a lambda target. */ private final class Panel implements HudLayout.Draw {