diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/BubbleColumnCache.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/BubbleColumnCache.java
new file mode 100644
index 00000000..25a33993
--- /dev/null
+++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/BubbleColumnCache.java
@@ -0,0 +1,43 @@
+package dev.loki.lovisual.features.module.modules.misc.optimize.particle;
+
+import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
+import net.minecraft.world.level.material.FluidState;
+
+/**
+ * Per-tick cache of {@code ClientLevel.getFluidState(pos)} results for
+ * {@code BubbleColumnUpParticle.tick}. Thousands of bubble particles share a
+ * few columns; each one re-checks its block's fluid every tick, which is a
+ * chunk-section lookup per call. This cache is keyed by {@code BlockPos.asLong()}
+ * and cleared once per tick (game-time change), so it never serves stale data
+ * across ticks.
+ *
+ *
Render-thread only (vanilla particle ticking is single-threaded). The
+ * cache is static because {@code BubbleColumnUpParticle} instances are created
+ * and destroyed constantly — per-particle state would be re-initialized too
+ * often to be useful.
+ *
+ *
Ported from {@code zako.opt.mixin.particle.BubbleColumnUpParticleMixin}
+ * (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
+ * The cache logic is in {@link TickCache} (a generic per-tick keyed cache),
+ * so it's unit-testable without Minecraft types.
+ */
+public enum BubbleColumnCache {
+ ;
+
+ private static final TickCache CACHE = new TickCache<>();
+
+ /** True when the cache is engaged this frame. */
+ public static boolean enabled() {
+ return OptimizeToggles.microOpts();
+ }
+
+ /** Look up a cached fluid state for the given block-pos key + tick. */
+ public static FluidState get(long posLong, long tick) {
+ return CACHE.get(posLong, tick);
+ }
+
+ /** Store a fluid state for the given block-pos key. */
+ public static void put(long posLong, FluidState fluid) {
+ CACHE.put(posLong, fluid);
+ }
+}
diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/TickCache.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/TickCache.java
new file mode 100644
index 00000000..3280a355
--- /dev/null
+++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/TickCache.java
@@ -0,0 +1,66 @@
+package dev.loki.lovisual.features.module.modules.misc.optimize.particle;
+
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+
+/**
+ * Generic per-tick cache keyed by {@code long}. The cache is cleared once per
+ * tick change (game-time change), so it never serves stale data across ticks.
+ *
+ * Used by {@link BubbleColumnCache} for {@code ClientLevel.getFluidState}
+ * results — thousands of bubble particles share a few columns; each one
+ * re-checks its block's fluid every tick. Generic on the value type so the
+ * logic is unit-testable without Minecraft types.
+ *
+ *
Render-thread only. The cache is static because particle instances are
+ * created and destroyed constantly — per-particle state would be re-initialized
+ * too often to be useful.
+ *
+ *
Ported from {@code zako.opt.mixin.particle.BubbleColumnUpParticleMixin}
+ * (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
+ */
+public final class TickCache {
+
+ private final Long2ObjectOpenHashMap map = new Long2ObjectOpenHashMap<>();
+ private long cachedTick = Long.MIN_VALUE;
+
+ /**
+ * Look up a cached value for the given key, on the given tick. If the tick
+ * differs from the cached tick, the cache is cleared first.
+ *
+ * @param key the packed position key (e.g. {@code BlockPos.asLong()})
+ * @param tick the current game tick
+ * @return the cached value, or {@code null} if not cached
+ */
+ public V get(long key, long tick) {
+ if (tick != cachedTick) {
+ map.clear();
+ cachedTick = tick;
+ }
+ return map.get(key);
+ }
+
+ /**
+ * Store a value for the given key, on the current tick. The cache must
+ * have been primed by a {@link #get} call first — that's where the tick
+ * check happens; this method assumes the cache is fresh.
+ */
+ public void put(long key, V value) {
+ map.put(key, value);
+ }
+
+ /** Number of cached entries on the current tick. */
+ public int size() {
+ return map.size();
+ }
+
+ /** The tick the cache currently holds data for. */
+ public long cachedTick() {
+ return cachedTick;
+ }
+
+ /** Reset the cache to empty (used by tests and on disable). */
+ public void reset() {
+ map.clear();
+ cachedTick = Long.MIN_VALUE;
+ }
+}
diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/world/OutlineTracker.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/world/OutlineTracker.java
new file mode 100644
index 00000000..22c36a0d
--- /dev/null
+++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/world/OutlineTracker.java
@@ -0,0 +1,81 @@
+package dev.loki.lovisual.features.module.modules.misc.optimize.world;
+
+import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
+
+/**
+ * Per-frame state for the entity-outline skip optimization. The vanilla
+ * pipeline clears the entity-outline target and runs {@code doEntityOutline}
+ * every frame even when no glowing entity is on screen. This tracker flips
+ * to {@code false} when the most recent frame had at least one glowing
+ * entity, and the {@code LevelRendererOutlineMixin} reads it to skip the
+ * clear + outline render on frames with no glowing entity.
+ *
+ * Render-thread only. The state is reset at the start of every frame
+ * ({@link #beginFrame()}), and {@link #markGlowing()} is called from the
+ * outline-target-clear {@code WrapOperation} when the renderer actually
+ * touches the outline target.
+ *
+ *
Ported from {@code zako.opt.mixin.entity.LevelRendererOutlineMixin}
+ * (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
+ * The state is lifted out of the mixin so the mixin stays a thin wrapper.
+ */
+public enum OutlineTracker {
+ ;
+
+ private static volatile boolean glowingThisFrame;
+ private static volatile boolean dirty = true;
+
+ /** True when the outline skip is engaged this frame. */
+ public static boolean enabled() {
+ return OptimizeToggles.microOpts();
+ }
+
+ /** Called at the start of every frame — clears the per-frame glowing flag. */
+ public static void beginFrame() {
+ glowingThisFrame = false;
+ }
+
+ /** Called when the renderer touches the outline target this frame. */
+ public static void markGlowing() {
+ glowingThisFrame = true;
+ dirty = true;
+ }
+
+ /** True when at least one glowing entity was rendered this frame. */
+ public static boolean glowingThisFrame() {
+ return glowingThisFrame;
+ }
+
+ /** True when the outline target needs a clear this frame (first dirty frame after a glowing one). */
+ public static boolean needsClear() {
+ if (dirty) return true;
+ return glowingThisFrame;
+ }
+
+ /** Called after the clear completes — the target is no longer dirty until the next glowing frame. */
+ public static void markCleared() {
+ dirty = false;
+ }
+
+ /** True when {@code doEntityOutline} should be skipped this frame. */
+ public static boolean shouldSkipOutlineRender() {
+ if (!enabled()) return false;
+ return !glowingThisFrame;
+ }
+
+ /** Test-only: reset all state. */
+ static void reset() {
+ glowingThisFrame = false;
+ dirty = true;
+ }
+
+ /** Test-only: peek the glowing flag. */
+ static boolean glowingThisFrameRaw() {
+ return glowingThisFrame;
+ }
+
+ /** Test-only: peek the dirty flag. */
+ static boolean dirtyRaw() {
+ return dirty;
+ }
+}
diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/TickCacheTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/TickCacheTest.java
new file mode 100644
index 00000000..a15e98b6
--- /dev/null
+++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/TickCacheTest.java
@@ -0,0 +1,114 @@
+package dev.loki.lovisual.features.module.modules.misc.optimize.particle;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Verifies {@link TickCache} — the generic per-tick keyed cache used by
+ * {@link BubbleColumnCache} and (later) other per-tick caches like
+ * {@code ParticleLightCache}. Pure logic, no Minecraft dependencies.
+ */
+public final class TickCacheTest {
+
+ @Test
+ void getReturnsNullOnEmptyCache() {
+ TickCache cache = new TickCache<>();
+ assertNull(cache.get(123L, 100L));
+ }
+
+ @Test
+ void putThenGetReturnsSameValue() {
+ TickCache cache = new TickCache<>();
+ cache.get(42L, 100L); // prime the tick
+ cache.put(42L, "fluid");
+ assertSame("fluid", cache.get(42L, 100L));
+ }
+
+ @Test
+ void getClearsCacheWhenTickChanges() {
+ TickCache cache = new TickCache<>();
+ cache.get(42L, 100L);
+ cache.put(42L, "fluid");
+ assertSame("fluid", cache.get(42L, 100L));
+ assertEquals(1, cache.size());
+ // Different tick — cache cleared.
+ assertNull(cache.get(42L, 101L));
+ assertEquals(0, cache.size());
+ }
+
+ @Test
+ void multipleKeysSameTick() {
+ TickCache cache = new TickCache<>();
+ cache.get(1L, 100L);
+ cache.put(1L, "a");
+ cache.get(2L, 100L);
+ cache.put(2L, "b");
+ cache.get(3L, 100L);
+ cache.put(3L, "c");
+ assertEquals(3, cache.size());
+ assertSame("a", cache.get(1L, 100L));
+ assertSame("b", cache.get(2L, 100L));
+ assertSame("c", cache.get(3L, 100L));
+ }
+
+ @Test
+ void resetClearsEverything() {
+ TickCache cache = new TickCache<>();
+ cache.get(1L, 100L);
+ cache.put(1L, "a");
+ cache.get(2L, 100L);
+ cache.put(2L, "b");
+ assertEquals(2, cache.size());
+ cache.reset();
+ assertEquals(0, cache.size());
+ assertEquals(Long.MIN_VALUE, cache.cachedTick());
+ }
+
+ @Test
+ void overwriteSameKey() {
+ TickCache cache = new TickCache<>();
+ cache.get(42L, 100L);
+ cache.put(42L, "first");
+ cache.put(42L, "second");
+ assertEquals(1, cache.size());
+ assertSame("second", cache.get(42L, 100L));
+ }
+
+ @Test
+ void cachedTickTracksLastGetTick() {
+ TickCache cache = new TickCache<>();
+ assertEquals(Long.MIN_VALUE, cache.cachedTick());
+ cache.get(1L, 100L);
+ assertEquals(100L, cache.cachedTick());
+ cache.get(2L, 200L);
+ assertEquals(200L, cache.cachedTick());
+ }
+
+ @Test
+ void nullValuesAreStored() {
+ TickCache cache = new TickCache<>();
+ cache.get(42L, 100L);
+ cache.put(42L, null);
+ // get returns null — same as "not cached". This is a known limitation:
+ // callers that want to cache null should use a sentinel.
+ assertNull(cache.get(42L, 100L));
+ // But the size is still 1, confirming the put went through.
+ assertEquals(1, cache.size());
+ }
+
+ @Test
+ void longKeyCollisionsHandled() {
+ TickCache cache = new TickCache<>();
+ long k1 = 1L << 40; // Large keys that don't collide in a hashmap.
+ long k2 = 2L << 40;
+ cache.get(k1, 100L);
+ cache.put(k1, "a");
+ cache.get(k2, 100L);
+ cache.put(k2, "b");
+ assertSame("a", cache.get(k1, 100L));
+ assertSame("b", cache.get(k2, 100L));
+ }
+}
diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/world/OutlineTrackerTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/world/OutlineTrackerTest.java
new file mode 100644
index 00000000..5896d7da
--- /dev/null
+++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/world/OutlineTrackerTest.java
@@ -0,0 +1,108 @@
+package dev.loki.lovisual.features.module.modules.misc.optimize.world;
+
+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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies {@link OutlineTracker} — per-frame state for the outline-skip
+ * optimization. Pure logic, no Minecraft dependencies.
+ */
+public final class OutlineTrackerTest {
+
+ @AfterEach
+ void reset() {
+ OptimizeToggles.reset();
+ OutlineTracker.reset();
+ }
+
+ @Test
+ void disabledWhenMicroOptsOff() {
+ OptimizeToggles.apply(
+ false, 0,
+ false, false, false,
+ false, 0,
+ false, false, false, false);
+ assertFalse(OutlineTracker.enabled());
+ // Even when glowing was set this frame, shouldSkipOutlineRender returns false
+ // when the toggle is off — the vanilla path runs unchanged.
+ OutlineTracker.markGlowing();
+ assertFalse(OutlineTracker.shouldSkipOutlineRender());
+ }
+
+ @Test
+ void skipWhenEnabledAndNoGlowing() {
+ OptimizeToggles.apply(
+ false, 0,
+ false, false, false,
+ false, 0,
+ false, false, false, true);
+ OutlineTracker.beginFrame();
+ assertTrue(OutlineTracker.shouldSkipOutlineRender(),
+ "Without glowing entities, doEntityOutline should be skipped");
+ }
+
+ @Test
+ void doNotSkipWhenGlowingThisFrame() {
+ OptimizeToggles.apply(
+ false, 0,
+ false, false, false,
+ false, 0,
+ false, false, false, true);
+ OutlineTracker.beginFrame();
+ OutlineTracker.markGlowing();
+ assertFalse(OutlineTracker.shouldSkipOutlineRender(),
+ "Glowing entity this frame — should not skip doEntityOutline");
+ assertTrue(OutlineTracker.glowingThisFrame());
+ }
+
+ @Test
+ void beginFrameClearsGlowingFlag() {
+ OptimizeToggles.apply(
+ false, 0,
+ false, false, false,
+ false, 0,
+ false, false, false, true);
+ OutlineTracker.markGlowing();
+ assertTrue(OutlineTracker.glowingThisFrame());
+ OutlineTracker.beginFrame();
+ assertFalse(OutlineTracker.glowingThisFrame());
+ }
+
+ @Test
+ void dirtyStartsTrue() {
+ OptimizeToggles.reset();
+ OutlineTracker.reset();
+ assertTrue(OutlineTracker.dirtyRaw());
+ assertTrue(OutlineTracker.needsClear());
+ }
+
+ @Test
+ void markClearedClearsDirty() {
+ OutlineTracker.markCleared();
+ assertFalse(OutlineTracker.dirtyRaw());
+ }
+
+ @Test
+ void resetRestoresDefaults() {
+ OutlineTracker.markGlowing();
+ OutlineTracker.markCleared();
+ OutlineTracker.reset();
+ assertFalse(OutlineTracker.glowingThisFrameRaw());
+ assertTrue(OutlineTracker.dirtyRaw());
+ }
+
+ @Test
+ void needsClearFollowsDirtyAndGlowing() {
+ // dirty=true (default) → needsClear true
+ OutlineTracker.markCleared();
+ // dirty=false, glowing=false → needsClear false (we'd need a glow to trigger a clear)
+ assertFalse(OutlineTracker.needsClear());
+ OutlineTracker.markGlowing();
+ // dirty=true again, glowing=true → needsClear true
+ assertTrue(OutlineTracker.needsClear());
+ }
+}