mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-08 12:35:01 +00:00
SDL presenter fixes and frame pacer adjustments (#2025)
This commit is contained in:
1 parent
553dec5b6d
commit
b2f597a73a
17 files changed
+336
-93
No files matched your search
@@ -58,7 +58,7 @@ public final class SystemProperties {
|
||||
public static final boolean DEBUG_F3_DETAIL = Boolean.getBoolean("angelica.debug.f3Detail");
|
||||
public static final boolean ENABLE_TEST_BLOCKS = Boolean.getBoolean("angelica.debug.testBlocks");
|
||||
public static final boolean DUMP_CLASS = Boolean.getBoolean("angelica.debug.dumpClass");
|
||||
public static final boolean DUMP_SHADERS = Boolean.getBoolean("angelica.debug.dumpShaders") || isDeobf();
|
||||
private static final boolean DUMP_SHADERS_PROP = Boolean.getBoolean("angelica.debug.dumpShaders");
|
||||
public static final boolean REDIRECTOR_LOGSPAM = Boolean.getBoolean("angelica.debug.redirectorLogspam");
|
||||
public static final boolean DEBUG_DISPLAY_LISTS = Boolean.getBoolean("angelica.debug.displayLists");
|
||||
public static final boolean LOG_DISPLAY_LIST_COMPILATION = Boolean.getBoolean("angelica.debug.displayLists.compilation");
|
||||
@@ -68,8 +68,12 @@ public final class SystemProperties {
|
||||
// Set by us, read by celeritas
|
||||
public static final String KEY_CELERITAS_ENABLE_GL_DEBUG = "celeritas.enableGLDebug";
|
||||
|
||||
public static boolean dumpShaders() {
|
||||
return DUMP_SHADERS_PROP || isDeobf();
|
||||
}
|
||||
|
||||
public static Path shaderDumpDir(String phase) {
|
||||
return DUMP_SHADERS ? Paths.get(SHADER_DUMP_ROOT, phase) : null;
|
||||
return dumpShaders() ? Paths.get(SHADER_DUMP_ROOT, phase) : null;
|
||||
}
|
||||
|
||||
public enum UnmappedGLMode {
|
||||
@@ -118,7 +122,7 @@ public final class SystemProperties {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isDeobf() {
|
||||
public static boolean isDeobf() {
|
||||
return Launch.blackboard != null && Boolean.TRUE.equals(Launch.blackboard.get("fml.deobfuscatedEnvironment"));
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ public final class Tracy {
|
||||
LOGGER.warn("Tracy requested (-Dangelica.tracy=true) but no TracyBackend service present");
|
||||
return null;
|
||||
}
|
||||
if (!implementsCurrentAbi(found)) return null;
|
||||
final TracyBackend backend = found;
|
||||
final CountDownLatch done = new CountDownLatch(1);
|
||||
final AtomicReference<Boolean> result = new AtomicReference<>();
|
||||
@@ -136,10 +137,28 @@ public final class Tracy {
|
||||
return backend;
|
||||
}
|
||||
|
||||
private static final String ABI_SENTINEL = "sectionEnter";
|
||||
|
||||
private static boolean implementsCurrentAbi(TracyBackend backend) {
|
||||
try {
|
||||
backend.getClass().getMethod(ABI_SENTINEL, int.class, String.class);
|
||||
return true;
|
||||
} catch (NoSuchMethodException e) {
|
||||
LOGGER.warn("Tracy backend {} was built against an older TracyBackend interface; profiling disabled", backend.getClass().getName());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private record InitRunner(TracyBackend backend, CountDownLatch done, AtomicReference<Boolean> result) implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
final boolean ok = backend.init();
|
||||
boolean ok;
|
||||
try {
|
||||
ok = backend.init();
|
||||
} catch (LinkageError e) {
|
||||
LOGGER.warn("Tracy backend {} failed to link; profiling disabled: {}", backend.getClass().getName(), e);
|
||||
ok = false;
|
||||
}
|
||||
if (!result.compareAndSet(null, ok) && ok) {
|
||||
backend.shutdown();
|
||||
}
|
||||
|
||||
@@ -88,7 +88,6 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.lwjgl.sdl.SDLGPU;
|
||||
import org.lwjgl.sdl.SDLSurface;
|
||||
import org.lwjgl.sdl.SDL_GPUBlitInfo;
|
||||
import org.lwjgl.sdl.SDL_GPUBufferLocation;
|
||||
import org.lwjgl.sdl.SDL_GPUTextureLocation;
|
||||
@@ -111,7 +110,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
|
||||
private final Device device = SDLGPUGate.device();
|
||||
private final FrameManager frameManager = new FrameManager(device);
|
||||
private Presenter presenter;
|
||||
private volatile Presenter presenter;
|
||||
private final ResourceManager resourceManager = new ResourceManager(device, frameManager);
|
||||
private final Image3DClear image3DClear = new Image3DClear();
|
||||
private final ShaderManager shaderManager = new ShaderManager(device);
|
||||
@@ -155,7 +154,8 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
|
||||
private volatile boolean shutdown;
|
||||
|
||||
private OffscreenTarget splashTarget;
|
||||
private volatile OffscreenTarget splashTarget;
|
||||
private volatile boolean splashTeardownPending;
|
||||
|
||||
private static final ThreadRegistry<ContextState> registeredStates = new ThreadRegistry<>(new ContextState[0]);
|
||||
private static final ThreadLocal<ContextState> tlState = new ThreadLocal<>();
|
||||
@@ -241,16 +241,12 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
});
|
||||
GLSMHooks.LOADING_CHECKPOINT
|
||||
.addListener(event -> {
|
||||
if (Thread.currentThread() == GLStateManager.getMainThread()) {
|
||||
if (!GLStateManager.isSplashComplete()) {
|
||||
SplashDispatcher.tryDispatch(this, splashTarget);
|
||||
} else if (splashTarget != null) {
|
||||
SplashDispatcher.signalFrameReady();
|
||||
SplashDispatcher.tryDispatch(this, splashTarget);
|
||||
splashTarget.destroy(resourceManager);
|
||||
splashTarget = null;
|
||||
SplashDispatcher.reset();
|
||||
if (!GLStateManager.isSplashComplete()) {
|
||||
if (Thread.currentThread() == GLStateManager.getMainThread()) {
|
||||
SplashDispatcher.tryDispatch(this, splashTarget, false);
|
||||
}
|
||||
} else if (splashTarget != null) {
|
||||
splashTeardownPending = true;
|
||||
}
|
||||
if (event.requiresSync) {
|
||||
frameManager.requestSyncFlushOnAllRegisteredFrames();
|
||||
@@ -411,12 +407,12 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
@Override public boolean wantsDisplayUpdateGateTiming() { return false; }
|
||||
|
||||
@Override public long lastFrameGateNanos() {
|
||||
if (presenter != null && presenter.isEngaged()) return frameManager.windowFrame().lastFrameGateNanos;
|
||||
if (presenter != null) return frameManager.windowFrame().lastFrameGateNanos;
|
||||
return frameManager.lastFrameGateNanos();
|
||||
}
|
||||
|
||||
@Override public long lastFrameGateEndNanos() {
|
||||
if (presenter != null && presenter.isEngaged()) return frameManager.windowFrame().lastFrameGateEndNanos;
|
||||
if (presenter != null) return frameManager.windowFrame().lastFrameGateEndNanos;
|
||||
return frameManager.lastFrameGateEndNanos();
|
||||
}
|
||||
|
||||
@@ -520,8 +516,9 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
@Override public boolean handleMakeCurrent(Object drawable) {
|
||||
if (!isSDLManagedDrawable(drawable)) return false;
|
||||
Lwjgl3GLCapabilitiesShim.installOnCurrentThread(advertisedCapabilities());
|
||||
if (splashTarget != null && Thread.currentThread() != GLStateManager.getMainThread()) {
|
||||
s().boundFboId = splashTarget.fboId();
|
||||
final OffscreenTarget splash = splashTarget;
|
||||
if (splash != null && Thread.currentThread() != GLStateManager.getMainThread()) {
|
||||
s().boundFboId = splash.fboId();
|
||||
frameManager.frame().swapchainUnavailable = true;
|
||||
}
|
||||
return true;
|
||||
@@ -542,7 +539,8 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
}
|
||||
|
||||
@Override public boolean handleSwapBuffers() {
|
||||
if (splashTarget != null && splashTarget.isFor(s()) && Thread.currentThread() != GLStateManager.getMainThread()) {
|
||||
final OffscreenTarget splash = splashTarget;
|
||||
if (splash != null && splash.isFor(s()) && Thread.currentThread() != GLStateManager.getMainThread()) {
|
||||
endFrameUploadFlush();
|
||||
frameManager.endFrame();
|
||||
SplashDispatcher.signalFrameReady((int) s().viewportW, (int) s().viewportH);
|
||||
@@ -560,13 +558,24 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
final long drawnSize = SplashDispatcher.getDrawnSize();
|
||||
final int srcW = Math.max(1, Math.min((int) (drawnSize >> 32), target.width()));
|
||||
final int srcH = Math.max(1, Math.min((int) drawnSize, target.height()));
|
||||
final FrameState f = frameManager.frame();
|
||||
frameManager.presentBlit(f, target.colorTexture(), srcW, srcH, SDLSurface.SDL_FLIP_VERTICAL);
|
||||
f.presentedThisFrame = false;
|
||||
frameManager.presentSplash(target.colorTexture(), srcW, srcH);
|
||||
}
|
||||
|
||||
private void finishSplash() {
|
||||
splashTeardownPending = false;
|
||||
final OffscreenTarget target = splashTarget;
|
||||
if (target == null) return;
|
||||
SplashDispatcher.signalFrameReady();
|
||||
SplashDispatcher.tryDispatch(this, target, true);
|
||||
if (presenter != null) presenter.drain();
|
||||
splashTarget = null;
|
||||
target.destroy(resourceManager);
|
||||
SplashDispatcher.reset();
|
||||
}
|
||||
|
||||
@Override public void onFrameBegin() {
|
||||
if (shutdown) return;
|
||||
if (splashTeardownPending) finishSplash();
|
||||
if (GLStateManager.isSplashComplete()) applyPendingVSync();
|
||||
frameManager.beginFrame();
|
||||
beginFrameInit();
|
||||
|
||||
@@ -30,6 +30,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.lwjgl.sdl.SDLGPU.*;
|
||||
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_NONE;
|
||||
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_VERTICAL;
|
||||
import static org.lwjgl.system.MemoryStack.*;
|
||||
|
||||
/**
|
||||
@@ -61,7 +62,6 @@ public final class FrameManager {
|
||||
public boolean fbo0UsedThisFrame;
|
||||
public boolean presentedThisFrame;
|
||||
public boolean swapchainUnavailable;
|
||||
public boolean windowThreadChecked;
|
||||
public long renderPassGeneration;
|
||||
|
||||
public boolean wantFenceOnNextSubmit;
|
||||
@@ -464,6 +464,20 @@ public final class FrameManager {
|
||||
}
|
||||
}
|
||||
|
||||
private volatile boolean acquireThreadReported;
|
||||
|
||||
private void checkAcquireOnWindowThread() {
|
||||
if (acquireThreadReported) return;
|
||||
final Thread self = Thread.currentThread();
|
||||
final Thread window = device.getWindowThread();
|
||||
if (window == null || window == self) return;
|
||||
acquireThreadReported = true;
|
||||
|
||||
final String message = "Swapchain acquired on '" + self.getName() + "', window created on '" + window.getName() + "'";
|
||||
if (SystemProperties.isDeobf() && !SystemProperties.DISABLE_SDL_PRESENTER_THREAD) throw new IllegalStateException(message);
|
||||
LOG.warn(message);
|
||||
}
|
||||
|
||||
public long ensureCopyPass() {
|
||||
final FrameState f = frame();
|
||||
if (f.copyPass != 0) {
|
||||
@@ -863,7 +877,7 @@ public final class FrameManager {
|
||||
return frame().lastFrameGateEndNanos;
|
||||
}
|
||||
|
||||
private Presenter presenter;
|
||||
private volatile Presenter presenter;
|
||||
|
||||
public void setPresenter(Presenter presenter) {
|
||||
this.presenter = presenter;
|
||||
@@ -877,18 +891,24 @@ public final class FrameManager {
|
||||
|
||||
public void presentFinalTarget() {
|
||||
if (!finalTarget.hasContent()) return;
|
||||
final long texture = finalTarget.colorTexture();
|
||||
final int width = finalTarget.width();
|
||||
final int height = finalTarget.height();
|
||||
present(finalTarget.colorTexture(), finalTarget.width(), finalTarget.height(), SDL_FLIP_NONE, false);
|
||||
}
|
||||
|
||||
public void presentSplash(long srcTexture, int srcW, int srcH) {
|
||||
present(srcTexture, srcW, srcH, SDL_FLIP_VERTICAL, true);
|
||||
}
|
||||
|
||||
private void present(long srcTexture, int srcW, int srcH, int flipMode, boolean splash) {
|
||||
final FrameState f = frame();
|
||||
if (f.presentedThisFrame) return;
|
||||
final Presenter p = presenter;
|
||||
if (p != null && p.isEngaged()) {
|
||||
final FrameState f = frame();
|
||||
if (f.presentedThisFrame) return;
|
||||
if (p != null) {
|
||||
f.presentedThisFrame = true;
|
||||
p.requestPresent(texture, width, height);
|
||||
p.requestPresent(srcTexture, srcW, srcH, flipMode);
|
||||
} else {
|
||||
presentBlit(frame(), texture, width, height, SDL_FLIP_NONE);
|
||||
presentBlit(f, srcTexture, srcW, srcH, flipMode);
|
||||
}
|
||||
if (splash) f.presentedThisFrame = false;
|
||||
}
|
||||
|
||||
private final AtomicLong windowAcquireWaitNanos = new AtomicLong();
|
||||
@@ -915,8 +935,8 @@ public final class FrameManager {
|
||||
else f.presentSkipsThisFrame++;
|
||||
}
|
||||
|
||||
public void presentOnWindowThread(long srcTexture, int srcW, int srcH) {
|
||||
presentBlit(windowFrame, srcTexture, srcW, srcH, SDL_FLIP_NONE);
|
||||
public void presentOnWindowThread(long srcTexture, int srcW, int srcH, int flipMode) {
|
||||
presentBlit(windowFrame, srcTexture, srcW, srcH, flipMode);
|
||||
windowFrame.presentedThisFrame = false;
|
||||
windowFrame.swapchainUnavailable = false;
|
||||
}
|
||||
@@ -925,16 +945,7 @@ public final class FrameManager {
|
||||
if (srcTexture == 0 || srcW <= 0 || srcH <= 0) return false;
|
||||
if (f.presentedThisFrame || f.swapchainUnavailable) return false;
|
||||
|
||||
if (!f.windowThreadChecked) {
|
||||
f.windowThreadChecked = true;
|
||||
final Thread self = Thread.currentThread();
|
||||
final Thread windowThread = device.getWindowThread();
|
||||
if (windowThread != null && windowThread != self) {
|
||||
LOG.warn("Swapchain acquired on '{}' but the SDL window was created on '{}'; SDL requires acquire on the window-creating thread", self.getName(), windowThread.getName());
|
||||
} else {
|
||||
LOG.info("First swapchain acquire on thread={}", self.getName());
|
||||
}
|
||||
}
|
||||
checkAcquireOnWindowThread();
|
||||
|
||||
final long window = device.getClaimedWindow();
|
||||
if (window == 0) return false;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.frame;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
@@ -16,17 +14,13 @@ public final class Presenter {
|
||||
this.windowThreadExecutor = windowThreadExecutor;
|
||||
}
|
||||
|
||||
public boolean isEngaged() {
|
||||
return windowThreadExecutor != null && GLStateManager.isSplashComplete();
|
||||
}
|
||||
|
||||
public void requestPresent(long srcTexture, int srcW, int srcH) {
|
||||
public void requestPresent(long srcTexture, int srcW, int srcH, int flipMode) {
|
||||
pending.acquireUninterruptibly();
|
||||
boolean submitted = false;
|
||||
try {
|
||||
windowThreadExecutor.execute(() -> {
|
||||
try {
|
||||
frameManager.presentOnWindowThread(srcTexture, srcW, srcH);
|
||||
frameManager.presentOnWindowThread(srcTexture, srcW, srcH, flipMode);
|
||||
} finally {
|
||||
pending.release();
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ final class SdlFramePlots {
|
||||
private static final long PLOT_PIPELINE_KEY_RECOMPUTES = Tracy.plotHandle("sdl.pipelineKeyRecomputes");
|
||||
private static final long PLOT_PIPELINE_FAST_PATH_HITS = Tracy.plotHandle("sdl.pipelineFastPathHits");
|
||||
private static final long PLOT_MID_FRAME_SUBMITS = Tracy.plotHandle("sdl.midFrameSubmits");
|
||||
private static final long PLOT_GATE_US = Tracy.plotHandle("sdl.gateUs");
|
||||
|
||||
private static final int[] contextCounters = new int[SDLGPURenderBackend.CTR_COUNT];
|
||||
|
||||
@@ -110,6 +109,5 @@ final class SdlFramePlots {
|
||||
Tracy.plotInt(PLOT_EMPTY_FRAMES, f.emptyFramesThisFrame);
|
||||
Tracy.plotInt(PLOT_DROPPED_DRAWS, f.droppedDrawsThisFrame);
|
||||
Tracy.plotInt(PLOT_MID_FRAME_SUBMITS, f.midFrameSubmitsThisFrame);
|
||||
Tracy.plotInt(PLOT_GATE_US, f.lastFrameGateNanos / 1000);
|
||||
}
|
||||
}
|
||||
@@ -681,7 +681,7 @@ public final class ShaderManager {
|
||||
return null;
|
||||
}
|
||||
LOG.info("Vertex attribute conversion: program {} converted {}", program, attribs);
|
||||
if (SystemProperties.DUMP_SHADERS) dumpVariantSource(program, src);
|
||||
if (SystemProperties.dumpShaders()) dumpVariantSource(program, src);
|
||||
return v;
|
||||
} finally {
|
||||
memFree(r.spirv());
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ public final class DxbcCrossCompile {
|
||||
return new Output(copy, cached.entrypoint);
|
||||
}
|
||||
|
||||
final int dumpId = SystemProperties.DUMP_SHADERS ? CrossCompileUtil.SHADER_DUMP_COUNTER.getAndIncrement() : -1;
|
||||
final int dumpId = SystemProperties.dumpShaders() ? CrossCompileUtil.SHADER_DUMP_COUNTER.getAndIncrement() : -1;
|
||||
if (dumpId >= 0) CrossCompileUtil.dumpSpirv(spirv, dumpId, glShaderType);
|
||||
|
||||
final String hlsl = compileToHlsl(spirv, glShaderType);
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ public final class MslCrossCompile {
|
||||
return new Output(copy, cached.entrypoint);
|
||||
}
|
||||
|
||||
final int dumpId = SystemProperties.DUMP_SHADERS ? CrossCompileUtil.SHADER_DUMP_COUNTER.getAndIncrement() : -1;
|
||||
final int dumpId = SystemProperties.dumpShaders() ? CrossCompileUtil.SHADER_DUMP_COUNTER.getAndIncrement() : -1;
|
||||
if (dumpId >= 0) CrossCompileUtil.dumpSpirv(spirv, dumpId, glShaderType);
|
||||
|
||||
try (MemoryStack stack = stackPush()) {
|
||||
|
||||
+2
-2
@@ -33,10 +33,10 @@ public final class SplashDispatcher {
|
||||
return (long) w << 32 | (h & 0xffffffffL);
|
||||
}
|
||||
|
||||
public static void tryDispatch(SDLGPURenderBackend backend, OffscreenTarget target) {
|
||||
public static void tryDispatch(SDLGPURenderBackend backend, OffscreenTarget target, boolean force) {
|
||||
if (!frameReady || target == null) return;
|
||||
final long now = System.nanoTime();
|
||||
if (now - lastBlitNanos < MIN_INTERVAL_NS) return;
|
||||
if (!force && now - lastBlitNanos < MIN_INTERVAL_NS) return;
|
||||
lastBlitNanos = now;
|
||||
frameReady = false;
|
||||
backend.dispatchSplashBlit(target);
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.concurrent.Executor;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_NONE;
|
||||
|
||||
@Timeout(10)
|
||||
class PresenterTest {
|
||||
@@ -43,10 +44,10 @@ class PresenterTest {
|
||||
final DeferredExecutor executor = new DeferredExecutor();
|
||||
final Presenter p = presenter(executor);
|
||||
|
||||
p.requestPresent(1L, 10, 10);
|
||||
p.requestPresent(1L, 10, 10, SDL_FLIP_NONE);
|
||||
assertEquals(1, executor.tasks.size());
|
||||
|
||||
final Thread second = new Thread(() -> p.requestPresent(2L, 10, 10));
|
||||
final Thread second = new Thread(() -> p.requestPresent(2L, 10, 10, SDL_FLIP_NONE));
|
||||
second.start();
|
||||
awaitWaiting(second);
|
||||
assertEquals(1, executor.tasks.size(), "second present must not enqueue while the first is in flight");
|
||||
@@ -61,7 +62,7 @@ class PresenterTest {
|
||||
final DeferredExecutor executor = new DeferredExecutor();
|
||||
final Presenter p = presenter(executor);
|
||||
|
||||
p.requestPresent(1L, 10, 10);
|
||||
p.requestPresent(1L, 10, 10, SDL_FLIP_NONE);
|
||||
|
||||
final Thread drainer = new Thread(p::drain);
|
||||
drainer.start();
|
||||
@@ -74,8 +75,8 @@ class PresenterTest {
|
||||
@Test
|
||||
void anInlineExecutorReleasesTheSlotImmediately() {
|
||||
final Presenter p = new Presenter(new FrameManager(new Device()), Runnable::run);
|
||||
p.requestPresent(1L, 10, 10);
|
||||
p.requestPresent(2L, 10, 10);
|
||||
p.requestPresent(1L, 10, 10, SDL_FLIP_NONE);
|
||||
p.requestPresent(2L, 10, 10, SDL_FLIP_NONE);
|
||||
p.drain();
|
||||
}
|
||||
|
||||
@@ -87,7 +88,7 @@ class PresenterTest {
|
||||
fm.frame();
|
||||
final int before = fm.registeredFrameCount();
|
||||
|
||||
p.requestPresent(1L, 10, 10);
|
||||
p.requestPresent(1L, 10, 10, SDL_FLIP_NONE);
|
||||
|
||||
final Thread windowThread = new Thread(executor::runNext, "window-thread");
|
||||
windowThread.start();
|
||||
@@ -95,4 +96,18 @@ class PresenterTest {
|
||||
|
||||
assertEquals(before, fm.registeredFrameCount(), "the window thread must stay out of the frame registry");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theSplashPresentGoesThroughTheWindowThreadToo() {
|
||||
final DeferredExecutor executor = new DeferredExecutor();
|
||||
final FrameManager fm = new FrameManager(new Device());
|
||||
fm.setPresenter(new Presenter(fm, executor));
|
||||
fm.frame();
|
||||
final int before = fm.registeredFrameCount();
|
||||
|
||||
fm.presentSplash(1L, 10, 10);
|
||||
|
||||
assertEquals(1, executor.tasks.size(), "the splash blit must be enqueued on the window thread");
|
||||
assertEquals(before, fm.registeredFrameCount(), "the splash present must not touch the calling thread's frame");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.debug.flyby;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.rendering.FramePacer;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import cpw.mods.fml.common.gameevent.TickEvent;
|
||||
import net.minecraft.client.Minecraft;
|
||||
@@ -279,6 +280,7 @@ public final class FlybyRunner {
|
||||
this.frameCount = 0;
|
||||
this.lastFrameNs = 0L;
|
||||
this.runStartNs = System.nanoTime();
|
||||
FramePacer.beginStats();
|
||||
if (Tracy.ENABLED) {
|
||||
Tracy.message("flyby start route=" + this.route.id() + " length=" + this.runLength + this.route.lengthUnit() + " speed=" + this.route.speedOr(this.speed) + "b/t ticks=" + this.runTicks + " sdlgpu=" + SystemProperties.USE_SDL_GPU);
|
||||
}
|
||||
@@ -384,6 +386,7 @@ public final class FlybyRunner {
|
||||
|
||||
final String summary = this.summarise(elapsedNs, player);
|
||||
LOGGER.info(summary);
|
||||
LOGGER.info(FramePacer.endStats());
|
||||
if (mc.thePlayer != null) {
|
||||
mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + summary));
|
||||
}
|
||||
@@ -438,6 +441,13 @@ public final class FlybyRunner {
|
||||
if (this.isActive()) {
|
||||
LOGGER.info("Flyby cancelled");
|
||||
this.state = State.DONE;
|
||||
Tracy.sectionLeave(this.warmupSection);
|
||||
this.warmupSection = 0L;
|
||||
Tracy.sectionLeave(this.legSection);
|
||||
this.legSection = 0L;
|
||||
Tracy.sectionLeave(this.runSection);
|
||||
this.runSection = 0L;
|
||||
FramePacer.endStats();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.TracyBackend;
|
||||
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
@@ -25,16 +26,21 @@ public final class FramePacer {
|
||||
private static final Tracy.ZoneId Z_PACER_IDLE = Tracy.zoneId("pacerIdleWork", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_PACER_SLEEP = Tracy.zoneId("pacerSleep", Tracy.COLOR_SWAP);
|
||||
private static final long P_FRAME_GATE_US = Tracy.plotHandle("frameGateUs");
|
||||
private static final long P_SLACK_US = Tracy.plotHandle("pacer.slackUs");
|
||||
private static final long P_SPIN_US = Tracy.plotHandle("pacer.spinUs");
|
||||
private static final long P_CEILING_HZ = Tracy.plotHandle("pacer.ceilingHz");
|
||||
private static final long P_GATE_ACTIVE = Tracy.plotHandle("pacer.gateActive");
|
||||
|
||||
private static long deadline;
|
||||
private static long lastFrameTime;
|
||||
|
||||
private static boolean warnedUncapped;
|
||||
private static boolean pacedLastFrame;
|
||||
private static boolean statsActive;
|
||||
|
||||
private static VSyncMode lastVSyncMode = VSyncMode.ON;
|
||||
private static int lastCapHz;
|
||||
private static int lastRefreshHz;
|
||||
private static int lastCeilingHz;
|
||||
|
||||
private static IdleWork idleWork;
|
||||
private static PacerSleeper sleeper = newSleeper();
|
||||
@@ -47,7 +53,6 @@ public final class FramePacer {
|
||||
private static int loggedRefreshHz;
|
||||
private static int loggedCapHz;
|
||||
|
||||
private static boolean gateBlocking;
|
||||
private static long lastSeenGateEnd;
|
||||
|
||||
private FramePacer() {}
|
||||
@@ -60,6 +65,35 @@ public final class FramePacer {
|
||||
idleWork = work;
|
||||
}
|
||||
|
||||
public static boolean pacedLastFrame() {
|
||||
return pacedLastFrame;
|
||||
}
|
||||
|
||||
public static void beginStats() {
|
||||
sleeper.resetStats();
|
||||
statsActive = true;
|
||||
}
|
||||
|
||||
public static String endStats() {
|
||||
statsActive = false;
|
||||
final String summary = sleeper.summary(configLine());
|
||||
Tracy.message(summary);
|
||||
return summary;
|
||||
}
|
||||
|
||||
static boolean statsActive() {
|
||||
return statsActive;
|
||||
}
|
||||
|
||||
static String configLine(VSyncMode mode, int refreshHz, int capHz, int ceilingHz) {
|
||||
return "cfg=[" + mode + " refresh=" + refreshHz + "Hz cap=" + capHz + " ceiling=" + ceilingHz + "Hz]";
|
||||
}
|
||||
|
||||
private static String configLine() {
|
||||
return configLine(lastVSyncMode, lastRefreshHz, lastCapHz,
|
||||
pacingCeilingHz(lastVSyncMode, lastCapHz, lastRefreshHz, gateActive()));
|
||||
}
|
||||
|
||||
public static int plausibleRefreshHz(int hz) {
|
||||
return hz >= MIN_PLAUSIBLE_REFRESH_HZ && hz <= MAX_PLAUSIBLE_REFRESH_HZ ? hz : 0;
|
||||
}
|
||||
@@ -155,11 +189,6 @@ public final class FramePacer {
|
||||
}
|
||||
|
||||
public static long pace(int ceilingHz, Runnable renderAheadWait) {
|
||||
if (ceilingHz != lastCeilingHz) {
|
||||
lastCeilingHz = ceilingHz;
|
||||
deadline = 0L;
|
||||
}
|
||||
|
||||
long now = System.nanoTime();
|
||||
long phaseEnd;
|
||||
|
||||
@@ -167,6 +196,9 @@ public final class FramePacer {
|
||||
final long thresholdNanos = gateBlockThresholdNanos(targetNanos);
|
||||
long frameGateNanos = 0L;
|
||||
|
||||
pacedLastFrame = ceilingHz > 0;
|
||||
Tracy.plotInt(P_CEILING_HZ, ceilingHz);
|
||||
|
||||
if (ceilingHz > 0) {
|
||||
frameGateNanos = GLStateManager.lastFrameGateNanos();
|
||||
final long gateEnd = GLStateManager.lastFrameGateEndNanos();
|
||||
@@ -192,8 +224,11 @@ public final class FramePacer {
|
||||
now = phaseEnd;
|
||||
}
|
||||
|
||||
if (now < deadline) {
|
||||
final long preSleep = now;
|
||||
final long slackNanos = deadline - now;
|
||||
Tracy.plotInt(P_SLACK_US, slackNanos / 1000);
|
||||
|
||||
final boolean slept = now < deadline;
|
||||
if (slept) {
|
||||
Tracy.beginZone(Z_PACER_SLEEP);
|
||||
try {
|
||||
now = sleeper.sleepUntil(deadline, now);
|
||||
@@ -201,6 +236,10 @@ public final class FramePacer {
|
||||
Tracy.endZone();
|
||||
}
|
||||
}
|
||||
if (Tracy.ENABLED || statsActive) {
|
||||
sleeper.noteFrame(slackNanos, slept ? now - deadline : 0L, slept);
|
||||
Tracy.plotInt(P_SPIN_US, sleeper.lastSpinNanos / 1000);
|
||||
}
|
||||
} else {
|
||||
deadline = 0L;
|
||||
}
|
||||
@@ -215,15 +254,18 @@ public final class FramePacer {
|
||||
final long waitNanos = phaseEnd - now;
|
||||
now = phaseEnd;
|
||||
if (ceilingHz > 0 && waitNanos > thresholdNanos) deadline = now;
|
||||
gateBlocking = observeGate(ceilingHz > 0 ? Math.max(frameGateNanos, waitNanos) : 0L, thresholdNanos);
|
||||
final boolean gateBlocking = observeGate(ceilingHz > 0 ? Math.max(frameGateNanos, waitNanos) : 0L, thresholdNanos);
|
||||
Tracy.plotInt(P_GATE_ACTIVE, gateBlocking ? 1 : 0);
|
||||
|
||||
GLStateManager.pumpDisplayMessages();
|
||||
|
||||
if (pacingStatusChanged(gateBlocking, lastVSyncMode, lastRefreshHz, lastCapHz)) {
|
||||
final boolean blocking = settledBlocking;
|
||||
final String line = pacingLine(lastVSyncMode, lastRefreshHz, lastCapHz, blocking);
|
||||
if (vsyncNotHonoured(lastVSyncMode, blocking)) AngelicaMod.LOGGER.warn(line);
|
||||
final boolean notHonoured = vsyncNotHonoured(lastVSyncMode, blocking);
|
||||
if (notHonoured) AngelicaMod.LOGGER.warn(line);
|
||||
else AngelicaMod.LOGGER.info(line);
|
||||
Tracy.message(line, notHonoured ? TracyBackend.SEVERITY_WARNING : TracyBackend.SEVERITY_INFO);
|
||||
}
|
||||
|
||||
final long period = lastFrameTime == 0L ? 0L : now - lastFrameTime;
|
||||
|
||||
@@ -5,31 +5,50 @@ import java.util.function.LongSupplier;
|
||||
|
||||
final class PacerSleeper {
|
||||
|
||||
private static final long SPIN_FLOOR_NANOS = 100_000L;
|
||||
private static final long MIN_PARK_NANOS = 1_000_000L;
|
||||
private static final long MAX_OVERSHOOT_NANOS = 4_000_000L;
|
||||
private static final int SLOTS = 10;
|
||||
static final int SLOTS = 10;
|
||||
|
||||
static final long MIN_SPIN_FLOOR_NANOS = 20_000L;
|
||||
private static final long MAX_SPIN_FLOOR_NANOS = 500_000L;
|
||||
|
||||
private final LongSupplier clock;
|
||||
private final LongConsumer parker;
|
||||
private final boolean spinWait;
|
||||
private final long[] overshoots = new long[SLOTS];
|
||||
private long overshootSum;
|
||||
private long spinFloorNanos = MIN_SPIN_FLOOR_NANOS;
|
||||
private int offset;
|
||||
|
||||
long lastSpinNanos;
|
||||
int lastParks;
|
||||
|
||||
private long frames;
|
||||
private long slackSum;
|
||||
private long parkTotal;
|
||||
private long lateFrames;
|
||||
private long lateMax;
|
||||
|
||||
PacerSleeper(LongSupplier clock, LongConsumer parker, boolean spinWait) {
|
||||
this.clock = clock;
|
||||
this.parker = parker;
|
||||
this.spinWait = spinWait;
|
||||
}
|
||||
|
||||
long spinFloorNanos() {
|
||||
return spinFloorNanos;
|
||||
}
|
||||
|
||||
long sleepUntil(long deadlineNanos, long now) {
|
||||
resetLastFrame();
|
||||
|
||||
long spinFloor = spinFloorNanos;
|
||||
while (true) {
|
||||
final long remaining = deadlineNanos - now;
|
||||
if (remaining <= SPIN_FLOOR_NANOS) break;
|
||||
if (remaining <= spinFloor) break;
|
||||
if (Thread.currentThread().isInterrupted()) return clock.getAsLong();
|
||||
|
||||
long request = remaining - avgOvershootNanos() - SPIN_FLOOR_NANOS;
|
||||
long request = remaining - avgOvershootNanos() - spinFloor;
|
||||
if (request <= 0L) {
|
||||
if (remaining <= MIN_PARK_NANOS) break;
|
||||
request = MIN_PARK_NANOS;
|
||||
@@ -39,17 +58,51 @@ final class PacerSleeper {
|
||||
parker.accept(request);
|
||||
now = clock.getAsLong();
|
||||
record(Math.min(Math.max(0L, now - before - request), MAX_OVERSHOOT_NANOS));
|
||||
spinFloor = spinFloorNanos;
|
||||
lastParks++;
|
||||
}
|
||||
|
||||
if (spinWait) {
|
||||
final long spinStart = now;
|
||||
while (now < deadlineNanos) {
|
||||
Thread.yield();
|
||||
if (deadlineNanos - now > spinFloor) Thread.yield();
|
||||
else Thread.onSpinWait();
|
||||
now = clock.getAsLong();
|
||||
}
|
||||
lastSpinNanos = now - spinStart;
|
||||
}
|
||||
return now;
|
||||
}
|
||||
|
||||
void noteFrame(long slackNanos, long wakeLateNanos, boolean slept) {
|
||||
if (!slept) resetLastFrame();
|
||||
|
||||
frames++;
|
||||
slackSum += slackNanos;
|
||||
parkTotal += lastParks;
|
||||
if (slackNanos < 0L && -slackNanos > lateMax) lateMax = -slackNanos;
|
||||
if (slackNanos < 0L || wakeLateNanos > 0L) lateFrames++;
|
||||
}
|
||||
|
||||
private void resetLastFrame() {
|
||||
lastSpinNanos = 0L;
|
||||
lastParks = 0;
|
||||
}
|
||||
|
||||
void resetStats() {
|
||||
frames = 0L;
|
||||
slackSum = 0L;
|
||||
parkTotal = 0L;
|
||||
lateFrames = 0L;
|
||||
lateMax = 0L;
|
||||
}
|
||||
|
||||
String summary(String config) {
|
||||
if (frames == 0L) return "pacer: " + config + " no paced frames";
|
||||
return "pacer: " + config + " frames=" + frames + " parks=" + parkTotal + " slackUs avg=" + slackSum / frames / 1000L + " floorUs=" + spinFloorNanos / 1000L
|
||||
+ " overshootUs est=" + avgOvershootNanos() / 1000L + " lateFrames=" + lateFrames + " lateMaxUs=" + lateMax / 1000L;
|
||||
}
|
||||
|
||||
long avgOvershootNanos() {
|
||||
return overshootSum / SLOTS;
|
||||
}
|
||||
@@ -58,5 +111,12 @@ final class PacerSleeper {
|
||||
overshootSum += nanos - overshoots[offset];
|
||||
overshoots[offset] = nanos;
|
||||
offset = (offset + 1) % SLOTS;
|
||||
|
||||
long max = 0L;
|
||||
for (final long o : overshoots) {
|
||||
if (o > max) max = o;
|
||||
}
|
||||
final long jitter = max - avgOvershootNanos();
|
||||
spinFloorNanos = Math.max(MIN_SPIN_FLOOR_NANOS, Math.min(MAX_SPIN_FLOOR_NANOS, jitter));
|
||||
}
|
||||
}
|
||||
@@ -81,12 +81,13 @@ public abstract class MixinMinecraft {
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "runGameLoop", at = @At(value = "INVOKE", target = "Ljava/lang/Thread;yield()V", remap = false))
|
||||
private void angelica$limitFPS(CallbackInfo ci) {
|
||||
if (AngelicaMod.proxy == null) return;
|
||||
@WrapWithCondition(method = "runGameLoop", at = @At(value = "INVOKE", target = "Ljava/lang/Thread;yield()V", remap = false))
|
||||
private boolean angelica$limitFPS() {
|
||||
if (AngelicaMod.proxy == null) return true;
|
||||
|
||||
final int capHz = isFramerateLimitBelowMax() ? getLimitFramerate() : 0;
|
||||
AngelicaMod.proxy.putFrametime(FramePacer.endFrame(capHz, angelica$renderAheadWait));
|
||||
return !FramePacer.pacedLastFrame();
|
||||
}
|
||||
|
||||
@Redirect(method = {"startGame", "toggleFullscreen"}, at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;setVSyncEnabled(Z)V", remap = false))
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@@ -99,6 +100,14 @@ class FramePacerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void aPeriodChangeKeepsThePhaseInsteadOfRestartingIt() {
|
||||
final long previousDeadline = 1_000_000_000L;
|
||||
final long newTarget = 1_000_000_000L / 76;
|
||||
final long now = previousDeadline + 200_000L;
|
||||
assertEquals(previousDeadline + newTarget, FramePacer.nextDeadline(previousDeadline, now, newTarget));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallingMoreThanTwoPeriodsBehindResynchronises() {
|
||||
final long target = 16_666_666L;
|
||||
@@ -131,4 +140,13 @@ class FramePacerTest {
|
||||
assertNull(FramePacer.debugIndicator(VSyncMode.OFF, 144, 0, false));
|
||||
assertNull(FramePacer.debugIndicator(VSyncMode.OFF, 0, 0, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void endingTheStatsPutsThemBackToSleep() {
|
||||
FramePacer.beginStats();
|
||||
assertTrue(FramePacer.statsActive());
|
||||
|
||||
FramePacer.endStats();
|
||||
assertFalse(FramePacer.statsActive(), "a cancelled run must not leave the bookkeeping running forever");
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
class PacerSleeperTest {
|
||||
|
||||
private static final long MS = 1_000_000L;
|
||||
private static final long SPIN_FLOOR = 100_000L;
|
||||
private static final long COLD_FLOOR = PacerSleeper.MIN_SPIN_FLOOR_NANOS;
|
||||
|
||||
private static final class FakeTime {
|
||||
long now;
|
||||
@@ -53,20 +53,20 @@ class PacerSleeperTest {
|
||||
s.sleepUntil(10 * MS, 0L);
|
||||
|
||||
assertEquals(1, time.parks, "the request must be sized to the deadline, not to a fixed chunk");
|
||||
assertEquals(10 * MS - SPIN_FLOOR, time.lastRequest, "the park should land one spin floor short of the deadline");
|
||||
assertEquals(10 * MS - COLD_FLOOR, time.lastRequest, "the park should land one spin floor short of the deadline");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whatIsLeftForTheSpinNeverExceedsTheFloor() {
|
||||
void aWellBehavedTimerLeavesNoMoreThanTheFloorToSpin() {
|
||||
final FakeTime time = new FakeTime();
|
||||
time.lag = 20_000L;
|
||||
final PacerSleeper s = sleeper(time);
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
final long deadline = time.now + 13 * MS;
|
||||
final long floor = s.spinFloorNanos();
|
||||
final long woke = s.sleepUntil(deadline, time.now);
|
||||
assertTrue(deadline - woke <= SPIN_FLOOR,
|
||||
"left " + (deadline - woke) + "ns to spin, floor is " + SPIN_FLOOR);
|
||||
assertTrue(deadline - woke <= floor, "left " + (deadline - woke) + "ns to spin, floor is " + floor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ class PacerSleeperTest {
|
||||
final long woke = s.sleepUntil(10 * MS, 0L);
|
||||
|
||||
assertEquals(2, time.parks, "a short park must be followed by another, not by a spin");
|
||||
assertTrue(10 * MS - woke <= SPIN_FLOOR, "the second park should still land inside the floor");
|
||||
assertTrue(10 * MS - woke <= s.spinFloorNanos(), "the second park should still land inside the floor");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -171,12 +171,74 @@ class PacerSleeperTest {
|
||||
assertEquals(0, time.parks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void theSpinLoopFinishesTheFrameAndIsCounted() {
|
||||
final long step = 10_000L;
|
||||
final long[] clock = { 0L };
|
||||
final PacerSleeper s = new PacerSleeper(() -> clock[0] += step, nanos -> clock[0] += nanos, true);
|
||||
|
||||
final long woke = s.sleepUntil(5 * MS, 0L);
|
||||
|
||||
assertEquals(5 * MS, woke, "the spin must carry the frame to the deadline");
|
||||
assertEquals(1, s.lastParks);
|
||||
assertEquals(10_000L, s.lastSpinNanos, "the spin covers what the park left short");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aGapWiderThanTheFloorIsStillClosedWithoutParking() {
|
||||
final long step = 10_000L;
|
||||
final long overshoot = 2 * MS;
|
||||
final long[] clock = { 0L };
|
||||
final PacerSleeper s = new PacerSleeper(() -> clock[0] += step, nanos -> clock[0] += nanos + overshoot, true);
|
||||
|
||||
for (int i = 0; i < PacerSleeper.SLOTS; i++) {
|
||||
s.sleepUntil(clock[0] + 13 * MS, clock[0]);
|
||||
}
|
||||
assertTrue(s.avgOvershootNanos() > MS, "the park estimate must exceed the remaining gap for this branch to be reached");
|
||||
|
||||
final long deadline = clock[0] + MS;
|
||||
final long woke = s.sleepUntil(deadline, clock[0]);
|
||||
|
||||
assertEquals(0, s.lastParks, "a gap under one park quantum must not park again");
|
||||
assertTrue(s.lastSpinNanos > 0L, "the spin loop has to close a gap far wider than the floor");
|
||||
assertTrue(woke >= deadline, "woke " + woke + "ns short of deadline " + deadline);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aSteadyPlatformNeedsOnlyTheMinimumSpinFloor() {
|
||||
final FakeTime time = new FakeTime();
|
||||
time.lag = 300_000L;
|
||||
final PacerSleeper s = sleeper(time);
|
||||
|
||||
for (int i = 0; i < PacerSleeper.SLOTS; i++) {
|
||||
s.sleepUntil(time.now + 13 * MS, time.now);
|
||||
}
|
||||
|
||||
assertEquals(PacerSleeper.MIN_SPIN_FLOOR_NANOS, s.spinFloorNanos(), "identical overshoots are pure bias, not jitter, so the floor stays at its minimum");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aJitteryPlatformWidensTheSpinFloorToCoverTheWorstPark() {
|
||||
final FakeTime time = new FakeTime();
|
||||
time.lag = 100_000L;
|
||||
final PacerSleeper s = sleeper(time);
|
||||
|
||||
for (int i = 0; i < PacerSleeper.SLOTS - 1; i++) {
|
||||
s.sleepUntil(time.now + 13 * MS, time.now);
|
||||
}
|
||||
time.lag = 500_000L;
|
||||
s.sleepUntil(time.now + 13 * MS, time.now);
|
||||
|
||||
assertEquals(140_000L, s.avgOvershootNanos());
|
||||
assertEquals(500_000L - 140_000L, s.spinFloorNanos(), "the floor must cover the spread between the worst park and the mean, not the mean itself");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aDeadlineInsideTheFloorParksNotAtAll() {
|
||||
final FakeTime time = new FakeTime();
|
||||
final PacerSleeper s = sleeper(time);
|
||||
|
||||
final long woke = s.sleepUntil(SPIN_FLOOR, 0L);
|
||||
final long woke = s.sleepUntil(COLD_FLOOR, 0L);
|
||||
|
||||
assertEquals(0, time.parks);
|
||||
assertEquals(0L, woke);
|
||||
|
||||
Reference in new issue
Block a user