Контекст OpenGL и честный счетчик кадров
Пункт ноль плана: клиент теперь просит контекст 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.
This commit is contained in:
@@ -1133,3 +1133,62 @@ this.framebuffer.framebufferRender(l * k, i1 * k);
|
|||||||
this.displayGuiScreen((GuiScreen) null);
|
this.displayGuiScreen((GuiScreen) null);
|
||||||
}
|
}
|
||||||
this.leftClickCounter = 10000;
|
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();
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
* Runs one game tick of mod work.
|
||||||
*
|
*
|
||||||
@@ -172,6 +199,7 @@ public final class RsxBootstrap {
|
|||||||
* the state it did not set alone.
|
* the state it did not set alone.
|
||||||
*/
|
*/
|
||||||
public static void renderFrame(Minecraft minecraft) {
|
public static void renderFrame(Minecraft minecraft) {
|
||||||
|
RsxFrameStats.frame();
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
client.advanceFrame();
|
client.advanceFrame();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,11 @@ public final class RsxClientServices implements ClientServices {
|
|||||||
return this.frame;
|
return this.frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public net.rustnotmc.api.client.FrameStats frameStats() {
|
||||||
|
return RsxFrameStats.latest();
|
||||||
|
}
|
||||||
|
|
||||||
private final class RsxScreen implements Screen {
|
private final class RsxScreen implements Screen {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -183,6 +183,10 @@ public final class ClientHost {
|
|||||||
public long frame() {
|
public long frame() {
|
||||||
return 0L;
|
return 0L;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FrameStats frameStats() {
|
||||||
|
return FrameStats.EMPTY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Names currently registered, for logging and diagnostics. */
|
/** Names currently registered, for logging and diagnostics. */
|
||||||
|
|||||||
@@ -29,4 +29,7 @@ public interface ClientServices {
|
|||||||
|
|
||||||
/** Frames drawn, which at a steady frame rate is a better clock than tick. */
|
/** Frames drawn, which at a steady frame rate is a better clock than tick. */
|
||||||
long frame();
|
long frame();
|
||||||
|
|
||||||
|
/** How fast the frames are going, never null. */
|
||||||
|
FrameStats frameStats();
|
||||||
}
|
}
|
||||||
@@ -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() + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,7 +90,7 @@ public final class DemoMod implements RustNotMcMod {
|
|||||||
"RustNotMC " + this.context.modVersion() + " (api v"
|
"RustNotMC " + this.context.modVersion() + " (api v"
|
||||||
+ this.context.apiVersion() + ")",
|
+ this.context.apiVersion() + ")",
|
||||||
kind.name().toLowerCase() + " | tick " + this.context.tick(),
|
kind.name().toLowerCase() + " | tick " + this.context.tick(),
|
||||||
this.context.client().frame() + " frames"
|
this.context.client().frameStats().describe()
|
||||||
+ (this.context.hasClient() ? "" : " [detached]"),
|
+ (this.context.hasClient() ? "" : " [detached]"),
|
||||||
"/rsxdemo x" + (this.commandHits == 0
|
"/rsxdemo x" + (this.commandHits == 0
|
||||||
? " | /rsxadmin -> server"
|
? " | /rsxadmin -> server"
|
||||||
@@ -99,7 +99,6 @@ public final class DemoMod implements RustNotMcMod {
|
|||||||
this.lastCommand,
|
this.lastCommand,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Draw is a plain interface with three methods, not a lambda target. */
|
/** Draw is a plain interface with three methods, not a lambda target. */
|
||||||
private final class Panel implements HudLayout.Draw {
|
private final class Panel implements HudLayout.Draw {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user