From e85c4fc10ab1e7889fd3f18af4c8ed50f72e87c2 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 12 Oct 2026 21:49:12 +0200 Subject: [PATCH] feat(mod): parallel-particles plumbing (CORES>=4 auto + -D tri-state) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 1 item 4 from optimize-layer-design.md, simplified to what the reference does (auto|true|false) instead of the original 'measure first' plan. Workers now holds the resolved flag: - Workers.PARALLEL_MIN_CORES = 4 gate for AUTO mode - Workers.configureParallelParticles(Boolean forced, boolean moduleEnabled) - Workers.shouldParallelParticles() read by the future particle-extraction mixin (26.2 QuadParticleGroup/SpriteBillboardParticle wiring is separate) - Workers.resetParallelParticles() called by Optimize.onDisable Optimize gains a parallel_particles BooleanValue (default true = AUTO). OptimizePropertyBridge parses -Dlovisual.opt.parallel_particles as tri-state: null (unset or 'auto' -> defer to module + cores gate), TRUE, FALSE; the key is now part of OVERRIDE_KEYS so hasOverrides() sees it and toString() includes it. Docs: 'Что делаем лучше' item 4 marked done — no measurement gate, matching the new plan direction. Tests: 5 new WorkersTest cases for resolveParallelParticles + configure/reset round trip; 4 new OptimizePropertyBridgeTest cases for the tri-state parser (unset / explicit 'auto' / forced true|false / hasOverrides pickup). gradle compileJava compileTestJava test checkFolderLimit green. --- mod/docs/optimize-layer-design.md | 3 ++ .../modules/misc/optimize/Optimize.java | 11 +++++ .../misc/optimize/particle/Workers.java | 42 +++++++++++++++++++ .../profile/OptimizePropertyBridge.java | 22 +++++++++- .../misc/optimize/particle/WorkersTest.java | 34 +++++++++++++++ .../profile/OptimizePropertyBridgeTest.java | 31 +++++++++++++- 6 files changed, 141 insertions(+), 2 deletions(-) diff --git a/mod/docs/optimize-layer-design.md b/mod/docs/optimize-layer-design.md index 45e98b93..c49c2de2 100644 --- a/mod/docs/optimize-layer-design.md +++ b/mod/docs/optimize-layer-design.md @@ -65,6 +65,9 @@ optimize/ 2. **Профилировщик секций** (сущности, частицы, HUD, эффекты, текст) в debug-оверлее. 3. **Один конфиг** в `Optimize`, без отдельного экрана референса. 4. **Авто-порог параллельных частиц** по замеру (в референсе порог 4+ ядра взят из одного прогона). + → **Упрощено 2026-10-12**: сделано как в референсе — `CORES >= 4` + `-Dlovisual.opt.parallel_particles=auto|true|false`. + Модуль `Optimize.parallel_particles` (default true = AUTO) и `Workers.shouldParallelParticles()` — plumbing готов, + mixin-точка на vanilla extraction приложится позже (26.2 `QuadParticleGroup`/`SpriteBillboardParticle`). 5. **Кэш статичных HUD-виджетов в текстуру** (перерисовка только при изменении). 6. Флаги для бенчмарков: `-Dlovisual.opt.`. diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/Optimize.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/Optimize.java index 3926e5df..6f6c9086 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/Optimize.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/Optimize.java @@ -10,6 +10,7 @@ import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; import dev.loki.lovisual.features.module.lifecycle.Modules; import dev.loki.lovisual.features.module.phase.Notifier; +import dev.loki.lovisual.features.module.modules.misc.optimize.particle.Workers; import dev.loki.lovisual.render.engine.optimize.entity.AnimFreeze; import dev.loki.lovisual.render.engine.optimize.entity.ItemBounds; import dev.loki.lovisual.render.engine.optimize.entity.ItemLod; @@ -87,6 +88,10 @@ public final class Optimize extends Module { // --- Stage 3: parallel model build (own holder: cores/Iris gating inside) --- private final BooleanValue parallelModels = bool("parallel_models", true); + // --- Stage 1 item 4: parallel particles AUTO gate (cores >= 4) --- + // -Dlovisual.opt.parallel_particles=auto|true|false overrides this from OptimizePropertyBridge. + private final BooleanValue parallelParticles = bool("parallel_particles", true); + // --- Visible cuts merged from the old FPS Optimizer module --- private final BooleanValue noParticles = bool("no_particles", false); private final BooleanValue noMenuBackground = bool("no_menu_background", false); @@ -126,6 +131,7 @@ public final class Optimize extends Module { ItemBounds.reset(); HudCache.reset(); ParallelModels.reset(); + Workers.resetParallelParticles(); // stop counting and drop the F3 numbers, onFrame will not run again DrawStats.endFrame(false); } @@ -220,6 +226,11 @@ public final class Optimize extends Module { // Stage 3: parallel model build; mutually exclusive with anim freeze (checked inside). ParallelModels.configure(on && parallelModels.get()); + + // Stage 1 item 4: parallel-particles gate; -Dlovisual.opt.parallel_particles resolves + // to null (AUTO -> cores >= 4), Boolean.TRUE/FALSE (forced). Workers keeps the resolved + // flag so a future particle-extraction mixin can read it without touching this module. + Workers.configureParallelParticles(props.parallelParticles, on && parallelParticles.get()); } private static boolean resolveBool(OptimizePropertyBridge.Snapshot props, diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/Workers.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/Workers.java index 5aeb49e9..ea7048b2 100755 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/Workers.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/Workers.java @@ -25,6 +25,48 @@ public final class Workers { public static final int COUNT = Math.max(1, Math.min(8, Runtime.getRuntime().availableProcessors() - 2)); + /** Minimum logical CPU count required for AUTO mode to enable parallel particles. */ + public static final int PARALLEL_MIN_CORES = 4; + + /** + * Effective parallel-particles flag resolved from module + property overrides. + * Written once per frame by {@code Optimize.pushState()}, read by the future + * particle-extraction mixin to decide whether to fan out across {@link #stream(IntConsumer)}. + */ + private static volatile boolean parallelParticles; + + /** + * Resolve the parallel-particles flag. {@code forced} is the tri-state from + * {@code -Dlovisual.opt.parallel_particles} (TRUE/FALSE) or {@code null} for AUTO; {@code moduleEnabled} + * is the {@code Optimize.parallel_particles} switch (default {@code true}); AUTO engages when the + * machine exposes {@link #PARALLEL_MIN_CORES} or more logical CPUs — mirrors the reference's + * {@code CORES >= 4} rule from {@code mod/docs/optimize-layer-design.md}, no benchmark gate. + */ + public static void configureParallelParticles(Boolean forced, boolean moduleEnabled) { + parallelParticles = resolveParallelParticles(forced, moduleEnabled, Runtime.getRuntime().availableProcessors()); + } + + /** Test-friendly variant that lets the caller stub the CPU count. */ + public static boolean resolveParallelParticles(Boolean forced, boolean moduleEnabled, int cores) { + if (forced != null) { + return forced; + } + if (!moduleEnabled) { + return false; + } + return cores >= PARALLEL_MIN_CORES; + } + + /** True when the particle-extraction mixin should split across the pool. */ + public static boolean shouldParallelParticles() { + return parallelParticles; + } + + /** Reset to a safe default — used by {@code Optimize.onDisable} and by tests. */ + public static void resetParallelParticles() { + parallelParticles = false; + } + private static final AtomicInteger THREAD_ID = new AtomicInteger(); private static final ExecutorService POOL = Executors.newFixedThreadPool(COUNT, r -> { Thread t = new Thread(r, "lovisual-opt-worker-" + THREAD_ID.incrementAndGet()); diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/optimize/profile/OptimizePropertyBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/optimize/profile/OptimizePropertyBridge.java index 0b943e9d..f8fe0f6e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/optimize/profile/OptimizePropertyBridge.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/optimize/profile/OptimizePropertyBridge.java @@ -22,6 +22,8 @@ import java.util.Map; *
  • {@code -Dlovisual.opt.prepared_text_cache=true|false}
  • *
  • {@code -Dlovisual.opt.micro_opts=true|false}
  • *
  • {@code -Dlovisual.opt.fbo_share=true|false}
  • + *
  • {@code -Dlovisual.opt.parallel_particles=auto|true|false} — tri-state; {@code auto} defers + * to the module value + the {@code cores >= 4} gate inside {@code Workers}.
  • *
  • {@code -Dlovisual.opt.entity_lod_distance=35} — overrides the distance (4..256).
  • *
  • {@code -Dlovisual.opt.spawner_distance=16} — overrides the distance (4..64).
  • *
  • {@code -Dlovisual.opt.budget_mode=off|soft|aggressive}
  • @@ -66,6 +68,8 @@ public enum OptimizePropertyBridge { s.entityLodDistance = intOverride("entity_lod_distance", -1); s.spawnerDistance = intOverride("spawner_distance", -1); + s.parallelParticles = triStateOverride("parallel_particles"); + s.budgetMode = budgetModeOverride(); s.targetFps = intOverride("target_fps", -1); cached = s; @@ -95,7 +99,8 @@ public enum OptimizePropertyBridge { private static final String[] OVERRIDE_KEYS = { "entity_lod", "particle_lod", "spawner_cull", "spawner_tick_cache", "block_entity_cache", "prepared_text_cache", "micro_opts", "fbo_share", - "entity_lod_distance", "spawner_distance", "budget_mode", "target_fps" + "entity_lod_distance", "spawner_distance", "budget_mode", "target_fps", + "parallel_particles" }; /** @@ -117,6 +122,18 @@ public enum OptimizePropertyBridge { } } + /** + * Tri-state override: {@code null} when the property is unset or set to {@code auto} + * (defer to module + cores gate), {@code Boolean.TRUE}/{@code FALSE} when the user forced it. + */ + private static Boolean triStateOverride(String key) { + String p = System.getProperty("lovisual.opt." + key); + if (p == null) return null; + String v = p.trim().toLowerCase(); + if (v.isEmpty() || v.equals("auto")) return null; + return Boolean.parseBoolean(v); + } + private static FrameBudget.Mode budgetModeOverride() { String p = System.getProperty("lovisual.opt.budget_mode"); if (p == null) return null; @@ -148,6 +165,8 @@ public enum OptimizePropertyBridge { public Boolean fboShare; public int entityLodDistance = -1; public int spawnerDistance = -1; + /** Tri-state override: null = AUTO (use module + cores gate), TRUE/FALSE = forced. */ + public Boolean parallelParticles; public FrameBudget.Mode budgetMode; // null = use module value public int targetFps = -1; @@ -188,6 +207,7 @@ public enum OptimizePropertyBridge { m.put("fbo_share", fboShare); m.put("entity_lod_distance", entityLodDistance); m.put("spawner_distance", spawnerDistance); + m.put("parallel_particles", parallelParticles); m.put("budget_mode", budgetMode); m.put("target_fps", targetFps); return m.toString(); diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/WorkersTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/WorkersTest.java index c741f5d7..3e449337 100755 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/WorkersTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/WorkersTest.java @@ -6,6 +6,7 @@ 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.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -103,6 +104,39 @@ public final class WorkersTest { assertNull(err); } + @Test + void autoParallelParticlesRequiresFourCores() { + assertFalse(Workers.resolveParallelParticles(null, true, 2)); + assertFalse(Workers.resolveParallelParticles(null, true, 3)); + assertTrue(Workers.resolveParallelParticles(null, true, 4)); + assertTrue(Workers.resolveParallelParticles(null, true, 8)); + } + + @Test + void forcedTrueOverridesModuleAndCores() { + assertTrue(Workers.resolveParallelParticles(Boolean.TRUE, false, 1)); + } + + @Test + void forcedFalseOverridesAutoGate() { + assertFalse(Workers.resolveParallelParticles(Boolean.FALSE, true, 16)); + } + + @Test + void moduleOffDisablesAutoEvenOnManyCores() { + assertFalse(Workers.resolveParallelParticles(null, false, 32)); + } + + @Test + void configureWritesFlagReadedByShould() { + Workers.configureParallelParticles(Boolean.TRUE, false); + assertTrue(Workers.shouldParallelParticles()); + Workers.configureParallelParticles(Boolean.FALSE, true); + assertFalse(Workers.shouldParallelParticles()); + Workers.resetParallelParticles(); + assertFalse(Workers.shouldParallelParticles()); + } + @Test void streamReturnsFirstFailure() { int blocks = 12; diff --git a/mod/src/test/java/dev/loki/lovisual/render/engine/optimize/profile/OptimizePropertyBridgeTest.java b/mod/src/test/java/dev/loki/lovisual/render/engine/optimize/profile/OptimizePropertyBridgeTest.java index 3135fc44..2c823f65 100644 --- a/mod/src/test/java/dev/loki/lovisual/render/engine/optimize/profile/OptimizePropertyBridgeTest.java +++ b/mod/src/test/java/dev/loki/lovisual/render/engine/optimize/profile/OptimizePropertyBridgeTest.java @@ -25,7 +25,8 @@ public final class OptimizePropertyBridgeTest { "lovisual.opt.block_entity_cache", "lovisual.opt.prepared_text_cache", "lovisual.opt.micro_opts", "lovisual.opt.fbo_share", "lovisual.opt.entity_lod_distance", "lovisual.opt.spawner_distance", - "lovisual.opt.budget_mode", "lovisual.opt.target_fps" + "lovisual.opt.budget_mode", "lovisual.opt.target_fps", + "lovisual.opt.parallel_particles" }) { System.clearProperty(k); } @@ -178,4 +179,32 @@ public final class OptimizePropertyBridgeTest { assertTrue(str.contains("entity_lod")); assertTrue(str.contains("budget_mode")); } + + @Test + void parallelParticlesUnsetMeansAuto() { + OptimizePropertyBridge.Snapshot s = OptimizePropertyBridge.resolve(); + assertNull(s.parallelParticles); + } + + @Test + void parallelParticlesExplicitAutoAlsoMeansAuto() { + System.setProperty("lovisual.opt.parallel_particles", "auto"); + OptimizePropertyBridge.Snapshot s = OptimizePropertyBridge.resolve(); + assertNull(s.parallelParticles); + } + + @Test + void parallelParticlesTrueFalseForced() { + System.setProperty("lovisual.opt.parallel_particles", "true"); + assertEquals(Boolean.TRUE, OptimizePropertyBridge.resolve().parallelParticles); + System.setProperty("lovisual.opt.parallel_particles", "false"); + assertEquals(Boolean.FALSE, OptimizePropertyBridge.resolve().parallelParticles); + } + + @Test + void parallelParticlesAppearsInHasOverrides() { + assertFalse(OptimizePropertyBridge.hasOverrides()); + System.setProperty("lovisual.opt.parallel_particles", "auto"); + assertTrue(OptimizePropertyBridge.hasOverrides()); + } }