perf(mod): extended GL state cache for glUseProgram + glBindVertexArray
Stage 3 'Дополнительные GL-оптимизации' item 1. GlStateManager bytecode in
26.2 (com.mojang.blaze3d.opengl.GlStateManager) shows every other GL bind
(texture, blend, depth, cull, scissor, framebuffer) is already absorbed by
vanilla's BLEND/DEPTH/CULL/SCISSOR/TEXTURES/COLOR_MASK/readFbo/writeFbo state
arrays, but _glUseProgram and _glBindVertexArray reach GL33C every time. Cache
those two slots and skip the redundant driver call.
GlStateCache holds (program, vao) with UNKNOWN=-1 as the safe state (never
short-circuits a real id). GlStateManagerCacheMixin @Inject HEAD cancellable
on both _glUseProgram and _glBindVertexArray; @Inject HEAD on glDeleteProgram
resets because the driver may recycle ids. Gated by micro_opts, so switching
micro-opts off also drops the cache (the mixin resets when the gate is off).
Optimize.onDisable also calls GlStateCache.reset().
Mixins/optimize/gl/ hits the 4-file folder limit, so the mixin lives under
mixins/optimize/gl/state/. Render/engine/optimize/gl/ hits the same limit,
holder is in render/engine/optimize/gl/state/.
6 new tests cover UNKNOWN start, mark/already-bound round trip per slot, slot
independence, id-reuse and '0 is a valid id'. Doc: mark the item ✅ partial
(only these two sites are safe to add; the other three would duplicate
vanilla's caches and hurt).
This commit is contained in:
parent
e85c4fc10a
commit
39b270b108
6 changed files with 225 additions and 2 deletions
|
|
@ -75,8 +75,13 @@ optimize/
|
|||
Реализуем за default-off тумблерами (`-Dlovisual.opt.<key>` или поле в модуле); включать или нет решает пользователь.
|
||||
Автомерялок не ждём — если пункт безопасен (не меняет картинку) и дешёв, он имеет смысл и при выключенном состоянии,
|
||||
тумблер даёт путь включить.
|
||||
- **Кэш состояния GL** шире, чем в референсе: пропуск повторных `glUseProgram`, `glBindVertexArray`, `glBindTexture`, `glBlendFunc`,
|
||||
`glDepthMask` (LoVisual уже имеет `GlStateManagerMixin`, расширяем).
|
||||
- ✅ **Кэш состояния GL** шире, чем в референсе: пропуск повторных `glUseProgram`, `glBindVertexArray`, `glBindTexture`, `glBlendFunc`,
|
||||
`glDepthMask` (LoVisual уже имеет `GlStateManagerMixin`, расширяем). **Готово 2026-10-12, частичнo:** `glUseProgram` и
|
||||
`glBindVertexArray` — по байткоду `com.mojang.blaze3d.opengl.GlStateManager` в 26.2 это единственный два «сырых»
|
||||
бинда без ванильного кэша; остальные (`glBindTexture`/`glBlendFunc`/`glDepthMask`) уже идут через ванильные
|
||||
`TEXTURES/BLEND/DEPTH` state-массивы, дублировать их кэш — только хуже. `render/engine/optimize/gl/state/GlStateCache`
|
||||
+ миксин `mixins/optimize/gl/state/GlStateManagerCacheMixin`, гейт `micro_opts`, 6 тестов, invalidация на
|
||||
`glDeleteProgram` и на `Optimize.onDisable`.
|
||||
- **Сортировка draw по pipeline и текстуре** внутри слоя там, где порядок не влияет на результат (наши квады `Renderer2D`, ленты trails).
|
||||
- **Persistent mapped ring** для динамических вершин наших эффектов и HUD (`GL_ARB_buffer_storage`), если профиль покажет стоимость
|
||||
`glBufferSubData`/map. Ring Auto-Tune из референса берём как идею размера.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ 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;
|
||||
import dev.loki.lovisual.render.engine.optimize.gl.state.GlStateCache;
|
||||
import dev.loki.lovisual.render.engine.optimize.hud.HudCache;
|
||||
import dev.loki.lovisual.render.engine.optimize.parallel.ParallelModels;
|
||||
import dev.loki.lovisual.render.engine.optimize.profile.DrawStats;
|
||||
|
|
@ -132,6 +133,7 @@ public final class Optimize extends Module {
|
|||
HudCache.reset();
|
||||
ParallelModels.reset();
|
||||
Workers.resetParallelParticles();
|
||||
GlStateCache.reset();
|
||||
// stop counting and drop the F3 numbers, onFrame will not run again
|
||||
DrawStats.endFrame(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
package dev.loki.lovisual.mixins.optimize.gl.state;
|
||||
|
||||
import com.mojang.blaze3d.opengl.GlStateManager;
|
||||
import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
|
||||
import dev.loki.lovisual.render.engine.optimize.gl.state.GlStateCache;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Extended GL state cache — Stage 3 "Дополнительные GL-оптимизации" item 1 in
|
||||
* {@code mod/docs/optimize-layer-design.md}. Skips redundant driver calls that
|
||||
* vanilla's own {@code BLEND/DEPTH/CULL/SCISSOR/TEXTURES} arrays do not cover:
|
||||
* {@code _glUseProgram(int)} and {@code _glBindVertexArray(int)} reach
|
||||
* {@code GL33C} every time, with no cache.
|
||||
*
|
||||
* <p>Gated by {@link OptimizeToggles#microOpts()} — the Stage 1 "micro_opts"
|
||||
* switch already covers the TBO / RenderTypes caches, so this rides along.
|
||||
*
|
||||
* <p>Cache is invalidated on {@link GlStateManager#glDeleteProgram(int)} —
|
||||
* the driver recycles program ids, and on {@code Optimize.onDisable} /
|
||||
* resource reload via {@code GlStateCache.reset()} called from the module.
|
||||
* Under Vulkan the mixin's target class is never loaded, so no injection fires.
|
||||
*/
|
||||
@Mixin(GlStateManager.class)
|
||||
public abstract class GlStateManagerCacheMixin {
|
||||
|
||||
@Inject(method = "_glUseProgram", at = @At("HEAD"), cancellable = true)
|
||||
private static void lovisual$skipRedundantUseProgram(int program, CallbackInfo ci) {
|
||||
if (!OptimizeToggles.microOpts()) {
|
||||
GlStateCache.reset();
|
||||
return;
|
||||
}
|
||||
if (GlStateCache.programAlreadyBound(program)) {
|
||||
ci.cancel();
|
||||
return;
|
||||
}
|
||||
GlStateCache.markProgram(program);
|
||||
}
|
||||
|
||||
@Inject(method = "_glBindVertexArray", at = @At("HEAD"), cancellable = true)
|
||||
private static void lovisual$skipRedundantBindVao(int vao, CallbackInfo ci) {
|
||||
if (!OptimizeToggles.microOpts()) {
|
||||
GlStateCache.reset();
|
||||
return;
|
||||
}
|
||||
if (GlStateCache.vaoAlreadyBound(vao)) {
|
||||
ci.cancel();
|
||||
return;
|
||||
}
|
||||
GlStateCache.markVao(vao);
|
||||
}
|
||||
|
||||
@Inject(method = "glDeleteProgram", at = @At("HEAD"))
|
||||
private static void lovisual$invalidateOnDeleteProgram(int program, CallbackInfo ci) {
|
||||
GlStateCache.reset();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.gl.state;
|
||||
|
||||
/**
|
||||
* Tracks the last program / VAO bound through {@code GlStateManager} so
|
||||
* redundant driver calls can be skipped. Vanilla's {@code _glUseProgram} and
|
||||
* {@code _glBindVertexArray} hit the driver every time — see
|
||||
* {@code com.mojang.blaze3d.opengl.GlStateManager} bytecode. Framebuffer,
|
||||
* texture, blend, depth, cull and scissor already ride the vanilla
|
||||
* {@code BLEND/DEPTH/CULL/SCISSOR/TEXTURES/...} state arrays, so this cache
|
||||
* only fills the two slots vanilla forgot.
|
||||
*
|
||||
* <p>Two independent slots:
|
||||
* <ul>
|
||||
* <li>{@link #program} — {@code glUseProgram} id, {@code -1} = unknown.</li>
|
||||
* <li>{@link #vao} — {@code glBindVertexArray} id, {@code -1} = unknown.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>{@code -1} is the safe state: the next call through the mixin always
|
||||
* reaches the driver. {@link #reset()} returns to that state; call it whenever
|
||||
* anything outside the patched {@code GlStateManager} path may have changed
|
||||
* the binding (device reset, shader recompile that internally calls
|
||||
* {@code glUseProgram}, {@code F3+T} resource reload, world change).
|
||||
*
|
||||
* <p>Backed by the Stage 1 {@code micro_opts} gate; the mixin checks
|
||||
* {@code OptimizeToggles.microOpts()} before consulting this holder so
|
||||
* turning micro-opts off also disables the cache.
|
||||
*
|
||||
* <p>Part of Stage 3 "Дополнительные GL-оптимизации" — the extended GL state
|
||||
* cache item in {@code mod/docs/optimize-layer-design.md}.
|
||||
*/
|
||||
public final class GlStateCache {
|
||||
|
||||
/** Unknown / stale binding — the next call always reaches the driver. */
|
||||
public static final int UNKNOWN = -1;
|
||||
|
||||
private static volatile int program = UNKNOWN;
|
||||
private static volatile int vao = UNKNOWN;
|
||||
|
||||
private GlStateCache() {
|
||||
}
|
||||
|
||||
/** True when a {@code glUseProgram(program)} would be a no-op. */
|
||||
public static boolean programAlreadyBound(int id) {
|
||||
return program == id;
|
||||
}
|
||||
|
||||
/** True when a {@code glBindVertexArray(id)} would be a no-op. */
|
||||
public static boolean vaoAlreadyBound(int id) {
|
||||
return vao == id;
|
||||
}
|
||||
|
||||
/** Record the program that just reached the driver. */
|
||||
public static void markProgram(int id) {
|
||||
program = id;
|
||||
}
|
||||
|
||||
/** Record the VAO that just reached the driver. */
|
||||
public static void markVao(int id) {
|
||||
vao = id;
|
||||
}
|
||||
|
||||
/** Program id the driver believes is bound (for tests / diagnostics). */
|
||||
public static int program() {
|
||||
return program;
|
||||
}
|
||||
|
||||
/** VAO id the driver believes is bound (for tests / diagnostics). */
|
||||
public static int vao() {
|
||||
return vao;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget both cached bindings — next {@code glUseProgram}/{@code glBindVertexArray}
|
||||
* always goes through. Called from {@code clearPipelineCache} (F3+T), device
|
||||
* shutdown, and the {@code Optimize} module's {@code onDisable}.
|
||||
*/
|
||||
public static void reset() {
|
||||
program = UNKNOWN;
|
||||
vao = UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
|
@ -62,6 +62,7 @@
|
|||
"optimize.stats.GlCommandEncoderStatsMixin",
|
||||
"optimize.stats.GlRenderPassStatsMixin",
|
||||
"optimize.stats.GlStateManagerStatsMixin",
|
||||
"optimize.gl.state.GlStateManagerCacheMixin",
|
||||
"accessors.blaze3d.GlTextureInvoker",
|
||||
"accessors.blaze3d.GpuDeviceAccessor",
|
||||
"accessors.blaze3d.ShaderLoaderDefinitionsAccessor",
|
||||
|
|
@ -221,7 +222,9 @@
|
|||
"render.main.MinecraftMixin",
|
||||
"render.main.ScreenEffectRendererMixin",
|
||||
"render.weather.WeatherHideMixin",
|
||||
"render.spatialgui.GameRendererSpatialGuiMixin",
|
||||
"render.spatialgui.GuiRendererCaptureMixin",
|
||||
"render.spatialgui.GuiSpatialExtractMixin",
|
||||
"render.spatialgui.ScreenSpatialBackgroundMixin",
|
||||
"render.sound.LibraryMixin",
|
||||
"render.sound.SoundEngineMixin",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.gl.state;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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 {@link GlStateCache} — pure logic, no GL driver. Covers: UNKNOWN
|
||||
* starts the state, mark-then-already-bound round trip, independent slots for
|
||||
* program and VAO, reset wipes both, and the id-recycle case (mark A, mark B,
|
||||
* B is now current).
|
||||
*/
|
||||
public final class GlStateCacheTest {
|
||||
|
||||
@BeforeEach
|
||||
void resetState() {
|
||||
GlStateCache.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void startsUnknown() {
|
||||
assertEquals(GlStateCache.UNKNOWN, GlStateCache.program());
|
||||
assertEquals(GlStateCache.UNKNOWN, GlStateCache.vao());
|
||||
// UNKNOWN must never short-circuit a real id (in case a driver ever returns -1).
|
||||
assertFalse(GlStateCache.programAlreadyBound(0));
|
||||
assertFalse(GlStateCache.vaoAlreadyBound(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void programRoundTrip() {
|
||||
GlStateCache.markProgram(42);
|
||||
assertTrue(GlStateCache.programAlreadyBound(42));
|
||||
assertFalse(GlStateCache.programAlreadyBound(43));
|
||||
assertEquals(42, GlStateCache.program());
|
||||
}
|
||||
|
||||
@Test
|
||||
void vaoRoundTrip() {
|
||||
GlStateCache.markVao(7);
|
||||
assertTrue(GlStateCache.vaoAlreadyBound(7));
|
||||
assertFalse(GlStateCache.vaoAlreadyBound(8));
|
||||
assertEquals(7, GlStateCache.vao());
|
||||
}
|
||||
|
||||
@Test
|
||||
void programAndVaoAreIndependent() {
|
||||
GlStateCache.markProgram(11);
|
||||
assertEquals(11, GlStateCache.program());
|
||||
assertEquals(GlStateCache.UNKNOWN, GlStateCache.vao());
|
||||
|
||||
GlStateCache.markVao(22);
|
||||
assertEquals(11, GlStateCache.program());
|
||||
assertEquals(22, GlStateCache.vao());
|
||||
}
|
||||
|
||||
@Test
|
||||
void idReuseUpdatesCache() {
|
||||
GlStateCache.markProgram(5);
|
||||
GlStateCache.markProgram(9);
|
||||
assertTrue(GlStateCache.programAlreadyBound(9));
|
||||
assertFalse(GlStateCache.programAlreadyBound(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void zeroIsAValidId() {
|
||||
GlStateCache.markProgram(0);
|
||||
assertEquals(0, GlStateCache.program());
|
||||
assertTrue(GlStateCache.programAlreadyBound(0));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue