feat(mod): parallel-particles plumbing (CORES>=4 auto + -D tri-state)
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.
This commit is contained in:
parent
ecde09560d
commit
e85c4fc10a
6 changed files with 141 additions and 2 deletions
|
|
@ -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.<key>`.
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import java.util.Map;
|
|||
* <li>{@code -Dlovisual.opt.prepared_text_cache=true|false}</li>
|
||||
* <li>{@code -Dlovisual.opt.micro_opts=true|false}</li>
|
||||
* <li>{@code -Dlovisual.opt.fbo_share=true|false}</li>
|
||||
* <li>{@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}.</li>
|
||||
* <li>{@code -Dlovisual.opt.entity_lod_distance=35} — overrides the distance (4..256).</li>
|
||||
* <li>{@code -Dlovisual.opt.spawner_distance=16} — overrides the distance (4..64).</li>
|
||||
* <li>{@code -Dlovisual.opt.budget_mode=off|soft|aggressive}</li>
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue