mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-14 17:24:43 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2f597a73a | ||
|
|
553dec5b6d |
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-5
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.loading.fml.transformers;
|
||||
import com.gtnewhorizons.angelica.loading.AngelicaClientTweaker;
|
||||
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
|
||||
import com.gtnewhorizons.angelica.loading.shared.transformers.CeleritasBlockTransform;
|
||||
import com.gtnewhorizons.angelica.loading.shared.transformers.TileEntityMarkerTransform;
|
||||
import net.minecraft.launchwrapper.IClassTransformer;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
@@ -12,10 +13,12 @@ import org.objectweb.asm.tree.ClassNode;
|
||||
public class CeleritasBlockTransformer implements IClassTransformer {
|
||||
|
||||
private final CeleritasBlockTransform inner;
|
||||
private final TileEntityMarkerTransform tileEntities;
|
||||
private final String[] exclusions;
|
||||
|
||||
public CeleritasBlockTransformer() {
|
||||
this.inner = new CeleritasBlockTransform(AngelicaClientTweaker.isObfEnv());
|
||||
this.tileEntities = new TileEntityMarkerTransform(AngelicaClientTweaker.isObfEnv());
|
||||
this.exclusions = inner.getTransformerExclusions();
|
||||
}
|
||||
|
||||
@@ -31,14 +34,19 @@ public class CeleritasBlockTransformer implements IClassTransformer {
|
||||
}
|
||||
|
||||
final ClassReader cr = new ClassReader(basicClass);
|
||||
inner.trackBlockSubclasses(cr.getClassName(), cr.getSuperName());
|
||||
final String className = cr.getClassName();
|
||||
inner.trackBlockSubclasses(className, cr.getSuperName());
|
||||
tileEntities.track(className, cr.getSuperName());
|
||||
|
||||
if (!inner.shouldTransform(basicClass)) {
|
||||
return basicClass;
|
||||
final int markers = tileEntities.markersFor(className, basicClass);
|
||||
final byte[] marked = markers == 0 ? basicClass : tileEntities.addMarkers(basicClass, markers);
|
||||
|
||||
if (!inner.shouldTransform(marked)) {
|
||||
return marked;
|
||||
}
|
||||
|
||||
final ClassNode cn = new ClassNode();
|
||||
cr.accept(cn, 0);
|
||||
(markers == 0 ? cr : new ClassReader(marked)).accept(cn, 0);
|
||||
final boolean changed = inner.transformClassNode(transformedName, cn);
|
||||
if (changed) {
|
||||
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
@@ -47,7 +55,7 @@ public class CeleritasBlockTransformer implements IClassTransformer {
|
||||
AngelicaClassDump.dumpClass(transformedName, basicClass, bytes, this);
|
||||
return bytes;
|
||||
}
|
||||
return basicClass;
|
||||
return marked;
|
||||
}
|
||||
|
||||
}
|
||||
+24
-3
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.loading.rfb.transformers;
|
||||
|
||||
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
|
||||
import com.gtnewhorizons.angelica.loading.shared.transformers.CeleritasBlockTransform;
|
||||
import com.gtnewhorizons.angelica.loading.shared.transformers.TileEntityMarkerTransform;
|
||||
import com.gtnewhorizons.retrofuturabootstrap.api.ClassHeaderMetadata;
|
||||
import com.gtnewhorizons.retrofuturabootstrap.api.ClassNodeHandle;
|
||||
import com.gtnewhorizons.retrofuturabootstrap.api.ExtensibleClassLoader;
|
||||
@@ -9,15 +10,18 @@ import com.gtnewhorizons.retrofuturabootstrap.api.RfbClassTransformer;
|
||||
import org.intellij.lang.annotations.Pattern;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
public class RFBCeleritasBlockTransformer implements RfbClassTransformer {
|
||||
|
||||
private final CeleritasBlockTransform inner;
|
||||
private final TileEntityMarkerTransform tileEntities;
|
||||
|
||||
public RFBCeleritasBlockTransformer(boolean isObf) {
|
||||
inner = new CeleritasBlockTransform(isObf);
|
||||
tileEntities = new TileEntityMarkerTransform(isObf);
|
||||
}
|
||||
|
||||
@Pattern("[a-z0-9-]+")
|
||||
@@ -54,15 +58,32 @@ public class RFBCeleritasBlockTransformer implements RfbClassTransformer {
|
||||
return false;
|
||||
}
|
||||
|
||||
inner.trackBlockSubclasses(metadata.binaryThisName(), metadata.binarySuperName());
|
||||
return inner.shouldTransform(classNode.getOriginalBytes());
|
||||
final String thisName = metadata.binaryThisName();
|
||||
final String superName = metadata.binarySuperName();
|
||||
final byte[] originalBytes = classNode.getOriginalBytes();
|
||||
|
||||
inner.trackBlockSubclasses(thisName, superName);
|
||||
if (context == RfbClassTransformer.Context.LCL_WITH_TRANSFORMS) {
|
||||
tileEntities.track(thisName, superName);
|
||||
if (tileEntities.markersFor(thisName, originalBytes) != 0) return true;
|
||||
}
|
||||
return inner.shouldTransform(originalBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean transformClassIfNeeded(@NotNull ExtensibleClassLoader classLoader,
|
||||
@NotNull RfbClassTransformer.Context context, @Nullable Manifest manifest,
|
||||
@NotNull String className, @NotNull ClassNodeHandle classNode) {
|
||||
final boolean changed = inner.transformClassNode(className, classNode.getNode());
|
||||
final ClassNode node = classNode.getNode();
|
||||
boolean changed = false;
|
||||
if (context == RfbClassTransformer.Context.LCL_WITH_TRANSFORMS) {
|
||||
final int markers = tileEntities.markersFor(node.name, classNode.getOriginalBytes());
|
||||
if (markers != 0) {
|
||||
tileEntities.addMarkers(node, markers);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
changed |= inner.transformClassNode(className, node);
|
||||
if (changed) {
|
||||
classNode.computeMaxs();
|
||||
AngelicaClassDump.dumpRFBClass(className, classNode, this);
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package com.gtnewhorizons.angelica.loading.shared.transformers;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.asm.ClassConstantPoolParser;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Marks TileEntity subclasses that declare getDescriptionPacket, shouldRenderInPass or getDistanceFrom with a
|
||||
* corresponding interface, so runtime code can test with instanceof instead of reflection.
|
||||
*/
|
||||
public final class TileEntityMarkerTransform {
|
||||
|
||||
public static final int MARK_DESCRIPTION_PACKET = 1 << 0;
|
||||
public static final int MARK_SHOULD_RENDER_IN_PASS = 1 << 1;
|
||||
public static final int MARK_GET_DISTANCE_FROM = 1 << 2;
|
||||
|
||||
private static final String SENDS_DESCRIPTION_PACKET = "com/gtnewhorizons/angelica/mixins/interfaces/SendsDescriptionPacket";
|
||||
private static final String OVERRIDES_SHOULD_RENDER_IN_PASS = "com/gtnewhorizons/angelica/mixins/interfaces/OverridesShouldRenderInPass";
|
||||
private static final String OVERRIDES_GET_DISTANCE_FROM = "com/gtnewhorizons/angelica/mixins/interfaces/OverridesGetDistanceFrom";
|
||||
|
||||
private static final String TILE_ENTITY = "net/minecraft/tileentity/TileEntity";
|
||||
private static final int SCAN_FLAGS = ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
|
||||
|
||||
private record Marker(int bit, String method, String desc, String iface) {}
|
||||
|
||||
private final Marker[] markers;
|
||||
private final ClassConstantPoolParser prefilter;
|
||||
private final Set<String> tileEntities = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
public TileEntityMarkerTransform(boolean isObf) {
|
||||
this.markers = new Marker[] {
|
||||
new Marker(MARK_DESCRIPTION_PACKET, isObf ? "func_145844_m" : "getDescriptionPacket", "()Lnet/minecraft/network/Packet;", SENDS_DESCRIPTION_PACKET),
|
||||
new Marker(MARK_SHOULD_RENDER_IN_PASS, "shouldRenderInPass", "(I)Z", OVERRIDES_SHOULD_RENDER_IN_PASS),
|
||||
new Marker(MARK_GET_DISTANCE_FROM, isObf ? "func_145835_a" : "getDistanceFrom", "(DDD)D", OVERRIDES_GET_DISTANCE_FROM) };
|
||||
|
||||
final String[] methods = new String[markers.length];
|
||||
for (int i = 0; i < markers.length; i++) methods[i] = markers[i].method();
|
||||
this.prefilter = new ClassConstantPoolParser(methods);
|
||||
|
||||
}
|
||||
|
||||
private boolean isTileEntity(String className) {
|
||||
return className != null && (className.startsWith(TILE_ENTITY) || tileEntities.contains(className));
|
||||
}
|
||||
|
||||
public void track(String className, String superClassName) {
|
||||
if (className != null && isTileEntity(superClassName)) tileEntities.add(className);
|
||||
}
|
||||
|
||||
public int markersFor(String className, byte[] classBytes) {
|
||||
if (TILE_ENTITY.equals(className) || !isTileEntity(className) || !prefilter.find(classBytes)) return 0;
|
||||
|
||||
final MarkerScanner scanner = new MarkerScanner();
|
||||
new ClassReader(classBytes).accept(scanner, SCAN_FLAGS);
|
||||
return scanner.declared & ~scanner.alreadyPresent;
|
||||
}
|
||||
|
||||
public byte[] addMarkers(byte[] classBytes, int markers) {
|
||||
final ClassReader cr = new ClassReader(classBytes);
|
||||
final ClassWriter cw = new ClassWriter(cr, 0);
|
||||
cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
|
||||
|
||||
@Override
|
||||
public void visit(int version, int access, String name, String sig, String superName, String[] interfaces) {
|
||||
final String[] added = interfacesFor(markers);
|
||||
if (interfaces == null) {
|
||||
super.visit(version, access, name, sig, superName, added);
|
||||
return;
|
||||
}
|
||||
final String[] merged = Arrays.copyOf(interfaces, interfaces.length + added.length);
|
||||
System.arraycopy(added, 0, merged, interfaces.length, added.length);
|
||||
super.visit(version, access, name, sig, superName, merged);
|
||||
}
|
||||
}, 0);
|
||||
return cw.toByteArray();
|
||||
}
|
||||
|
||||
public void addMarkers(ClassNode cn, int markers) {
|
||||
Collections.addAll(cn.interfaces, interfacesFor(markers));
|
||||
}
|
||||
|
||||
private int bitOf(int access, String name, String desc) {
|
||||
if ((access & Opcodes.ACC_PUBLIC) == 0 || (access & Opcodes.ACC_STATIC) != 0) return 0;
|
||||
for (Marker m : markers) if (m.method().equals(name) && m.desc().equals(desc)) return m.bit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int bitOf(String interfaceName) {
|
||||
for (Marker m : markers) if (m.iface().equals(interfaceName)) return m.bit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
private String[] interfacesFor(int wanted) {
|
||||
final String[] names = new String[Integer.bitCount(wanted)];
|
||||
int next = 0;
|
||||
for (Marker m : markers) if ((wanted & m.bit()) != 0) names[next++] = m.iface();
|
||||
return names;
|
||||
}
|
||||
|
||||
private final class MarkerScanner extends ClassVisitor {
|
||||
|
||||
private int declared;
|
||||
private int alreadyPresent;
|
||||
|
||||
MarkerScanner() {
|
||||
super(Opcodes.ASM5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(int version, int access, String name, String sig, String superName, String[] interfaces) {
|
||||
if (interfaces == null) return;
|
||||
for (String iface : interfaces) alreadyPresent |= bitOf(iface);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String name, String desc, String sig, String[] exceptions) {
|
||||
declared |= bitOf(access, name, desc);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
-2
@@ -6,6 +6,4 @@ public interface ITileEntityBoundingBoxCache {
|
||||
AxisAlignedBB angelica$getCachedRenderBoundingBox();
|
||||
|
||||
byte angelica$boundsClass();
|
||||
|
||||
byte angelica$passClass();
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.gtnewhorizons.angelica.mixins.interfaces;
|
||||
|
||||
public interface OverridesGetDistanceFrom {}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.gtnewhorizons.angelica.mixins.interfaces;
|
||||
|
||||
public interface OverridesShouldRenderInPass {}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.gtnewhorizons.angelica.mixins.interfaces;
|
||||
|
||||
public interface SendsDescriptionPacket {}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
-82
@@ -1,7 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ByteOpenHashMap;
|
||||
import net.minecraft.launchwrapper.Launch;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
|
||||
@@ -17,25 +16,12 @@ public final class TileEntityRenderBoundsRegistry {
|
||||
public static final byte INFINITE = 2;
|
||||
public static final byte DYNAMIC = 3;
|
||||
|
||||
public static final byte PASS0_ONLY = 1;
|
||||
public static final byte PASS_OVERRIDES = 2;
|
||||
|
||||
private static final byte NOT_OVERRIDDEN = 1;
|
||||
private static final byte OVERRIDDEN = 2;
|
||||
|
||||
private static final Reference2ByteOpenHashMap<Class<? extends TileEntity>> classRegistry = new Reference2ByteOpenHashMap<>();
|
||||
private static final Reference2ByteOpenHashMap<Class<? extends TileEntity>> passClassRegistry = new Reference2ByteOpenHashMap<>();
|
||||
private static final Reference2ByteOpenHashMap<Class<? extends TileEntity>> distanceOverrideRegistry = new Reference2ByteOpenHashMap<>();
|
||||
|
||||
private static final Set<String> dynamicClassNames = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private static final String[] SHOULD_RENDER_IN_PASS_NAMES = { "shouldRenderInPass" };
|
||||
private static String[] distanceFromNames;
|
||||
|
||||
static {
|
||||
classRegistry.defaultReturnValue(UNKNOWN);
|
||||
passClassRegistry.defaultReturnValue(UNKNOWN);
|
||||
distanceOverrideRegistry.defaultReturnValue(UNKNOWN);
|
||||
}
|
||||
|
||||
private TileEntityRenderBoundsRegistry() {}
|
||||
@@ -73,72 +59,4 @@ public final class TileEntityRenderBoundsRegistry {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean overridesMethod(Class<?> clazz, Class<?> base, String[] candidateNames, Class<?>... params) {
|
||||
for (String name : candidateNames) {
|
||||
try {
|
||||
return clazz.getMethod(name, params).getDeclaringClass() != base;
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
} catch (Throwable t) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String[] distanceFromNames() {
|
||||
String[] names = distanceFromNames;
|
||||
if (names == null) {
|
||||
String resolved;
|
||||
try {
|
||||
resolved = Boolean.TRUE.equals(Launch.blackboard.get("fml.deobfuscatedEnvironment")) ? "getDistanceFrom" : "func_145835_a";
|
||||
} catch (Throwable t) {
|
||||
resolved = "getDistanceFrom";
|
||||
}
|
||||
distanceFromNames = names = new String[] { resolved };
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
public static byte classifyPass(TileEntity te) {
|
||||
final Class<? extends TileEntity> clazz = te.getClass();
|
||||
synchronized (passClassRegistry) {
|
||||
final byte cached = passClassRegistry.getByte(clazz);
|
||||
if (cached != UNKNOWN) return cached;
|
||||
}
|
||||
|
||||
final byte result = overridesMethod(clazz, TileEntity.class, SHOULD_RENDER_IN_PASS_NAMES, int.class)
|
||||
? PASS_OVERRIDES : PASS0_ONLY;
|
||||
|
||||
synchronized (passClassRegistry) {
|
||||
passClassRegistry.put(clazz, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean overridesGetDistanceFrom(Class<? extends TileEntity> clazz) {
|
||||
synchronized (distanceOverrideRegistry) {
|
||||
final byte cached = distanceOverrideRegistry.getByte(clazz);
|
||||
if (cached != UNKNOWN) return cached == OVERRIDDEN;
|
||||
}
|
||||
|
||||
final boolean overrides = overridesMethod(clazz, TileEntity.class, distanceFromNames(), double.class, double.class, double.class);
|
||||
|
||||
synchronized (distanceOverrideRegistry) {
|
||||
distanceOverrideRegistry.put(clazz, overrides ? OVERRIDDEN : NOT_OVERRIDDEN);
|
||||
}
|
||||
return overrides;
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
synchronized (classRegistry) {
|
||||
classRegistry.clear();
|
||||
}
|
||||
synchronized (passClassRegistry) {
|
||||
passClassRegistry.clear();
|
||||
}
|
||||
synchronized (distanceOverrideRegistry) {
|
||||
distanceOverrideRegistry.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -4,6 +4,7 @@ import com.gtnewhorizon.gtnhlib.blockpos.BlockPos;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.rendering.AngelicaRenderQueue;
|
||||
import com.gtnewhorizons.angelica.rendering.StateAwareTessellator;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.OverridesGetDistanceFrom;
|
||||
import com.gtnewhorizons.angelica.rendering.TileEntityRenderBoundsRegistry;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProvider;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder;
|
||||
@@ -190,7 +191,7 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
renderData.culledBlockEntities.add(tileEntity);
|
||||
AngelicaBuiltRenderSectionData.packSectionLocalBounds(culledBounds, aabb, minX, minY, minZ);
|
||||
double teDistSq;
|
||||
if (TileEntityRenderBoundsRegistry.overridesGetDistanceFrom(tileEntity.getClass())) {
|
||||
if (tileEntity instanceof OverridesGetDistanceFrom) {
|
||||
teDistSq = Double.POSITIVE_INFINITY;
|
||||
} else {
|
||||
try {
|
||||
|
||||
+2
-1
@@ -45,6 +45,7 @@ import com.gtnewhorizons.angelica.dynamiclights.IDynamicLightWorldRenderer;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.ITileEntityBoundingBoxCache;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.OverridesShouldRenderInPass;
|
||||
import com.gtnewhorizons.angelica.profiling.RenderClassTimings;
|
||||
import com.gtnewhorizons.angelica.proxy.ClientProxy;
|
||||
import com.gtnewhorizons.angelica.rendering.RenderingState;
|
||||
@@ -405,7 +406,7 @@ public class CeleritasWorldRenderer extends SimpleWorldRenderer<WorldClient, Ang
|
||||
try {
|
||||
for (int i = 0; i < frameTEs.size(); i++) {
|
||||
final TileEntity te = frameTEs.get(i);
|
||||
if (((ITileEntityBoundingBoxCache) te).angelica$passClass() == TileEntityRenderBoundsRegistry.PASS0_ONLY) {
|
||||
if (!(te instanceof OverridesShouldRenderInPass)) {
|
||||
dispatchTE(te, partialTicks);
|
||||
count++;
|
||||
continue;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.gtnewhorizons.angelica.utils;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
@@ -9,6 +10,7 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.AwaitingDescriptorTE;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.SendsDescriptionPacket;
|
||||
|
||||
public final class AwaitingDescriptor {
|
||||
|
||||
@@ -16,12 +18,12 @@ public final class AwaitingDescriptor {
|
||||
|
||||
private static final int WAIT_TICKS = 100;
|
||||
|
||||
private static final Map<Class<?>, Boolean> SENDS_DESCRIPTOR = new ConcurrentHashMap<>();
|
||||
private static final Set<Class<?>> NEVER_SENT = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
private AwaitingDescriptor() {}
|
||||
|
||||
public static void begin(TileEntity te) {
|
||||
if (!SENDS_DESCRIPTOR.computeIfAbsent(te.getClass(), AwaitingDescriptor::sendsDescriptor)) return;
|
||||
if (!(te instanceof SendsDescriptionPacket) || NEVER_SENT.contains(te.getClass())) return;
|
||||
|
||||
final AwaitingDescriptorTE tile = (AwaitingDescriptorTE) te;
|
||||
tile.angelica$setAwaitingDescriptor(true);
|
||||
@@ -43,16 +45,9 @@ public final class AwaitingDescriptor {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean sendsDescriptor(Class<?> cls) {
|
||||
try {
|
||||
return cls.getMethod("getDescriptionPacket").getDeclaringClass() != TileEntity.class;
|
||||
} catch (NoSuchMethodException e) {
|
||||
return false;
|
||||
private static void stopWaitingOn(Class<?> cls) {
|
||||
if (NEVER_SENT.add(cls)) {
|
||||
LOGGER.warn("{} declares getDescriptionPacket() but never sent one", cls.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static void stopWaitingOn(Class<?> cls) {
|
||||
if (!Boolean.TRUE.equals(SENDS_DESCRIPTOR.put(cls, Boolean.FALSE))) return;
|
||||
LOGGER.warn("{} declares getDescriptionPacket() but never sent one", cls.getName());
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -53,7 +53,6 @@ public abstract class MixinTileEntity implements ITileEntityBoundingBoxCache {
|
||||
@Unique private Block angelica$cachedBBBlockType = null;
|
||||
@Unique private AxisAlignedBB angelica$cachedRenderBB = null;
|
||||
@Unique private byte angelica$boundsClassCached = 0; // 0 = unchecked, otherwise STATIC/INFINITE/DYNAMIC
|
||||
@Unique private byte angelica$passClassCached = 0; // 0 = unchecked, otherwise PASS0_ONLY/PASS_OVERRIDES
|
||||
|
||||
/**
|
||||
* @author mitchej123
|
||||
@@ -122,11 +121,4 @@ public abstract class MixinTileEntity implements ITileEntityBoundingBoxCache {
|
||||
return angelica$boundsClassCached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte angelica$passClass() {
|
||||
if (angelica$passClassCached == 0) {
|
||||
angelica$passClassCached = TileEntityRenderBoundsRegistry.classifyPass((TileEntity) (Object) this);
|
||||
}
|
||||
return angelica$passClassCached;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class OverridesMethodTest {
|
||||
|
||||
static class Base {
|
||||
public double dist(double x, double y, double z) { return 0; }
|
||||
public boolean pass(int p) { return p == 0; }
|
||||
}
|
||||
|
||||
static class Plain extends Base {}
|
||||
|
||||
static class Direct extends Base {
|
||||
@Override public double dist(double x, double y, double z) { return 1; }
|
||||
}
|
||||
|
||||
static class Inherited extends Direct {}
|
||||
|
||||
interface DefaultDist {
|
||||
default double dist(double x, double y, double z) { return 2; }
|
||||
}
|
||||
|
||||
static class WithInterfaceDefault extends Base implements DefaultDist {}
|
||||
|
||||
private static final String[] DIST = { "dist" };
|
||||
private static final String[] PASS = { "pass" };
|
||||
private static final Class<?>[] DIST_PARAMS = { double.class, double.class, double.class };
|
||||
|
||||
@Test
|
||||
void baseDeclarationIsNotAnOverride() {
|
||||
assertFalse(TileEntityRenderBoundsRegistry.overridesMethod(Base.class, Base.class, DIST, DIST_PARAMS));
|
||||
assertFalse(TileEntityRenderBoundsRegistry.overridesMethod(Plain.class, Base.class, DIST, DIST_PARAMS));
|
||||
assertFalse(TileEntityRenderBoundsRegistry.overridesMethod(Plain.class, Base.class, PASS, int.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void directOverrideDetected() {
|
||||
assertTrue(TileEntityRenderBoundsRegistry.overridesMethod(Direct.class, Base.class, DIST, DIST_PARAMS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedOverrideDetected() {
|
||||
assertTrue(TileEntityRenderBoundsRegistry.overridesMethod(Inherited.class, Base.class, DIST, DIST_PARAMS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void classMethodBeatsInterfaceDefault() {
|
||||
assertFalse(TileEntityRenderBoundsRegistry.overridesMethod(WithInterfaceDefault.class, Base.class, DIST, DIST_PARAMS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unresolvableNameTreatedAsOverriding() {
|
||||
assertTrue(TileEntityRenderBoundsRegistry.overridesMethod(Plain.class, Base.class, new String[] { "func_145835_a" }, DIST_PARAMS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrongParametersTreatedAsOverriding() {
|
||||
assertTrue(TileEntityRenderBoundsRegistry.overridesMethod(Plain.class, Base.class, DIST, double.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void firstMatchingCandidateNameWins() {
|
||||
assertTrue(TileEntityRenderBoundsRegistry.overridesMethod(Direct.class, Base.class, new String[] { "func_145835_a", "dist" }, DIST_PARAMS));
|
||||
assertFalse(TileEntityRenderBoundsRegistry.overridesMethod(Plain.class, Base.class, new String[] { "func_145835_a", "dist" }, DIST_PARAMS));
|
||||
}
|
||||
}
|
||||
@@ -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