test(mod): JUnit 5 tests for Stage 1 optimization helpers
53 tests covering the pure-logic pieces: OptimizeToggles, LruMap, EntityLod, ParticleLod, Workers, HudWorth, SpawnerCache, StableText, CachedStack.
This commit is contained in:
parent
1a50a757b7
commit
494990ca4c
9 changed files with 953 additions and 0 deletions
|
|
@ -0,0 +1,143 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.misc.optimize.block;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
|
||||||
|
import net.minecraft.client.renderer.entity.state.EntityRenderState;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
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.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the {@link SpawnerCache} lookup logic. We can't construct a real
|
||||||
|
* {@code BaseSpawner} in the test JVM, so the test uses a hand-rolled mock
|
||||||
|
* that implements {@link TickCached} directly — the cache only relies on the
|
||||||
|
* interface, never on the {@code BaseSpawner} class itself.
|
||||||
|
*/
|
||||||
|
public final class SpawnerCacheTest {
|
||||||
|
|
||||||
|
/** Minimal TickCached implementation for testing. */
|
||||||
|
private static final class FakeSpawner implements TickCached {
|
||||||
|
Object cached;
|
||||||
|
long cachedTick = Long.MIN_VALUE;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object lovisual$cached() {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long lovisual$cachedTick() {
|
||||||
|
return cachedTick;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void lovisual$setCached(Object value, long tick) {
|
||||||
|
this.cached = value;
|
||||||
|
this.cachedTick = tick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void resetToggles() {
|
||||||
|
OptimizeToggles.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enabledReflectsOptimizeToggles() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
assertFalse(SpawnerCache.enabled());
|
||||||
|
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
true, false, false, false);
|
||||||
|
assertTrue(SpawnerCache.enabled());
|
||||||
|
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
assertFalse(SpawnerCache.enabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getReturnsNullForEmptyCache() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
true, false, false, false);
|
||||||
|
FakeSpawner spawner = new FakeSpawner();
|
||||||
|
assertNull(SpawnerCache.get(spawner, 100L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void putThenGetReturnsCachedValueForSameTick() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
true, false, false, false);
|
||||||
|
FakeSpawner spawner = new FakeSpawner();
|
||||||
|
EntityRenderState state = new EntityRenderState();
|
||||||
|
SpawnerCache.put(spawner, 42L, state);
|
||||||
|
assertSame(state, SpawnerCache.get(spawner, 42L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getReturnsNullForDifferentTick() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
true, false, false, false);
|
||||||
|
FakeSpawner spawner = new FakeSpawner();
|
||||||
|
EntityRenderState state = new EntityRenderState();
|
||||||
|
SpawnerCache.put(spawner, 42L, state);
|
||||||
|
assertNull(SpawnerCache.get(spawner, 43L));
|
||||||
|
assertNull(SpawnerCache.get(spawner, 41L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void putOverwritesPreviousCache() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
true, false, false, false);
|
||||||
|
FakeSpawner spawner = new FakeSpawner();
|
||||||
|
EntityRenderState first = new EntityRenderState();
|
||||||
|
EntityRenderState second = new EntityRenderState();
|
||||||
|
SpawnerCache.put(spawner, 1L, first);
|
||||||
|
SpawnerCache.put(spawner, 2L, second);
|
||||||
|
assertSame(second, SpawnerCache.get(spawner, 2L));
|
||||||
|
assertNull(SpawnerCache.get(spawner, 1L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cachedValuePersistsAcrossMultipleGets() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
true, false, false, false);
|
||||||
|
FakeSpawner spawner = new FakeSpawner();
|
||||||
|
EntityRenderState state = new EntityRenderState();
|
||||||
|
SpawnerCache.put(spawner, 100L, state);
|
||||||
|
// Multiple reads should all return the same value (no eviction).
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
assertSame(state, SpawnerCache.get(spawner, 100L));
|
||||||
|
}
|
||||||
|
assertEquals(100L, spawner.lovisual$cachedTick());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.misc.optimize.core;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public final class LruMapTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void accessOrderIsRespected() {
|
||||||
|
LruMap<String, Integer> map = new LruMap<>(3);
|
||||||
|
map.put("a", 1);
|
||||||
|
map.put("b", 2);
|
||||||
|
map.put("c", 3);
|
||||||
|
// Touch "a" — it should now be the most-recently-used.
|
||||||
|
Integer touched = map.get("a");
|
||||||
|
assertEquals(1, touched);
|
||||||
|
// Adding a fourth entry evicts the LRU, which is "b" (since "a" was just touched and "c" is newest).
|
||||||
|
map.put("d", 4);
|
||||||
|
assertNull(map.get("b"));
|
||||||
|
assertTrue(map.containsKey("a"));
|
||||||
|
assertTrue(map.containsKey("c"));
|
||||||
|
assertTrue(map.containsKey("d"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void capIsMaxSizePlusOne() {
|
||||||
|
// removeEldestEntry fires when size > maxSize, so we hold up to maxSize entries after the next put.
|
||||||
|
LruMap<Integer, String> map = new LruMap<>(2);
|
||||||
|
map.put(1, "one");
|
||||||
|
map.put(2, "two");
|
||||||
|
map.put(3, "three");
|
||||||
|
assertEquals(2, map.size());
|
||||||
|
assertNull(map.get(1));
|
||||||
|
assertEquals("two", map.get(2));
|
||||||
|
assertEquals("three", map.get(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void maxSizeAccessorMatchesConstructor() {
|
||||||
|
LruMap<String, String> map = new LruMap<>(1024);
|
||||||
|
assertEquals(1024, map.maxSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void iterationOrderFollowsAccessOrder() {
|
||||||
|
LruMap<String, Integer> map = new LruMap<>(5);
|
||||||
|
map.put("a", 1);
|
||||||
|
map.put("b", 2);
|
||||||
|
map.put("c", 3);
|
||||||
|
map.get("a");
|
||||||
|
List<String> keys = new ArrayList<>(map.keySet());
|
||||||
|
// After touching "a", iteration order should be b, c, a.
|
||||||
|
assertEquals(List.of("b", "c", "a"), keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void putReplacingExistingKeyDoesNotEvict() {
|
||||||
|
LruMap<String, Integer> map = new LruMap<>(2);
|
||||||
|
map.put("a", 1);
|
||||||
|
map.put("b", 2);
|
||||||
|
map.put("a", 100);
|
||||||
|
assertEquals(2, map.size());
|
||||||
|
assertEquals(100, map.get("a"));
|
||||||
|
// "b" should still be present — replacing a key's value doesn't grow size.
|
||||||
|
assertSame(2, map.get("b"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.misc.optimize.entity;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.lwjgl.system.MemoryStack;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies {@link CachedStack} — the ThreadLocal-free MemoryStack push helper.
|
||||||
|
*
|
||||||
|
* <p>Tests run on the test JVM's main thread; the first call populates the
|
||||||
|
* cache, subsequent calls from the same thread take the fast path.
|
||||||
|
*/
|
||||||
|
public final class CachedStackTest {
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void reset() {
|
||||||
|
// The cache is a single volatile — for the next test we just push something
|
||||||
|
// from this thread again so cachedThread() returns to the current thread.
|
||||||
|
// (Cross-test isolation isn't perfect because the cache is global, but
|
||||||
|
// the tests below assert observable invariants rather than internal state.)
|
||||||
|
CachedStack.push().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pushReturnsValidStack() {
|
||||||
|
try (MemoryStack stack = CachedStack.push()) {
|
||||||
|
assertNotNull(stack);
|
||||||
|
// A sanity check that the returned stack actually works.
|
||||||
|
long ptr = stack.nmalloc(8);
|
||||||
|
assertNotSame(0L, ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pushTwiceFromSameThreadReturnsSameStack() {
|
||||||
|
MemoryStack first;
|
||||||
|
try (MemoryStack s = CachedStack.push()) {
|
||||||
|
first = s;
|
||||||
|
}
|
||||||
|
MemoryStack second;
|
||||||
|
try (MemoryStack s = CachedStack.push()) {
|
||||||
|
second = s;
|
||||||
|
}
|
||||||
|
assertSame(first, second,
|
||||||
|
"Same-thread push should return the same underlying MemoryStack instance");
|
||||||
|
assertEquals(Thread.currentThread(), CachedStack.cachedThread());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pushFromDifferentThreadUpdatesCachedThread() throws InterruptedException {
|
||||||
|
// First call from this thread to populate the cache.
|
||||||
|
try (MemoryStack s = CachedStack.push()) {
|
||||||
|
assertNotNull(s);
|
||||||
|
}
|
||||||
|
assertEquals(Thread.currentThread(), CachedStack.cachedThread());
|
||||||
|
|
||||||
|
// Now push from a different thread — the cache should switch.
|
||||||
|
Thread other = new Thread(() -> {
|
||||||
|
try (MemoryStack s = CachedStack.push()) {
|
||||||
|
assertNotNull(s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
other.start();
|
||||||
|
other.join();
|
||||||
|
// After the other thread pushed, the cached thread is the other thread.
|
||||||
|
assertEquals(other, CachedStack.cachedThread());
|
||||||
|
assertNotNull(CachedStack.cachedStack());
|
||||||
|
|
||||||
|
// Restore the cache to this thread for the next test.
|
||||||
|
try (MemoryStack s = CachedStack.push()) {
|
||||||
|
assertNotNull(s);
|
||||||
|
}
|
||||||
|
assertEquals(Thread.currentThread(), CachedStack.cachedThread());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nestedPushesWorkLikeVanillaStack() {
|
||||||
|
// Vanilla MemoryStack supports nested stackPush/stackClose — CachedStack should too.
|
||||||
|
try (MemoryStack outer = CachedStack.push()) {
|
||||||
|
long outerPtr = outer.nmalloc(8);
|
||||||
|
try (MemoryStack inner = CachedStack.push()) {
|
||||||
|
long innerPtr = inner.nmalloc(8);
|
||||||
|
assertNotSame(outerPtr, innerPtr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.misc.optimize.entity;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
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.assertTrue;
|
||||||
|
|
||||||
|
public final class EntityLodTest {
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void resetToggles() {
|
||||||
|
OptimizeToggles.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void farReturnsFalseWhenLodDisabled() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 35,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
assertFalse(EntityLod.far(10_000_000));
|
||||||
|
assertFalse(EntityLod.farPlayer(10_000_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void farComparesSquaredDistance() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
true, 35,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
// d=35 -> d²=1225: 1225 is NOT greater than 1225, so not far.
|
||||||
|
assertFalse(EntityLod.far(35 * 35));
|
||||||
|
// d² just above threshold: should be far.
|
||||||
|
assertTrue(EntityLod.far(35 * 35 + 1));
|
||||||
|
// very close: not far.
|
||||||
|
assertFalse(EntityLod.far(0));
|
||||||
|
assertFalse(EntityLod.far(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void farThresholdSqMatchesDistance() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
true, 16,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
assertEquals(16 * 16, EntityLod.farThresholdSq(), 0.0001);
|
||||||
|
assertFalse(EntityLod.far(16 * 16));
|
||||||
|
assertTrue(EntityLod.far(16 * 16 + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void farPlayerMatchesFarInStage1() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
true, 35,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
// Stage 1 collapses player LOD into entity LOD — same toggle, same threshold.
|
||||||
|
for (double dSq : new double[]{0, 100, 1224, 1226, 10_000}) {
|
||||||
|
assertEquals(EntityLod.far(dSq), EntityLod.farPlayer(dSq),
|
||||||
|
"far and farPlayer disagree at dSq=" + dSq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void farRespectsRuntimeToggleChanges() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
true, 35,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
assertTrue(EntityLod.far(10_000));
|
||||||
|
// Toggle off mid-test — same distance should now be "not far".
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 35,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
assertFalse(EntityLod.far(10_000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.misc.optimize.hud;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public final class HudWorthTest {
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void reset() {
|
||||||
|
HudWorth.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void worthStartsTrue() {
|
||||||
|
assertTrue(HudWorth.worth());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void worthStaysTrueWhenFramesPerRefreshIsHigh() {
|
||||||
|
// Frame time 5 ms, refresh 16.6 ms (60 Hz) -> ~3.3 frames per refresh -> well above ON_ABOVE (2.0).
|
||||||
|
// Should stay "worth".
|
||||||
|
long refreshNs = 16_600_000L;
|
||||||
|
long now = 0;
|
||||||
|
for (int i = 0; i < 50; i++) {
|
||||||
|
now += 5_000_000L;
|
||||||
|
assertTrue(HudWorth.update(now, refreshNs));
|
||||||
|
}
|
||||||
|
assertTrue(HudWorth.worth());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void worthFlipsToFalseWhenFramesPerRefreshDropsBelowHysteresis() {
|
||||||
|
// Frame time ~= refresh time -> ~1 frame per refresh -> below OFF_BELOW (1.5).
|
||||||
|
long refreshNs = 16_600_000L;
|
||||||
|
long now = 0;
|
||||||
|
// Warm up the EMA at the high frame rate (worth stays true).
|
||||||
|
for (int i = 0; i < 50; i++) {
|
||||||
|
now += 5_000_000L;
|
||||||
|
HudWorth.update(now, refreshNs);
|
||||||
|
}
|
||||||
|
assertTrue(HudWorth.worth());
|
||||||
|
// Now drop the frame rate to one frame per refresh.
|
||||||
|
for (int i = 0; i < 50; i++) {
|
||||||
|
now += 16_600_000L;
|
||||||
|
HudWorth.update(now, refreshNs);
|
||||||
|
}
|
||||||
|
assertFalse(HudWorth.worth());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hysteresisAvoidsFlickerNearTheThreshold() {
|
||||||
|
// Sawtooth between 1.4 and 1.6 frames-per-refresh should NOT cause worth to flip back and forth.
|
||||||
|
// (Default initial state is worth=true, so 1.4 will drop it; the test asserts it stays dropped
|
||||||
|
// rather than flickering on every 1.6 frame.)
|
||||||
|
long refreshNs = 16_600_000L;
|
||||||
|
long now = 0;
|
||||||
|
// Drop to ~1.4 frames per refresh first (16.6 / ~12ms = 1.38).
|
||||||
|
for (int i = 0; i < 60; i++) {
|
||||||
|
now += 12_000_000L;
|
||||||
|
HudWorth.update(now, refreshNs);
|
||||||
|
}
|
||||||
|
assertFalse(HudWorth.worth());
|
||||||
|
// A few short frames (~10ms => 1.66 fps/refresh) should NOT flip worth back on (below ON_ABOVE=2.0).
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
now += 10_000_000L;
|
||||||
|
HudWorth.update(now, refreshNs);
|
||||||
|
}
|
||||||
|
assertFalse(HudWorth.worth());
|
||||||
|
// A longer sustained high frame rate (5ms => 3.32 fps/refresh) flips it back on.
|
||||||
|
for (int i = 0; i < 60; i++) {
|
||||||
|
now += 5_000_000L;
|
||||||
|
HudWorth.update(now, refreshNs);
|
||||||
|
}
|
||||||
|
assertTrue(HudWorth.worth());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateIgnoresExtremeGaps() {
|
||||||
|
// A 2-second gap (paused or main-thread stall) should NOT poison the EMA.
|
||||||
|
long refreshNs = 16_600_000L;
|
||||||
|
long now = 0;
|
||||||
|
for (int i = 0; i < 30; i++) {
|
||||||
|
now += 5_000_000L;
|
||||||
|
HudWorth.update(now, refreshNs);
|
||||||
|
}
|
||||||
|
double beforeGap = HudWorth.frameNs();
|
||||||
|
now += 2_000_000_000L; // 2 s gap
|
||||||
|
HudWorth.update(now, refreshNs);
|
||||||
|
// EMA should not have jumped to the gap value (the guard rejects dt >= 1e9 ns).
|
||||||
|
// It may have moved slightly due to the legitimate 5 ms frames, but not to ~2 s.
|
||||||
|
assertTrue(HudWorth.frameNs() < 100_000_000L,
|
||||||
|
"EMA should not absorb a 2-second pause, frameNs=" + HudWorth.frameNs());
|
||||||
|
assertTrue(HudWorth.frameNs() > 0, "EMA should be positive after warmup");
|
||||||
|
// Sanity: the gap reading didn't blow up frameNs to near 2 seconds.
|
||||||
|
assertTrue(HudWorth.frameNs() < beforeGap * 2 + 1_000_000L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.misc.optimize.particle;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public final class ParticleLodTest {
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void resetToggles() {
|
||||||
|
OptimizeToggles.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keepAllWhenLodDisabled() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
Object p = new Object();
|
||||||
|
assertTrue(ParticleLod.keep(p, 0));
|
||||||
|
assertTrue(ParticleLod.keep(p, 100));
|
||||||
|
assertTrue(ParticleLod.keep(p, 10_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keepAllWhenNear() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
true, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
Object p = new Object();
|
||||||
|
// Strictly under 16 blocks: always keep (NEAR_SQ = 16² = 256).
|
||||||
|
assertTrue(ParticleLod.keep(p, 0));
|
||||||
|
assertTrue(ParticleLod.keep(p, 16 * 16 - 1));
|
||||||
|
assertTrue(ParticleLod.keep(p, 255));
|
||||||
|
assertTrue(ParticleLod.keep(p, 255.999));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exactlyAtSixteenBlocksFallsThroughToMidRange() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
true, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
// At exactly 16² (the boundary), the particle is no longer "near" — its
|
||||||
|
// keep decision is delegated to the mid-range thinning. Over many
|
||||||
|
// particles, about half should survive.
|
||||||
|
int kept = 0;
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
Object p = new Object();
|
||||||
|
if (ParticleLod.keep(p, 16 * 16)) kept++;
|
||||||
|
}
|
||||||
|
assertTrue(kept >= 410 && kept <= 614,
|
||||||
|
"At 16² (boundary), about half should survive thinning, got " + kept + "/1024");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void thinningAtMidRangeKeepsAboutHalf() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
true, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
double midSq = 24 * 24; // Between 16² and 32².
|
||||||
|
Set<Boolean> seen = new HashSet<>();
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
Object p = new Object();
|
||||||
|
seen.add(ParticleLod.keep(p, midSq));
|
||||||
|
}
|
||||||
|
// We should see both keep and drop, with keep being roughly half (60-100).
|
||||||
|
assertTrue(seen.contains(true) && seen.contains(false),
|
||||||
|
"Mid-range thinning should keep some and drop some over 256 trials");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void thinningAtFarRangeKeepsAboutQuarter() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
true, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
double farSq = 48 * 48; // Beyond 32².
|
||||||
|
int kept = 0;
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
Object p = new Object();
|
||||||
|
if (ParticleLod.keep(p, farSq)) kept++;
|
||||||
|
}
|
||||||
|
// Expect roughly 256 (1024/4). Allow a wide band (200-320) because identityHashCode is not perfectly uniform.
|
||||||
|
assertTrue(kept >= 200 && kept <= 320,
|
||||||
|
"Far-range thinning should keep roughly 1/4 of particles, got " + kept + "/1024");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sameParticleKeepsStableChoiceAcrossCalls() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
true, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
Object p = new Object();
|
||||||
|
double farSq = 48 * 48;
|
||||||
|
boolean first = ParticleLod.keep(p, farSq);
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
assertTrue(first == ParticleLod.keep(p, farSq),
|
||||||
|
"Same particle should get the same keep decision every call");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void primitiveOverloadMatchesObjectOverload() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
true, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
Object p = new Object();
|
||||||
|
// particle at (50, 0, 0), camera at (0, 0, 0): distance² = 2500.
|
||||||
|
boolean objectApi = ParticleLod.keep(p, 50 * 50);
|
||||||
|
boolean primitiveApi = ParticleLod.keep(p, 50, 0, 0, 0, 0, 0);
|
||||||
|
assertTrue(objectApi == primitiveApi,
|
||||||
|
"Primitive overload should agree with object overload at same distance");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nearAlwaysKeepsEvenWhenParticleLodOn() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
true, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
Object p = new Object();
|
||||||
|
assertTrue(ParticleLod.keep(p, 15 * 15));
|
||||||
|
assertTrue(ParticleLod.keep(p, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void disablingParticleLodMidTestFlipsEverythingToKeep() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
true, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
Object p = new Object();
|
||||||
|
boolean first = ParticleLod.keep(p, 48 * 48);
|
||||||
|
// Toggle off — every distance should now keep.
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
assertTrue(ParticleLod.keep(p, 48 * 48));
|
||||||
|
assertTrue(ParticleLod.keep(p, 0));
|
||||||
|
// first is just whatever it was — we only care that the new state is "keep everything".
|
||||||
|
assertFalse(first == false && first != false, "dummy assertion to mark first read");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.misc.optimize.particle;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public final class WorkersTest {
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void shutdown() {
|
||||||
|
Workers.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void runDispatchesAllBlocks() {
|
||||||
|
int blocks = 32;
|
||||||
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
Throwable err = Workers.run(blocks, b -> counter.incrementAndGet());
|
||||||
|
assertNull(err);
|
||||||
|
assertEquals(blocks, counter.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void zeroBlocksIsNoop() {
|
||||||
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
Throwable err = Workers.run(0, b -> counter.incrementAndGet());
|
||||||
|
assertNull(err);
|
||||||
|
assertEquals(0, counter.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void negativeBlocksIsNoop() {
|
||||||
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
Throwable err = Workers.run(-1, b -> counter.incrementAndGet());
|
||||||
|
assertNull(err);
|
||||||
|
assertEquals(0, counter.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void firstFailureIsReturnedAndRemainingBlocksShortCircuit() {
|
||||||
|
int blocks = 16;
|
||||||
|
AtomicInteger completed = new AtomicInteger();
|
||||||
|
Throwable err = Workers.run(blocks, b -> {
|
||||||
|
if (b == 3) {
|
||||||
|
throw new IllegalStateException("block 3 failed");
|
||||||
|
}
|
||||||
|
completed.incrementAndGet();
|
||||||
|
});
|
||||||
|
assertNotNull(err);
|
||||||
|
assertTrue(err instanceof IllegalStateException, "error should be IllegalStateException, got " + err.getClass());
|
||||||
|
assertEquals("block 3 failed", err.getMessage());
|
||||||
|
// Some other blocks may have run before block 3 errored, but no more than the full count.
|
||||||
|
assertTrue(completed.get() <= blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void poolSizeIsAtLeastOne() {
|
||||||
|
assertTrue(Workers.poolSize() >= 1, "pool size should be at least 1, got " + Workers.poolSize());
|
||||||
|
assertTrue(Workers.poolSize() <= 8, "pool size should be at most 8, got " + Workers.poolSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parallelWorkCompletesAndCounterMatches() {
|
||||||
|
int blocks = 64;
|
||||||
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
Throwable err = Workers.run(blocks, b -> {
|
||||||
|
// Simulate a tiny unit of work. Swallow InterruptedException so the
|
||||||
|
// IntConsumer stays compatible with Workers.run's signature.
|
||||||
|
try {
|
||||||
|
Thread.sleep(1);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
counter.incrementAndGet();
|
||||||
|
});
|
||||||
|
assertNull(err);
|
||||||
|
assertEquals(blocks, counter.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.misc.optimize.text;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
|
||||||
|
import net.minecraft.util.FormattedCharSequence;
|
||||||
|
import net.minecraft.util.FormattedCharSink;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
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.assertTrue;
|
||||||
|
|
||||||
|
public final class StableTextTest {
|
||||||
|
|
||||||
|
/** A trivial FormattedCharSequence that just records whether accept() was called. */
|
||||||
|
private static final class Recorder implements FormattedCharSequence {
|
||||||
|
boolean accepted;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean accept(FormattedCharSink sink) {
|
||||||
|
accepted = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void resetTogglesAndTracked() {
|
||||||
|
OptimizeToggles.reset();
|
||||||
|
StableText.clearTracked();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void markReturnsStableTextInstance() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
Recorder inner = new Recorder();
|
||||||
|
FormattedCharSequence marked = StableText.mark(inner);
|
||||||
|
assertTrue(marked instanceof StableText,
|
||||||
|
"mark() should return a StableText wrapper");
|
||||||
|
assertEquals(1, StableText.trackedCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isStableReturnsTrueForMarkedText() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
Recorder inner = new Recorder();
|
||||||
|
FormattedCharSequence marked = StableText.mark(inner);
|
||||||
|
assertTrue(StableText.isStable(marked));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isStableReturnsFalseForUnmarkedText() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
Recorder plain = new Recorder();
|
||||||
|
assertFalse(StableText.isStable(plain));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isStableFastPathUsedWhenMicroOptsOn() {
|
||||||
|
// When microOpts is on, isStable uses instanceof — should return true for StableText
|
||||||
|
// wrappers even after clearing the tracked set (the set isn't consulted on the fast path).
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, true);
|
||||||
|
Recorder inner = new Recorder();
|
||||||
|
FormattedCharSequence marked = StableText.mark(inner);
|
||||||
|
StableText.clearTracked();
|
||||||
|
// Fast path: instanceof check, doesn't consult the set.
|
||||||
|
assertTrue(StableText.isStable(marked));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isStableSlowPathUsedWhenMicroOptsOff() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
Recorder inner = new Recorder();
|
||||||
|
FormattedCharSequence marked = StableText.mark(inner);
|
||||||
|
assertTrue(StableText.isStable(marked));
|
||||||
|
StableText.clearTracked();
|
||||||
|
// Slow path: after clearing the set, the wrapper is no longer "known stable".
|
||||||
|
assertFalse(StableText.isStable(marked));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void acceptDelegatesToInner() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
Recorder inner = new Recorder();
|
||||||
|
FormattedCharSequence marked = StableText.mark(inner);
|
||||||
|
// Pass a sink that just returns true; the marked wrapper should delegate to inner.
|
||||||
|
marked.accept((index, style, codePoint) -> true);
|
||||||
|
assertTrue(inner.accepted, "StableText.accept should delegate to the wrapped sequence");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multipleMarksAreTrackedSeparately() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
assertEquals(0, StableText.trackedCount());
|
||||||
|
StableText.mark(new Recorder());
|
||||||
|
StableText.mark(new Recorder());
|
||||||
|
StableText.mark(new Recorder());
|
||||||
|
assertEquals(3, StableText.trackedCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
package dev.loki.lovisual.render.engine.optimize;
|
||||||
|
|
||||||
|
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.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the {@link OptimizeToggles} snapshot holder. Mirrors the existing
|
||||||
|
* {@code OptimizeState} tests in shape: reset → assert default false → apply →
|
||||||
|
* assert each knob tracks independently → reset → assert default again.
|
||||||
|
*/
|
||||||
|
public final class OptimizeTogglesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stateIsCleanUntilApplied() {
|
||||||
|
OptimizeToggles.reset();
|
||||||
|
assertFalse(OptimizeToggles.entityLod());
|
||||||
|
assertFalse(OptimizeToggles.particleLod());
|
||||||
|
assertFalse(OptimizeToggles.spawnerCull());
|
||||||
|
assertFalse(OptimizeToggles.spawnerTickCache());
|
||||||
|
assertFalse(OptimizeToggles.blockEntityCache());
|
||||||
|
assertFalse(OptimizeToggles.preparedTextCache());
|
||||||
|
assertFalse(OptimizeToggles.microOpts());
|
||||||
|
assertEquals(0, OptimizeToggles.entityLodDistance());
|
||||||
|
assertEquals(0, OptimizeToggles.spawnerDistance());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void applyTracksEachKnobIndependently() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
true, 35,
|
||||||
|
false, true, false,
|
||||||
|
true, 16,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true);
|
||||||
|
assertTrue(OptimizeToggles.entityLod());
|
||||||
|
assertEquals(35, OptimizeToggles.entityLodDistance());
|
||||||
|
assertFalse(OptimizeToggles.particleLod());
|
||||||
|
assertTrue(OptimizeToggles.particleLight());
|
||||||
|
assertFalse(OptimizeToggles.particlePhysics());
|
||||||
|
assertTrue(OptimizeToggles.spawnerCull());
|
||||||
|
assertEquals(16, OptimizeToggles.spawnerDistance());
|
||||||
|
assertTrue(OptimizeToggles.spawnerTickCache());
|
||||||
|
assertFalse(OptimizeToggles.blockEntityCache());
|
||||||
|
assertTrue(OptimizeToggles.preparedTextCache());
|
||||||
|
assertTrue(OptimizeToggles.microOpts());
|
||||||
|
|
||||||
|
OptimizeToggles.reset();
|
||||||
|
assertFalse(OptimizeToggles.entityLod());
|
||||||
|
assertEquals(0, OptimizeToggles.entityLodDistance());
|
||||||
|
assertFalse(OptimizeToggles.spawnerCull());
|
||||||
|
assertFalse(OptimizeToggles.microOpts());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void applyWithAllOffMatchesReset() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false);
|
||||||
|
assertFalse(OptimizeToggles.entityLod());
|
||||||
|
assertFalse(OptimizeToggles.particleLod());
|
||||||
|
assertFalse(OptimizeToggles.spawnerCull());
|
||||||
|
assertFalse(OptimizeToggles.spawnerTickCache());
|
||||||
|
assertFalse(OptimizeToggles.blockEntityCache());
|
||||||
|
assertFalse(OptimizeToggles.preparedTextCache());
|
||||||
|
assertFalse(OptimizeToggles.microOpts());
|
||||||
|
assertEquals(0, OptimizeToggles.entityLodDistance());
|
||||||
|
assertEquals(0, OptimizeToggles.spawnerDistance());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue