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>` или поле в модуле); включать или нет решает пользователь.
|
Реализуем за default-off тумблерами (`-Dlovisual.opt.<key>` или поле в модуле); включать или нет решает пользователь.
|
||||||
Автомерялок не ждём — если пункт безопасен (не меняет картинку) и дешёв, он имеет смысл и при выключенном состоянии,
|
Автомерялок не ждём — если пункт безопасен (не меняет картинку) и дешёв, он имеет смысл и при выключенном состоянии,
|
||||||
тумблер даёт путь включить.
|
тумблер даёт путь включить.
|
||||||
- **Кэш состояния GL** шире, чем в референсе: пропуск повторных `glUseProgram`, `glBindVertexArray`, `glBindTexture`, `glBlendFunc`,
|
- ✅ **Кэш состояния GL** шире, чем в референсе: пропуск повторных `glUseProgram`, `glBindVertexArray`, `glBindTexture`, `glBlendFunc`,
|
||||||
`glDepthMask` (LoVisual уже имеет `GlStateManagerMixin`, расширяем).
|
`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).
|
- **Сортировка draw по pipeline и текстуре** внутри слоя там, где порядок не влияет на результат (наши квады `Renderer2D`, ленты trails).
|
||||||
- **Persistent mapped ring** для динамических вершин наших эффектов и HUD (`GL_ARB_buffer_storage`), если профиль покажет стоимость
|
- **Persistent mapped ring** для динамических вершин наших эффектов и HUD (`GL_ARB_buffer_storage`), если профиль покажет стоимость
|
||||||
`glBufferSubData`/map. Ring Auto-Tune из референса берём как идею размера.
|
`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.AnimFreeze;
|
||||||
import dev.loki.lovisual.render.engine.optimize.entity.ItemBounds;
|
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.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.hud.HudCache;
|
||||||
import dev.loki.lovisual.render.engine.optimize.parallel.ParallelModels;
|
import dev.loki.lovisual.render.engine.optimize.parallel.ParallelModels;
|
||||||
import dev.loki.lovisual.render.engine.optimize.profile.DrawStats;
|
import dev.loki.lovisual.render.engine.optimize.profile.DrawStats;
|
||||||
|
|
@ -132,6 +133,7 @@ public final class Optimize extends Module {
|
||||||
HudCache.reset();
|
HudCache.reset();
|
||||||
ParallelModels.reset();
|
ParallelModels.reset();
|
||||||
Workers.resetParallelParticles();
|
Workers.resetParallelParticles();
|
||||||
|
GlStateCache.reset();
|
||||||
// stop counting and drop the F3 numbers, onFrame will not run again
|
// stop counting and drop the F3 numbers, onFrame will not run again
|
||||||
DrawStats.endFrame(false);
|
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.GlCommandEncoderStatsMixin",
|
||||||
"optimize.stats.GlRenderPassStatsMixin",
|
"optimize.stats.GlRenderPassStatsMixin",
|
||||||
"optimize.stats.GlStateManagerStatsMixin",
|
"optimize.stats.GlStateManagerStatsMixin",
|
||||||
|
"optimize.gl.state.GlStateManagerCacheMixin",
|
||||||
"accessors.blaze3d.GlTextureInvoker",
|
"accessors.blaze3d.GlTextureInvoker",
|
||||||
"accessors.blaze3d.GpuDeviceAccessor",
|
"accessors.blaze3d.GpuDeviceAccessor",
|
||||||
"accessors.blaze3d.ShaderLoaderDefinitionsAccessor",
|
"accessors.blaze3d.ShaderLoaderDefinitionsAccessor",
|
||||||
|
|
@ -221,7 +222,9 @@
|
||||||
"render.main.MinecraftMixin",
|
"render.main.MinecraftMixin",
|
||||||
"render.main.ScreenEffectRendererMixin",
|
"render.main.ScreenEffectRendererMixin",
|
||||||
"render.weather.WeatherHideMixin",
|
"render.weather.WeatherHideMixin",
|
||||||
|
"render.spatialgui.GameRendererSpatialGuiMixin",
|
||||||
"render.spatialgui.GuiRendererCaptureMixin",
|
"render.spatialgui.GuiRendererCaptureMixin",
|
||||||
|
"render.spatialgui.GuiSpatialExtractMixin",
|
||||||
"render.spatialgui.ScreenSpatialBackgroundMixin",
|
"render.spatialgui.ScreenSpatialBackgroundMixin",
|
||||||
"render.sound.LibraryMixin",
|
"render.sound.LibraryMixin",
|
||||||
"render.sound.SoundEngineMixin",
|
"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