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