Compare commits

...
2 Commits
30 changed files with 524 additions and 278 deletions

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());
@@ -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);
@@ -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()) {
@@ -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();
}
}
@@ -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;
}
}
@@ -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);
@@ -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;
}
}
}
@@ -6,6 +6,4 @@ public interface ITileEntityBoundingBoxCache {
AxisAlignedBB angelica$getCachedRenderBoundingBox();
byte angelica$boundsClass();
byte angelica$passClass();
}
@@ -0,0 +1,3 @@
package com.gtnewhorizons.angelica.mixins.interfaces;
public interface OverridesGetDistanceFrom {}
@@ -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));
}
}
@@ -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();
}
}
}
@@ -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 {
@@ -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);