feat(mod): Stage 3 steps 1-8 — DrawStats, SharedDepthFbos, AnimFreeze, ItemLod/Bounds, HudCache, raw vertices, ParallelModels
This commit is contained in:
parent
8ef46e4560
commit
645896ca4b
83 changed files with 3107 additions and 143 deletions
|
|
@ -3,6 +3,8 @@
|
|||
Статус: Этап 1 готов (2026-10-10), Этап 2 + начало Этапа 3 готовы (2026-10-10). Цель MC: 26.2.
|
||||
|
||||
## История изменений
|
||||
- **2026-10-12**: референсная реализация обновлена с 1.0.1 до 1.0.3 (сверка исходников). Корректировки плана ниже, в разделе
|
||||
«Что нового в референсе 1.0.2-1.0.3».
|
||||
- **2026-10-10**: Этап 1 завершён — 11 портированных файлов (`core/LruMap`, `entity/{EntityLod,CachedName,StateEntity,CachedStack}`, `particle/{ParticleLod,Workers}`, `hud/HudWorth`, `block/{TickCached,CachedLight,CachedValidity,SpawnerCache}`, `text/StableText`) + `OptimizeToggles` + `mod/NOTICE`. 53 теста. Патчи: `0001-feat-mod-...patch`, `0002-test-mod-...patch`.
|
||||
- **2026-10-10**: Этап 2 завершён — `FrameBudget` (mode + target FPS + per-subsystem caps), `FrameProfiler` (5 секций EMA-таймингов), `OptimizePropertyBridge` (`-Dlovisual.opt.*` overrides), `OptimizeDebugHudMixin` (F3 overlay). 22 новых теста. `Optimize.java` расширен тумблерами `budget_mode` / `target_fps` / `show_profiler`.
|
||||
- **2026-10-10**: Этап 3 (частично) — `RenderBackendOps` интерфейс + `GlRenderBackendOps` реализация (lazy clear, TBO cache, RenderTypes cache). Три mixin'а зарегистрированы в `lovisual.mixins.json`: `RenderTypesMixin`, `GlCommandEncoderClearMixin`, `GlCommandEncoderTexBufferMixin`. SkinAtlas/AnimFreeze/HudCache/FrameFence оставлены на поздний Этап 3.
|
||||
|
|
@ -110,15 +112,68 @@ optimize/
|
|||
|
||||
Существующие Vulkan-миксины LoVisual (`VulkanDeviceMixin` и др.) не переписываем, новый код подключается рядом.
|
||||
|
||||
## Что нового в референсе 1.0.2-1.0.3 (сверено 2026-10-12)
|
||||
Референс обновился с 1.0.1 до 1.0.3 (+166 файлов). Что важно для нашего плана:
|
||||
- **FrameFence/RealFence для 26.2 не нужен.** Комментарий в `26.1.2/gl/GlCommandEncoderMixin`: «26.1 ещё делает GL-sync
|
||||
на каждый fence; с 26.2 ваниль делает один fence на submit и ждёт его по индексу submit'а». Хук `createFence` переехал из
|
||||
`26.x` в модуль `26.1.2`, в 26.2 его нет; в changelog прямо сказано, что опция «Один fence на кадр» оставлена только для
|
||||
26.1.2, с 26.2 это делает игра. Плюс история краша Xaero's World Map («Cannot wait on a fence for the current submit»)
|
||||
при общем fence — риск, который ваниль уже закрыла. => Пункт «FrameFence/RealFence» из Этапа 3 вычеркиваем; остаётся
|
||||
только хук конца кадра (`MinecraftFrameMixin`: `SkinAtlas.endFrame`, `AnimFreeze.frame++`, `refresh()` конфига).
|
||||
- **Счётчики draw-вызовов (`DrawStats`, `common/gl/DrawStats.java`, 75 строк)** — ровно наше заявленное предусловие
|
||||
«перед Этапом 3 нужен профиль HUD: число draw-вызовов и смен состояния на кадр». Считает draws/flush'и слоёв/FBO-bind'ы,
|
||||
средние за секунду, топ-5 слоёв, рисует только пока открыт F3. Дёшево, даёт данные для раздела «Дополнительные
|
||||
GL-оптимизации». => Включить в Этап 3 первым пунктом.
|
||||
- **Parallel Models (крупное новое, Impact HIGH).** `26.2/ModelFeatureRendererMixin` перехватывает `buildGroup` и собирает
|
||||
группу submit'ов на нескольких ядрах (`ParallelModelHook` + `ParallelModels` + два invoker'а). Заявлено +20-50% FPS в
|
||||
толпе бронированных мобов, отдельная опция. Отключается при <4 ядрах и при активном шейдерпаке Iris
|
||||
(`!(IRIS && IrisApi…isShaderPackInUse())` — Iris собирает расширенные атрибуты из вершин, отданных по одной).
|
||||
Зависит от `AnimFreeze.freezes()` (часть, которую нельзя реплеить) — ставить после AnimFreeze.
|
||||
- **ItemLod получил точный таргет в 26.2**: `entity/ItemFeatureRendererMixin` — `@WrapOperation` на `Submit.quads()` в
|
||||
`prepareMainSubmit`/`prepareOutlineSubmit`/`prepareFoilSubmit`, плоские квады дальше `ItemLod.distanceSq()`.
|
||||
- **Выброшенные предметы (новое):** `no_item_sort` (пропуск сортировки `item_entity_translucent_cull`, по умолчанию вкл,
|
||||
до +40% FPS на кучах), `item_single_copy` (стак одним предметом), `ItemBounds` (кэш AABB модели предмета, IdentityHashMap
|
||||
с полной очисткой >4096 — render state пересоздаётся каждый кадр, его собственный кэш умирает вместе с ним).
|
||||
`BufferSourceMixin`+`RenderTypeAccessor` живут в модуле `26.1.2`, в `26.x` их нет — перед переносом проверить, есть ли
|
||||
в 26.2 `MultiBufferSource.BufferSource.endBatch(RenderType, BufferBuilder)` и `RenderType.sortOnUpload()`.
|
||||
- **Фиксы HudCache, которые надо взять сразу** (иначе воспроизведём баги): главный target снимается в `beginFrame`
|
||||
(`main = gameRenderer.mainRenderTarget()`) и сравнение идёт с ним, а не с «текущим» — иначе редиректим чужой target
|
||||
(Xaero подменяет свой, пока рисует); двойной троттлинг `GuiItemRefresh`+`HudCache` приводил к пропавшим зачарованным
|
||||
предметам хотбара (фикс: пропускать свой троттлинг, когда `HudCache.active()`); composite — premultiplied + write mask
|
||||
7 (RGB), depth clear 0 (reversed depth в 26.x); кэш выключается при чужих HUD-кэшах (`axolotlclient`, `exordium`,
|
||||
`gnetum`) и под VulkanMod; интервал = рефреш-рейт монитора (`MonitorRate`, GLFW-опрос 1 раз в секунду).
|
||||
- **Правило утечек:** кэши с нативной памятью/GL-объектами чистить и на перезагрузку ресурсов (F3+T), и на смену мира.
|
||||
Фикс 1.0.3: `BubbleColumnUpParticleMixin` держал `ClientLevel` в статике через сильную ссылку — теперь `WeakReference`
|
||||
+ очистка карты при смене уровня; `MovingBlockCache`/`ParallelModels` чинили утечки native/model.
|
||||
- **Порог параллельных частиц:** план п.4 («авто-порог по замеру») упрощаем до того, что сделано в референсе —
|
||||
`CORES >= 4` + `-D…=auto|true|false`; 6+ ядер оказалось догадкой (замер на 4 ядрах дал +12%), вернули к 4.
|
||||
`Workers` получил `stream()` — публикация задач по мере готовности, рабочие берут их сразу, пока готовится остальное.
|
||||
- **MixinPlugin** — условная регистрация миксинов (Sodium/Vulkan/Lunar, `shouldApplyMixin` + `getMixins`), а тумблеры в UI
|
||||
прячутся, если миксин не построен/не применяется (`available()` → `MixinPlugin::applies`). У нас уже есть
|
||||
`mixins/LoVisualMixinPlugin` — расширить для Vulkan (Этап 4) и для «миксин есть не всегда».
|
||||
- **Не берём:** Lunar multi-draw (лаунчер Lunar, не наш случай); `blockEntityEarlyCull` перенесён только в `1.21.9+`,
|
||||
в `26.x` его нет — при случае проверить, применим ли к 26.2; `26.3` поддержка и `26.3/renames.txt` пригодятся при
|
||||
апгрейде версии (пакет `com.mojang.blaze3d` → `com.mojang.renderpearl`).
|
||||
|
||||
## Этапы
|
||||
1. ✅ `core/` + helpers + adapter interfaces + `OptimizeToggles` (бэкенд-независимые части). Тесты, сборка. **Завершён 2026-10-10** — 11 портированных .java, 53 теста, 0 падений, `checkFolderLimit` OK.
|
||||
2. ✅ Бюджет кадра, профилировщик, `-Dlovisual.opt.*`. **Завершён 2026-10-10** — `FrameBudget` (3 режима: OFF/SOFT/AGGRESSIVE, target FPS, per-subsystem caps для entities/particles/HUD/effects/text/trails), `FrameProfiler` (5 секций, EMA, decay через 2 с idle), `OptimizePropertyBridge` (10 системных свойств + `all=false` отключает всё), `OptimizeDebugHudMixin` (F3 overlay показывает тумблеры + бюджет + тайминги секций + override'ы). 22 новых теста.
|
||||
3. 🔄 `gl/`: shared FBO, FrameFence, lazy clear, TBO cache, SkinAtlas через GL; затем новые GL-идеи из раздела ниже. **Частично готово 2026-10-10**: `RenderBackendOps` интерфейс (GL/Vulkan стратегия), `GlRenderBackendOps` реализация, 3 mixin'а (`RenderTypesMixin`, `GlCommandEncoderClearMixin`, `GlCommandEncoderTexBufferMixin`), зарегистрированы в `lovisual.mixins.json`. **Осталось**: FrameFence/RealFence (shared GPU fence на кадр), SharedDepthFbos (DSA depth clear через cached FBO), SkinAtlas (glCopyImageSubData), AnimFreeze, ItemLod, LivingEntityLodMixin, ParticleMixin, SpawnerRendererMixin, BubbleColumnUpParticleMixin, VertexConsumerUtilsMixin, SodiumEntityRendererMixin, BlockEntity* mixins, HUD cache mixins, LevelRendererOutlineMixin, MinecraftFrameMixin.
|
||||
3. 🔄 `gl/` + остаток рендера. **Готово 2026-10-10**: `RenderBackendOps`, `GlRenderBackendOps`, `RenderTypesMixin`, `GlCommandEncoderClearMixin`, `GlCommandEncoderTexBufferMixin`. **Готово 2026-10-10…10-11**: миксины `LivingEntityLodMixin`, `ParticleMixin`, `BubbleColumnUpParticleMixin`, `SodiumEntityRendererMixin`, `BlockEntity*`, `ChestRendererMixin`, `LevelRendererOutlineMixin`, `BaseSpawnerMixin`, `EntityName/State/Renderer*`. **Осталось, в порядке приоритета** (порядок пересмотрен 2026-10-12 по референсу 1.0.3):
|
||||
1. ✅ `DrawStats` + строки в F3 — счётчики draw/flush/bind за кадр (предусловие для решений о GL-оптимизациях; дёшево). **Готово 2026-10-12** — `DrawStats`, миксины `mixins/optimize/stats/*` (executeDraw/Draws/Indirect/Multiple, setPipeline, glBindFramebuffer), строка F3 `draws/pipelines/fbo binds`, подключены `FrameProfiler.beginFrame/endFrame` (баг Стадии 2: не вызывались в проде), 7 тестов.
|
||||
2. ✅ `SharedDepthFbos` кластер — `SharedDepthFbos` (DSA `glClearNamedFramebufferfv`, fallback на ваниль без GL4.5/DSA), `FrameBufferCacheMixin` (remember/forget по `getFbo`/`destroyFbo`), `GlCommandEncoderDepthClearMixin` (`clearDepthTexture` HEAD, cancellable), `GlBufferCloseMixin` (`forgetTboCache()` при `close` GlBuffer.Direct — чинит протухание TBO-кэша, id буфера переиспользуется). **Готово 2026-10-12** — тумблер `fbo_share` (модуль + `-Dlovisual.opt.fbo_share`), методы `RenderBackendOps.fboShareSupported/clearDepthInPlace`, миксины в `mixins/optimize/gl/` и `mixins/optimize/gl/fbo/`, 8+3 тестов.
|
||||
3. ~~`SkinAtlas` кластер~~ **Выкинут 2026-10-12** — в референсе 1.0.3 фичи нет на 26.2 (в `26.2/` нет `SkinAtlas.java`/`LivingEntityRendererSkinMixin`, флаг `skinAtlas()` никем не вызывается; атлас скинов был только для 1.21.4, см. историю версий референса, раздел «1.21.4»). Как и FrameFence, не переносим.
|
||||
4. ✅ `AnimFreeze` кластер — `AnimFreeze` (capture раз в тик, реплей в вершины), `EntityAnimHolderMixin`, `AnimHolder`, метод `freezes(state, pose)` (часть, построенная каждый кадр в другом месте, не реплеим — это и чинит рябь брони), `RawVertices`/`SodiumVertices`. **Готово 2026-10-12** — `AnimFreeze.configure(enabled, distance)` (свой холдер, не OptimizeToggles), тумблеры `entity_anim_freeze` (по умолчанию выкл) + `entity_anim_distance` (16), `frame++` в `MinecraftMixin.renderFrame` HEAD, вызов из `ModelFeatureRendererMixin.prepareModel` (`@WrapOperation` на `Model.renderToBuffer`), инфраструктура сырых вершин из п.7 (`RawBuffer`/`BufferBuilderRawMixin`/`RawVertices`/`SodiumVertices`/`VertexConsumerUtilsMixin`), 3 теста. TypeBuilders не выделялся — свёрнут в приватные хелперы `AnimFreeze`.
|
||||
5. ✅ `ItemLod` — `ItemLod` + `ItemFeatureRendererMixin` (`@WrapOperation` на `Submit.quads()` в `prepareMain/Outline/FoilSubmit`) + `ItemBounds`. **Готово 2026-10-12** — `ItemLod`/`ItemBounds` в `render/engine/optimize/entity/` (в `entity/` теперь ровно 4 файла — лимит исчерпан), LOD-хук добавлен в существующий `ItemFeatureRendererMixin`, `ItemStackRenderStateMixin`+`ItemLayerAccessor` в `mixins/render/item/bounds/`, тумблеры `item_lod`/`item_lod_distance` (8)/`item_bounds`, 9 тестов.
|
||||
6. ✅ `HudCache` кластер — `HudCache`, `MonitorRate`, `HudWorth` (есть), миксины `GameRendererHudMixin`, `GuiHudMixin`, `GuiRendererTargetMixin`, `HudInvoker`, `HudLiveMixin`; все фиксы совместимости из раздела выше — сразу. **Готово 2026-10-12** — тумблер `hud_cache` **по умолчанию выкл** (взаимодействует с нашими deferred-HUD-маркерами в `GuiRendererMixin`); гейт на `axolotlclient`/`exordium`/`gnetum`/`vulkanmod` через FabricLoader; main target снимается в `beginFrame`; composite premultiplied + write mask 7, depth clear 0; `HudInvoker` в подпапке `hud/cache/invoker/` (лимит 4 файлов), 9 тестов (MonitorRate clamp+троттлинг, HudWorth-гистерезис).
|
||||
7. ✅ `VertexConsumerUtilsMixin` (кэш `VertexBufferWriter`, только при Sodium — в `OPTIONAL_SODIUM_MIXINS`) + `BufferBuilderRawMixin`/`RawBuffer` (запись без Sodium) + `RawVertices`/`SodiumVertices`. **Готово 2026-10-12** (упреждённо, как предусловие п.4) — гейт на `micro_opts`, 2 миксина в `mixins/optimize/gl/raw/`.
|
||||
8. ✅ `ParallelModels` — `ParallelModelHook`, `ModelFeatureRendererMixin` (`buildGroup` cancellable), invoker'ы; после `AnimFreeze` (зависит от `freezes()`). **Готово 2026-10-12** — `ParallelModels`/`ParallelModelHook`/`ModelGeometry`/`ModelBuilder` в `render/engine/optimize/parallel/` (оригинальный 348-строчный класс разбит на 4 файла, лимит 250), `Workers` получил `stream()` (публикация по мере готовности), 3 интефейса в `mixins/optimize/entity/parallel/`, `buildGroup`-inject добавлен в существующий `ModelFeatureRendererMixin`, тумблер `parallel_models` (on) с внутренними гейтами: ≥4 ядер, нет активного Iris-шейдерпака, взаимоисключение с `AnimFreeze`, формат `DefaultVertexFormat.ENTITY` (36 байт, оффсеты сверены javap), `parts[0]=null` против утечки weak-map, native-страницы освобождаются в `reset()`, 7 тестов (3 stream + 4 гейтинг).
|
||||
9. По желанию/по замерам: `no_item_sort` + `item_single_copy` (`BufferSourceMixin`, `ItemClusterRenderStateMixin`) — сначала проверить наличие `endBatch(RenderType, BufferBuilder)` в 26.2. **Проверено 2026-10-12**: двухаргументного `endBatch(RenderType, BufferBuilder)` в 26.2 `BufferSource` **нет**, `MeshData.DrawState` без sort-методов; пункт остаётся отложенным до замеров и дизайна под 26.2-API.
|
||||
Хук конца кадра не нужен отдельным `MinecraftFrameMixin`: `renderFrame` у нас уже перехвачен (`mixins/render/main/MinecraftMixin`), туда же вешаем `SkinAtlas.endFrame`/`AnimFreeze.frame++`/конфиг-`refresh()`. **FrameFence/RealFence вычеркнут** — см. «Что нового», ваниль 26.2 делает это сама.
|
||||
4. ❌ `vulkan/`: `vkCmdCopyImage`, затем `VkPipelineCache`, затем остальное по замерам. **Не начат** — дизайн готов, ждёт профилирования GL-части чтобы понять, какие из Vulkan-оптимизаций реально нужны.
|
||||
5. ❌ Бенчмарки до и после, отчёт цифр в этот документ. **Не начат** — требует запуска Minecraft на Windows, задача владельца.
|
||||
|
||||
## Оценки для Этапов 3-4 (после Stage 1-2)
|
||||
- **Этап 3 (остальное)**: ~25 mixin'ов + ~8 helper-классов, ~80-100 новых .java-файлов. Сложные: `AnimFreeze` (vertex format + MemoryUtil), `SkinAtlas` (glCopyImageSubData + LRU), `HudCache` (RenderPipeline + RenderPass + TextureTarget). Безопасные простые: `LevelRendererOutlineMixin`, `BubbleColumnUpParticleMixin`, `SodiumEntityRendererMixin`. ~3-4 рабочих сессии.
|
||||
- **Этап 3 (остальное)**: порядка 20 mixin'ов + ~10 helper-классов, ~70-90 новых .java-файлов. Сложные: `AnimFreeze` (vertex format + MemoryUtil), `SkinAtlas` (GPU-to-GPU копия + LRU), `HudCache` (RenderPipeline + RenderPass + TextureTarget), `ParallelModels`. Простые: `DrawStats`, `SharedDepthFbos`, `ItemLod`. ~3 рабочие сессии (FrameFence вычеркнут).
|
||||
- **Этап 4**: 6 пунктов, каждый требует профилирования. Минимум 1 сессия на `vkCmdCopyImage` (чтобы SkinAtlas заработал на Vulkan), остальное по мере замеров.
|
||||
|
||||
## Риски
|
||||
|
|
|
|||
|
|
@ -20,3 +20,4 @@ viafabricplus_version=4.6.0
|
|||
entityculling_version=1.10.5
|
||||
voicechat_api_version=2.6.24
|
||||
mixinextras_version=0.5.5
|
||||
lovisualDevEnabled=false
|
||||
|
|
|
|||
|
|
@ -7,10 +7,18 @@ import dev.loki.lovisual.features.module.core.Module;
|
|||
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||
import dev.loki.lovisual.features.module.phase.Notifier;
|
||||
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.hud.HudCache;
|
||||
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.FrameBudget;
|
||||
import dev.loki.lovisual.render.engine.optimize.profile.FrameProfiler;
|
||||
import dev.loki.lovisual.render.engine.optimize.profile.OptimizePropertyBridge;
|
||||
import dev.loki.lovisual.render.engine.optimize.OptimizeState;
|
||||
import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
||||
/**
|
||||
* Manual FPS rubilnik: one switch plus a set of knobs, each cutting a named cost
|
||||
|
|
@ -24,13 +32,13 @@ import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
|
|||
* — never this class; this module only pushes state into them on
|
||||
* enable/disable and once per frame.
|
||||
*
|
||||
* <p>Stage 2 of {@code mod/docs/optimize-layer-design.md}: the module now also
|
||||
* <p>Stage 2 of {@code mod/docs/optimize-layer-design.md}: the module also
|
||||
* pushes a {@link FrameBudget} snapshot (mode + target FPS) and resolves
|
||||
* {@code -Dlovisual.opt.*} system-property overrides via
|
||||
* {@link OptimizePropertyBridge}. The frame profiler ({@code FrameProfiler})
|
||||
* is fed by the {@code MinecraftFrameMixin} (Stage 3 — not yet present); the
|
||||
* F3 overlay ({@code OptimizeDebugHudMixin}) reads the profiler's snapshot
|
||||
* whenever the user opens F3.
|
||||
* {@link OptimizePropertyBridge}. Stage 3 step 1 adds {@link DrawStats} —
|
||||
* draw/pipeline/framebuffer counters fed by the {@code mixins/optimize/stats}
|
||||
* hooks, opened and closed here so they only run while F3 is up. The F3
|
||||
* overlay ({@code OptimizeDebugHudMixin}) reads both snapshots.
|
||||
*/
|
||||
@ModuleInfo(
|
||||
id = "optimize",
|
||||
|
|
@ -58,6 +66,23 @@ public final class Optimize extends Module {
|
|||
private final BooleanValue preparedTextCache = bool("prepared_text_cache", true);
|
||||
private final BooleanValue microOpts = bool("micro_opts", true);
|
||||
|
||||
// --- Stage 3: GL-level helpers ---
|
||||
private final BooleanValue fboShare = bool("fbo_share", true);
|
||||
private final BooleanValue entityAnimFreeze = bool("entity_anim_freeze", false);
|
||||
private final NumberValue<Integer> entityAnimDistance =
|
||||
num("entity_anim_distance", 16, 4, 128);
|
||||
|
||||
// --- Stage 3: items ---
|
||||
private final BooleanValue itemLod = bool("item_lod", true);
|
||||
private final NumberValue<Integer> itemLodDistance = num("item_lod_distance", 8, 2, 64);
|
||||
private final BooleanValue itemBounds = bool("item_bounds", true);
|
||||
|
||||
// --- Stage 3: HUD cache (off by default — interacts with the deferred-HUD markers) ---
|
||||
private final BooleanValue hudCacheToggle = bool("hud_cache", false);
|
||||
|
||||
// --- Stage 3: parallel model build (own holder: cores/Iris gating inside) ---
|
||||
private final BooleanValue parallelModels = bool("parallel_models", true);
|
||||
|
||||
// --- Stage 2: frame budget + profiling ---
|
||||
private final EnumValue<FrameBudget.Mode> budgetMode =
|
||||
enumMode("budget_mode", FrameBudget.Mode.OFF, FrameBudget.Mode.values());
|
||||
|
|
@ -84,12 +109,27 @@ public final class Optimize extends Module {
|
|||
OptimizeState.reset();
|
||||
OptimizeToggles.reset();
|
||||
FrameBudget.reset();
|
||||
AnimFreeze.reset();
|
||||
ItemLod.reset();
|
||||
ItemBounds.reset();
|
||||
HudCache.reset();
|
||||
ParallelModels.reset();
|
||||
// stop counting and drop the F3 numbers, onFrame will not run again
|
||||
DrawStats.endFrame(false);
|
||||
}
|
||||
|
||||
/** Picks up knob changes made while the module stays on. */
|
||||
@Override
|
||||
public void onFrame(float d) {
|
||||
pushState();
|
||||
// close the window of the frame that just drew; the next publish lands once a second
|
||||
DrawStats.endFrame(debugOverlayVisible());
|
||||
}
|
||||
|
||||
/** F3 counts are only worth collecting while they are on screen. */
|
||||
private static boolean debugOverlayVisible() {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
return mc != null && mc.getDebugOverlay() != null && mc.getDebugOverlay().showDebugScreen();
|
||||
}
|
||||
|
||||
private void pushState() {
|
||||
|
|
@ -114,11 +154,25 @@ public final class Optimize extends Module {
|
|||
resolveBool(props, props.spawnerTickCache, on && spawnerTickCache.get()),
|
||||
resolveBool(props, props.blockEntityCache, on && blockEntityCache.get()),
|
||||
resolveBool(props, props.preparedTextCache, on && preparedTextCache.get()),
|
||||
resolveBool(props, props.microOpts, on && microOpts.get()));
|
||||
resolveBool(props, props.microOpts, on && microOpts.get()),
|
||||
resolveBool(props, props.fboShare, on && fboShare.get()));
|
||||
|
||||
// Stage 2: frame budget. -Dlovisual.opt.budget_mode / target_fps override the module.
|
||||
FrameBudget.Mode mode = on ? budgetMode.get() : FrameBudget.Mode.OFF;
|
||||
FrameBudget.apply(props.overrides(mode, mode), props.resolveTargetFps(targetFps.get()));
|
||||
|
||||
// Stage 3: far-entity animation freeze — own holder, pushed once per frame.
|
||||
AnimFreeze.configure(on && entityAnimFreeze.get(), entityAnimDistance.get());
|
||||
|
||||
// Stage 3: far-item LOD + shared single-layer item bounds.
|
||||
ItemLod.configure(on && itemLod.get(), itemLodDistance.get());
|
||||
ItemBounds.configure(on && itemBounds.get());
|
||||
|
||||
// Stage 3: HUD-at-refresh-rate cache; own holder (foreign-HUD/Vulkan gating inside).
|
||||
HudCache.configure(on && hudCacheToggle.get());
|
||||
|
||||
// Stage 3: parallel model build; mutually exclusive with anim freeze (checked inside).
|
||||
ParallelModels.configure(on && parallelModels.get());
|
||||
}
|
||||
|
||||
private static boolean resolveBool(OptimizePropertyBridge.Snapshot props,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package dev.loki.lovisual.features.module.modules.misc.optimize.gl;
|
||||
|
||||
import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
|
||||
import dev.loki.lovisual.render.engine.optimize.gl.SharedDepthFbos;
|
||||
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
|
@ -11,31 +12,22 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||
*
|
||||
* <p>Resolves the current GL device via {@code GlBackendAccess.current()} —
|
||||
* returns {@code null} on Vulkan so all mixins short-circuit to the vanilla
|
||||
* call. The cache state is static and render-thread-only (ZakoOpt does the
|
||||
* same — the GL command encoder runs single-threaded).
|
||||
*
|
||||
* <p>Ported from ZakoOpt's three GL caches:
|
||||
* <ul>
|
||||
* <li>{@code zako.opt.mixin.gl.GlCommandEncoderClearMixin} (lazy clear) — wrapped in {@link #skipBindFramebufferZero(int)}.</li>
|
||||
* <li>{@code zako.opt.gl.TexBufferCache} + {@code zako.opt.mixin.gl.GlCommandEncoderTexBufferMixin} — wrapped in the TBO-cache methods.</li>
|
||||
* <li>{@code zako.opt.mixin.gl.RenderTypesMixin} — wrapped in the RenderTypes-cache methods.</li>
|
||||
* </ul>
|
||||
* call. The cache state is static and render-thread-only (the GL command
|
||||
* encoder runs single-threaded).
|
||||
*
|
||||
* <p>Each toggle is gated by {@link OptimizeToggles#microOpts()} (the Stage 1
|
||||
* "micro_opts" toggle wired into {@code Optimize.java}); ZakoOpt splits this
|
||||
* into {@code lookupCaches}, {@code microOpts}, {@code tboCache} — Stage 1
|
||||
* collapses them, Stage 2 may split if profiling warrants.
|
||||
* "micro_opts" toggle wired into {@code Optimize.java}).
|
||||
*/
|
||||
public final class GlRenderBackendOps implements RenderBackendOps {
|
||||
|
||||
private static final GlRenderBackendOps INSTANCE = new GlRenderBackendOps();
|
||||
|
||||
// --- TBO cache (ported from zako.opt.gl.TexBufferCache) ---
|
||||
// --- TBO cache ---
|
||||
/** Maps bound-texture-id -> packed (format << 32) | (buffer & 0xFFFFFFFFL). -1 = nothing attached. */
|
||||
private final Int2LongOpenHashMap tboAttached = new Int2LongOpenHashMap();
|
||||
private int boundTboTexture;
|
||||
|
||||
// --- RenderTypes cache (ported from zako.opt.mixin.gl.RenderTypesMixin) ---
|
||||
// --- RenderTypes cache ---
|
||||
private final Map<Identifier, Object> translucentCache = new ConcurrentHashMap<>();
|
||||
|
||||
private GlRenderBackendOps() {
|
||||
|
|
@ -100,6 +92,18 @@ public final class GlRenderBackendOps implements RenderBackendOps {
|
|||
return ((long) format << 32) | (buffer & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
// --- FBO share (Stage 3 step 2) ---
|
||||
|
||||
@Override
|
||||
public boolean fboShareSupported() {
|
||||
return OptimizeToggles.fboShare();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clearDepthInPlace(int depthId, double value) {
|
||||
return fboShareSupported() && SharedDepthFbos.clear(depthId, value);
|
||||
}
|
||||
|
||||
// --- RenderTypes cache ---
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ package dev.loki.lovisual.features.module.modules.misc.optimize.gl;
|
|||
* <li>{@link #tboCacheSupported()} / {@link #tboAlreadyAttached(int, int)} /
|
||||
* {@link #markTboAttached(int, int)} / {@link #trackBoundTboTexture(int)} /
|
||||
* {@link #forgetTboCache()} — TBO cache</li>
|
||||
* <li>{@link #fboShareSupported()} / {@link #clearDepthInPlace(int, double)} —
|
||||
* shared depth FBOs (Stage 3 step 2)</li>
|
||||
* <li>{@link #renderTypesCacheSupported()} / {@link #renderTypeFor(Identifier)} /
|
||||
* {@link #cacheRenderType(Identifier, Object)} — RenderTypes cache</li>
|
||||
* </ul>
|
||||
|
|
@ -64,6 +66,22 @@ public interface RenderBackendOps {
|
|||
/** Forget all TBO attachments — called when a {@code GlBuffer.Direct} is closed. */
|
||||
void forgetTboCache();
|
||||
|
||||
// --- FBO share (Stage 3 step 2) ---
|
||||
|
||||
/** True when this backend can clear a depth texture in place via a cached FBO. */
|
||||
boolean fboShareSupported();
|
||||
|
||||
/**
|
||||
* Called from {@code GlCommandEncoder.clearDepthTexture} on the
|
||||
* {@code GlTexture} depth argument.
|
||||
*
|
||||
* @param depthId the GL id of the depth texture
|
||||
* @param value the clear value (0..1)
|
||||
* @return {@code true} when the clear already ran in place (mixin cancels
|
||||
* the vanilla path); {@code false} to fall through to vanilla
|
||||
*/
|
||||
boolean clearDepthInPlace(int depthId, double value);
|
||||
|
||||
// --- RenderTypes cache ---
|
||||
|
||||
/** True when this backend can cache {@code RenderTypes.entityTranslucent(id, flag=true)}. */
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ import net.minecraft.world.level.material.FluidState;
|
|||
* and destroyed constantly — per-particle state would be re-initialized too
|
||||
* often to be useful.
|
||||
*
|
||||
* <p>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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
|||
* created and destroyed constantly — per-particle state would be re-initialized
|
||||
* too often to be useful.
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.particle.BubbleColumnUpParticleMixin}
|
||||
* (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||
*/
|
||||
public final class TickCache<V> {
|
||||
|
||||
|
|
|
|||
|
|
@ -59,11 +59,78 @@ public final class Workers {
|
|||
return job.error.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a task stream: helpers start right away and take items as the
|
||||
* caller publishes them, so work overlaps with preparing the rest.
|
||||
* Used by the parallel model builder (Stage 3).
|
||||
*/
|
||||
public static Stream stream(IntConsumer task) {
|
||||
Stream stream = new Stream(task);
|
||||
for (int h = COUNT; h > 0; h--) {
|
||||
POOL.execute(stream::work);
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
/** Test-only: shut the pool down so the JVM can exit cleanly. */
|
||||
public static void shutdown() {
|
||||
POOL.shutdownNow();
|
||||
}
|
||||
|
||||
/** Items {@code 0..count-1} are fully set up and may be taken by helpers. */
|
||||
public static final class Stream {
|
||||
final IntConsumer task;
|
||||
final AtomicInteger next = new AtomicInteger();
|
||||
final AtomicInteger completed = new AtomicInteger();
|
||||
final AtomicReference<Throwable> error = new AtomicReference<>();
|
||||
volatile int ready;
|
||||
volatile int total = -1;
|
||||
|
||||
Stream(IntConsumer task) {
|
||||
this.task = task;
|
||||
}
|
||||
|
||||
public void publish(int count) {
|
||||
ready = count;
|
||||
}
|
||||
|
||||
/** The caller helps with what is left and waits for the rest; first failure or null. */
|
||||
public Throwable finish(int count) {
|
||||
ready = count;
|
||||
total = count;
|
||||
work();
|
||||
while (completed.get() < count) {
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
return error.get();
|
||||
}
|
||||
|
||||
void work() {
|
||||
while (true) {
|
||||
int n = next.get();
|
||||
if (n < ready) {
|
||||
if (next.compareAndSet(n, n + 1)) {
|
||||
try {
|
||||
if (error.get() == null) {
|
||||
task.accept(n);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
error.compareAndSet(null, t);
|
||||
} finally {
|
||||
completed.incrementAndGet();
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
int t = total;
|
||||
if (t >= 0 && n >= t) {
|
||||
return;
|
||||
}
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Job {
|
||||
final int blocks;
|
||||
final IntConsumer task;
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ import net.minecraft.world.phys.Vec3;
|
|||
* {@link ParticleMixin} reads it without re-walking the gameRenderer chain
|
||||
* per particle.
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.particle.CameraPosition} (ZakoOpt, LGPL-3.0).
|
||||
* Original author: Zako — https://t.me/StarikZako. Render-thread only —
|
||||
* particle ticking runs on the render thread.
|
||||
*/
|
||||
public final class CameraPosition {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
|
|||
* outline-target-clear {@code WrapOperation} when the renderer actually
|
||||
* touches the outline target.
|
||||
*
|
||||
* <p>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 {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ public final class LoVisualMixinPlugin implements IMixinConfigPlugin {
|
|||
"dev.loki.lovisual.mixins.sodium.vertex.SodiumVertexConsumerTrackerMixin",
|
||||
"dev.loki.lovisual.mixins.sodium.placement.SodiumPlaceCullMixin",
|
||||
"dev.loki.lovisual.mixins.sodium.placement.SodiumPlaceHideMixin",
|
||||
"dev.loki.lovisual.mixins.optimize.entity.SodiumEntityRendererMixin"
|
||||
"dev.loki.lovisual.mixins.optimize.entity.SodiumEntityRendererMixin",
|
||||
"dev.loki.lovisual.mixins.optimize.gl.raw.VertexConsumerUtilsMixin"
|
||||
);
|
||||
private static final Set<String> OPTIONAL_IRIS_MIXINS = Set.of(
|
||||
"dev.loki.lovisual.mixins.iris.render.IrisCommonUniformsMixin",
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ import org.spongepowered.asm.mixin.Unique;
|
|||
* the display entity only changes on tick boundaries (spin speed, mob
|
||||
* rotation, etc.), so caching by tick removes that rebuild from the hot path.
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.block.BaseSpawnerMixin} (ZakoOpt,
|
||||
* LGPL-3.0). Original author: Zako — https://t.me/StarikZako. Field/method
|
||||
* prefix changed from {@code zakoopt$} to {@code lovisual$}.
|
||||
*/
|
||||
@Mixin(BaseSpawner.class)
|
||||
public abstract class BaseSpawnerMixin implements TickCached {
|
||||
|
|
|
|||
|
|
@ -21,9 +21,6 @@ import org.spongepowered.asm.mixin.Unique;
|
|||
* distance check that only changes when the entity moves across the cull
|
||||
* boundary. All three are cached via the interface fields implemented here.
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.block.BlockEntityMixin} (ZakoOpt,
|
||||
* LGPL-3.0). Original author: Zako — https://t.me/StarikZako. Field/method
|
||||
* prefix changed from {@code zakoopt$} to {@code lovisual$}.
|
||||
*/
|
||||
@Mixin(BlockEntity.class)
|
||||
public abstract class BlockEntityMixin implements TickCached, CachedLight, CachedValidity {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||
* {@link CachedLight} mixin from Stage 3-continued-2) before falling through
|
||||
* to the original.
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.block.BlockEntityRenderStateMixin}
|
||||
* (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||
* Gated on the {@code blockEntityCache} toggle via
|
||||
* {@link OptimizeToggles#blockEntityCache()}. When the toggle is off, the
|
||||
* original runs unchanged.
|
||||
|
|
|
|||
|
|
@ -35,12 +35,8 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||
* {@link CachedLight#lovisual$pairLight()} slot.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.block.ChestRendererMixin} (ZakoOpt,
|
||||
* LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||
* Combine cache gated on {@code blockEntityCache} toggle; pair-light cache
|
||||
* gated on {@code microOpts} toggle (matches ZakoOpt's microOpts2 split —
|
||||
* Stage 1 collapsed microOpts2 into microOpts, so both paths read the same
|
||||
* toggle).
|
||||
* gated on {@code microOpts} toggle.
|
||||
*/
|
||||
@Mixin(ChestRenderer.class)
|
||||
public abstract class ChestRendererMixin {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,6 @@ import org.spongepowered.asm.mixin.Unique;
|
|||
* prefix + name + suffix on every call; renderers call it once per frame per
|
||||
* entity in view, so caching by tick cuts the per-frame cost.
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.entity.EntityNameMixin} (ZakoOpt,
|
||||
* LGPL-3.0). Original author: Zako — https://t.me/StarikZako. Field/method
|
||||
* prefix changed from {@code zakoopt$} to {@code lovisual$}.
|
||||
*/
|
||||
@Mixin(Entity.class)
|
||||
public abstract class EntityNameMixin implements CachedName {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,6 @@ import org.spongepowered.asm.mixin.Unique;
|
|||
* {@link EntityRendererStateMixin} captures it once per state construction
|
||||
* via {@code createRenderState} and downstream renderers read it back.
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.entity.EntityRenderStateEntityMixin}
|
||||
* (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||
* Prefix changed from {@code zakoopt$} to {@code lovisual$}.
|
||||
*/
|
||||
@Mixin(EntityRenderState.class)
|
||||
public abstract class EntityRenderStateEntityMixin implements StateEntity {
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||
* entity; we AND it with "not far" so far entities skip the layers but
|
||||
* close entities render them as usual.
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.entity.LivingEntityLodMixin} (ZakoOpt,
|
||||
* LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||
*
|
||||
* <p>Gated on the {@code entity_lod} toggle via {@link EntityLod#far(double)}.
|
||||
* When the toggle is off, {@code far} returns {@code false} so the modify
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||
* but the convention is kept for safety. The mixin is registered with
|
||||
* {@code remap = false} because the target is a Sodium class, not a Mojang one.
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.entity.SodiumEntityRendererMixin}
|
||||
* (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||
* Gated on the {@code microOpts} toggle via {@link OptimizeToggles#microOpts()}.
|
||||
* When the toggle is off, the original runs unchanged.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
package dev.loki.lovisual.mixins.optimize.entity.anim;
|
||||
|
||||
import dev.loki.lovisual.render.engine.optimize.entity.AnimFreeze;
|
||||
import dev.loki.lovisual.render.engine.optimize.entity.AnimHolder;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
||||
/**
|
||||
* Stage 3 step 4: implements {@link AnimHolder} on {@code Entity} — adds the
|
||||
* per-entity bag of frozen animation captures. The field lives as long as
|
||||
* the entity; captures hold direct buffers, so long-lived entities keep at
|
||||
* most one capture per (model, renderType, sprite) submission slot.
|
||||
*/
|
||||
@Mixin(Entity.class)
|
||||
public abstract class EntityAnimHolderMixin implements AnimHolder {
|
||||
|
||||
@Unique
|
||||
private AnimFreeze.Holder lovisual$anim;
|
||||
|
||||
@Override
|
||||
public AnimFreeze.Holder lovisual$anim() {
|
||||
return lovisual$anim;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lovisual$anim(AnimFreeze.Holder holder) {
|
||||
lovisual$anim = holder;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package dev.loki.lovisual.mixins.optimize.entity.anim;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dev.loki.lovisual.features.module.modules.misc.optimize.entity.StateEntity;
|
||||
import dev.loki.lovisual.render.engine.optimize.entity.AnimFreeze;
|
||||
import dev.loki.lovisual.render.engine.optimize.parallel.ParallelModelHook;
|
||||
import java.util.List;
|
||||
import net.minecraft.client.model.Model;
|
||||
import net.minecraft.client.renderer.entity.state.EntityRenderState;
|
||||
import net.minecraft.client.renderer.feature.FeatureFrameContext;
|
||||
import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Stage 3 step 4: the AnimFreeze call site. When a far entity's model part is
|
||||
* about to render, hand the submission to {@link AnimFreeze#render} — it
|
||||
* replays the once-per-tick raw vertex capture shifted to the current
|
||||
* position; on success the vanilla {@code renderToBuffer} is skipped.
|
||||
*
|
||||
* <p>The buffer may already be sprite-wrapped; the replayed vertices go
|
||||
* through it, so Sodium's push remaps their UVs. Sheeted decals (block
|
||||
* cracks) never freeze — their pose is built elsewhere.
|
||||
*/
|
||||
@Mixin(ModelFeatureRenderer.class)
|
||||
public abstract class ModelFeatureRendererMixin {
|
||||
|
||||
@Inject(method = "buildGroup", at = @At("HEAD"), cancellable = true)
|
||||
private void lovisual$parallel(FeatureFrameContext context, List<ModelFeatureRenderer.Submit<?>> submits,
|
||||
CallbackInfo ci) {
|
||||
if (ParallelModelHook.buildGroup((ModelFeatureRenderer) (Object) this, submits)) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@WrapOperation(
|
||||
method = "prepareModel",
|
||||
at = @At(value = "INVOKE",
|
||||
target = "Lnet/minecraft/client/model/Model;renderToBuffer(Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;III)V")
|
||||
)
|
||||
private void lovisual$frozenAnimation(Model<?> model, PoseStack poseStack, VertexConsumer consumer,
|
||||
int light, int overlay, int color, Operation<Void> original,
|
||||
ModelFeatureRenderer.Submit<?> submit) {
|
||||
if (AnimFreeze.enabled() && submit.sheetedDecalPose() == null
|
||||
&& submit.state() instanceof EntityRenderState state) {
|
||||
Entity entity = ((StateEntity) state).lovisual$entity();
|
||||
if (entity != null && AnimFreeze.render(entity, model, submit.renderType(), null,
|
||||
poseStack.last().pose(), consumer,
|
||||
target -> original.call(model, poseStack, target, light, overlay, color))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
original.call(model, poseStack, consumer, light, overlay, color);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package dev.loki.lovisual.mixins.optimize.entity.parallel;
|
||||
|
||||
import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||
|
||||
/**
|
||||
* Stage 3 step 8: the vanilla per-model build, invoked as the fallback for
|
||||
* submits the parallel path could not take (wrong format, decals, frozen
|
||||
* animations, raw-vertex consumer unavailable).
|
||||
*/
|
||||
@Mixin(ModelFeatureRenderer.class)
|
||||
public interface ModelFeatureRendererInvoker {
|
||||
|
||||
@Invoker("prepareModel")
|
||||
<S> void lovisual$prepareModel(ModelFeatureRenderer.Submit<S> submit);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package dev.loki.lovisual.mixins.optimize.entity.parallel;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
/**
|
||||
* Stage 3 step 8: exposes the part-tree internals the parallel model build
|
||||
* flattens once per model — the baked cubes and the ordered children map.
|
||||
*/
|
||||
@Mixin(ModelPart.class)
|
||||
public interface ModelPartAccessor {
|
||||
|
||||
@Accessor("cubes")
|
||||
List<ModelPart.Cube> lovisual$cubes();
|
||||
|
||||
@Accessor("children")
|
||||
Map<String, ModelPart> lovisual$children();
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package dev.loki.lovisual.mixins.optimize.entity.parallel;
|
||||
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import net.minecraft.client.renderer.feature.RenderTypeFeatureRenderer;
|
||||
import net.minecraft.client.renderer.rendertype.RenderType;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||
|
||||
/**
|
||||
* Stage 3 step 8: the per-render-type vertex builder the parallel emit hands
|
||||
* its raw vertices to — one builder call per model, exactly as vanilla's
|
||||
* prepareModel would make.
|
||||
*/
|
||||
@Mixin(RenderTypeFeatureRenderer.class)
|
||||
public interface RenderTypeFeatureRendererInvoker {
|
||||
|
||||
@Invoker("getVertexBuilder")
|
||||
VertexConsumer lovisual$vertexBuilder(RenderType renderType);
|
||||
}
|
||||
|
|
@ -22,8 +22,6 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||
* original runs and the result is stored; subsequent calls in the same tick
|
||||
* return the cached instance.
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.entity.EntityRendererNameMixin}
|
||||
* (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||
* Gated on the {@code microOpts} toggle via {@link OptimizeToggles#microOpts()}.
|
||||
* When the toggle is off, the original runs unchanged.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
|||
* renderers (model feature renderer, item feature renderer, future LOD mixins)
|
||||
* can reach the live entity.
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.entity.EntityRendererStateMixin}
|
||||
* (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||
* The original used {@code @ModifyReturnValue}; LoVisual uses {@code @Inject}
|
||||
* at RETURN with {@code CallbackInfoReturnable} so the mixin stays simple and
|
||||
* doesn't rely on the (more fragile across versions) MixinExtras return-value
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package dev.loki.lovisual.mixins.optimize.gl;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.misc.optimize.gl.RenderBackendOps;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Stage 3 step 2 (completes the TBO cache): only {@code GlBuffer.Direct}
|
||||
* deletes its GL handle; transient buffers are slices of a shared one. A
|
||||
* deleted buffer's id can be reused by a new buffer, so the attachment cache
|
||||
* must not outlive it — this hook clears the cache on every Direct close.
|
||||
*
|
||||
*
|
||||
* <p>Unconditional (clearing a tiny map on buffer close is free); correctness
|
||||
* beats the micro-saving of keeping stale attachments around.
|
||||
*/
|
||||
@Mixin(targets = "com.mojang.blaze3d.opengl.GlBuffer$Direct")
|
||||
public abstract class GlBufferCloseMixin {
|
||||
|
||||
@Inject(method = "close", at = @At("HEAD"))
|
||||
private void lovisual$forgetTboCache(CallbackInfo ci) {
|
||||
RenderBackendOps backend = RenderBackendOps.current();
|
||||
if (backend != null) {
|
||||
backend.forgetTboCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,8 +15,6 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||
* we don't pay for a {@code glBindFramebuffer(GL_FRAMEBUFFER, 0)} that's
|
||||
* immediately overwritten.
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.gl.GlCommandEncoderClearMixin} (ZakoOpt,
|
||||
* LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||
*
|
||||
* <p>Gated on the {@code microOpts} toggle via {@link RenderBackendOps#skipBindFramebufferZero(int)}.
|
||||
* When the toggle is off, the original call runs unchanged.
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||
* re-attaching makes the driver revalidate the texture (clouds: ~2% of a
|
||||
* frame in {@code glDrawElements}).
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.gl.GlCommandEncoderTexBufferMixin} (ZakoOpt,
|
||||
* LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||
*
|
||||
* <p>Gated on the {@code microOpts} toggle via
|
||||
* {@link RenderBackendOps#tboCacheSupported()}. When the toggle is off, both
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ import java.util.function.BiFunction;
|
|||
* items, etc.), the result depends only on the identifier, so we cache it
|
||||
* directly in a {@code ConcurrentHashMap}.
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.gl.RenderTypesMixin} (ZakoOpt, LGPL-3.0).
|
||||
* Original author: Zako — https://t.me/StarikZako. The original cache logic
|
||||
* is wrapped in {@link RenderBackendOps}; LoVisual's GL implementation is
|
||||
* {@code GlRenderBackendOps}, which gates on {@code microOpts} (Stage 1 toggle).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package dev.loki.lovisual.mixins.optimize.gl.fbo;
|
||||
|
||||
import com.mojang.blaze3d.opengl.DirectStateAccess;
|
||||
import com.mojang.blaze3d.opengl.FrameBufferAttachment;
|
||||
import com.mojang.blaze3d.opengl.FrameBufferCache;
|
||||
import dev.loki.lovisual.render.engine.optimize.gl.SharedDepthFbos;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Stage 3 step 2: feeds {@link SharedDepthFbos} from the vanilla FBO cache.
|
||||
* {@code getFbo} records "this depth texture lives in that FBO" (mip 0 only —
|
||||
* higher mips are never the depth attachment of the cached FBO);
|
||||
* {@code destroyFbo} drops the mapping before the FBO is deleted, because its
|
||||
* GL id may be reused by the next one.
|
||||
*
|
||||
*
|
||||
* <p>Bookkeeping only — runs regardless of the {@code fbo_share} toggle (a
|
||||
* map put/remove per FBO create/destroy is free); the toggle gates the clear
|
||||
* path in {@code GlCommandEncoderDepthClearMixin}.
|
||||
*/
|
||||
@Mixin(FrameBufferCache.class)
|
||||
public abstract class FrameBufferCacheMixin {
|
||||
|
||||
@Shadow
|
||||
@Final
|
||||
private Object2IntMap<FrameBufferCache.CacheKey> cache;
|
||||
|
||||
@Inject(method = "getFbo", at = @At("RETURN"))
|
||||
private void lovisual$rememberDepth(DirectStateAccess dsa,
|
||||
List<FrameBufferAttachment> colors,
|
||||
FrameBufferAttachment depth,
|
||||
CallbackInfoReturnable<Integer> cir) {
|
||||
if (depth != null && depth.fboMipLevel() == 0) {
|
||||
SharedDepthFbos.remember(depth.glId(), cir.getReturnValueI());
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "destroyFbo", at = @At("HEAD"))
|
||||
private void lovisual$forget(FrameBufferCache.CacheKey key, CallbackInfo ci) {
|
||||
if (cache.containsKey(key)) {
|
||||
SharedDepthFbos.forget(cache.getInt(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package dev.loki.lovisual.mixins.optimize.gl.fbo;
|
||||
|
||||
import com.mojang.blaze3d.opengl.GlTexture;
|
||||
import com.mojang.blaze3d.textures.GpuTexture;
|
||||
import dev.loki.lovisual.features.module.modules.misc.optimize.gl.RenderBackendOps;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Stage 3 step 2: vanilla attaches the depth texture to a scratch FBO,
|
||||
* clears, detaches and unbinds; a cached FBO (see {@code FrameBufferCacheMixin})
|
||||
* already holds the attachment, so the clear becomes one
|
||||
* {@code glClearNamedFramebufferfv}. Cancels the vanilla path when the
|
||||
* in-place clear ran.
|
||||
*
|
||||
*
|
||||
* <p>Gated on the {@code fbo_share} toggle via
|
||||
* {@link RenderBackendOps#clearDepthInPlace(int, double)}; a closed texture
|
||||
* or a missing DSA capability falls through to vanilla unchanged.
|
||||
*/
|
||||
@Mixin(targets = "com.mojang.blaze3d.opengl.GlCommandEncoder")
|
||||
public abstract class GlCommandEncoderDepthClearMixin {
|
||||
|
||||
@Inject(method = "clearDepthTexture", at = @At("HEAD"), cancellable = true)
|
||||
private void lovisual$clearInPlace(GpuTexture depth, double value, CallbackInfo ci) {
|
||||
if (!(depth instanceof GlTexture gl) || gl.isClosed()) {
|
||||
return;
|
||||
}
|
||||
RenderBackendOps backend = RenderBackendOps.current();
|
||||
if (backend != null && backend.clearDepthInPlace(gl.glId(), value)) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package dev.loki.lovisual.mixins.optimize.gl.raw;
|
||||
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import dev.loki.lovisual.render.engine.optimize.gl.RawBuffer;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
/**
|
||||
* Stage 3 step 7: what Sodium's {@code VertexBufferWriter} does for a
|
||||
* {@code BufferBuilder} — push ready-made vertex bytes straight into the
|
||||
* buffer — reimplemented on the vanilla builder so raw-vertex consumers
|
||||
* (AnimFreeze, ParallelModels) work without Sodium.
|
||||
*
|
||||
*/
|
||||
@Mixin(BufferBuilder.class)
|
||||
public abstract class BufferBuilderRawMixin implements RawBuffer {
|
||||
|
||||
@Shadow
|
||||
private int vertices;
|
||||
@Shadow
|
||||
@Final
|
||||
private int vertexSize;
|
||||
@Shadow
|
||||
private long vertexPointer;
|
||||
@Shadow
|
||||
@Final
|
||||
private ByteBufferBuilder buffer;
|
||||
@Shadow
|
||||
private int elementsToFill;
|
||||
@Shadow
|
||||
@Final
|
||||
private VertexFormat format;
|
||||
|
||||
@Override
|
||||
public boolean lovisual$accepts(VertexFormat format) {
|
||||
return format == this.format;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lovisual$push(long src, int count) {
|
||||
int length = count * vertexSize;
|
||||
long dst = buffer.reserve(length);
|
||||
MemoryUtil.memCopy(src, dst, length);
|
||||
vertices += count;
|
||||
// the pushed vertices are complete: the next addVertex must not see a half-filled one
|
||||
vertexPointer = dst + length - vertexSize;
|
||||
elementsToFill = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package dev.loki.lovisual.mixins.optimize.gl.raw;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
|
||||
import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
||||
/**
|
||||
* Stage 3 step 7: caches Sodium's {@code VertexConsumerUtils.convertOrLog}
|
||||
* result. The vanilla method is an interface instanceof plus an interface
|
||||
* call on a handful of alternating consumer classes, which keeps evicting
|
||||
* HotSpot's per-class type-check cache; an identity compare against the last
|
||||
* successful consumer skips both.
|
||||
*
|
||||
* Sodium-only: registered in {@code OPTIONAL_SODIUM_MIXINS}, never applied
|
||||
* without Sodium.
|
||||
*
|
||||
* <p>Gated on the {@code micro_opts} toggle. When the toggle is off, the
|
||||
* original method runs unchanged.
|
||||
*/
|
||||
@Mixin(targets = "net.caffeinemc.mods.sodium.client.render.vertex.VertexConsumerUtils", remap = false)
|
||||
public abstract class VertexConsumerUtilsMixin {
|
||||
|
||||
// last consumer that passed the check; a whole model goes to one consumer, so this hits for nearly every cuboid
|
||||
@Unique
|
||||
private static volatile Object lovisual$lastWriter;
|
||||
|
||||
@WrapMethod(method = "convertOrLog")
|
||||
private static VertexBufferWriter lovisual$cachedConvert(VertexConsumer consumer, Operation<VertexBufferWriter> original) {
|
||||
if (consumer == lovisual$lastWriter && OptimizeToggles.microOpts()) {
|
||||
return (VertexBufferWriter) consumer;
|
||||
}
|
||||
VertexBufferWriter writer = original.call(consumer);
|
||||
if (writer == consumer) {
|
||||
lovisual$lastWriter = consumer;
|
||||
}
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package dev.loki.lovisual.mixins.optimize.hud;
|
|||
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.misc.optimize.Optimize;
|
||||
import dev.loki.lovisual.render.engine.optimize.profile.DrawStats;
|
||||
import dev.loki.lovisual.render.engine.optimize.profile.FrameBudget;
|
||||
import dev.loki.lovisual.render.engine.optimize.profile.FrameProfiler;
|
||||
import dev.loki.lovisual.render.engine.optimize.profile.OptimizePropertyBridge;
|
||||
|
|
@ -65,12 +66,25 @@ public abstract class OptimizeDebugHudMixin {
|
|||
out.add(String.format("frame: %.2f ms (%.0f fps)",
|
||||
snap.frameMs(),
|
||||
1000.0 / Math.max(0.01, snap.frameMs())));
|
||||
out.add(String.format(" entities=%.2fms particles=%.2fms hud=%.2fms effects=%.2fms text=%.2fms",
|
||||
snap.entityMs(),
|
||||
snap.particleMs(),
|
||||
snap.hudMs(),
|
||||
snap.effectMs(),
|
||||
snap.textMs()));
|
||||
if (hasSectionTimings(snap)) {
|
||||
out.add(String.format(" entities=%.2fms particles=%.2fms hud=%.2fms effects=%.2fms text=%.2fms",
|
||||
snap.entityMs(),
|
||||
snap.particleMs(),
|
||||
snap.hudMs(),
|
||||
snap.effectMs(),
|
||||
snap.textMs()));
|
||||
}
|
||||
}
|
||||
|
||||
DrawStats.Snapshot counters = DrawStats.snapshot();
|
||||
if (counters.frames() > 0) {
|
||||
out.add(String.format("draws: %d, pipelines: %d, fbo binds: %d (avg/frame)",
|
||||
counters.draws(),
|
||||
counters.pipelines(),
|
||||
counters.binds()));
|
||||
if (!counters.top().isEmpty()) {
|
||||
out.add(" top: " + String.join(", ", counters.top()));
|
||||
}
|
||||
}
|
||||
|
||||
if (OptimizePropertyBridge.hasOverrides()) {
|
||||
|
|
@ -82,6 +96,15 @@ public abstract class OptimizeDebugHudMixin {
|
|||
return out;
|
||||
}
|
||||
|
||||
/** Sections are only reported once something opens them — a row of zeros is noise. */
|
||||
private static boolean hasSectionTimings(FrameProfiler.Snapshot snap) {
|
||||
return snap.entityMs() > 0
|
||||
|| snap.particleMs() > 0
|
||||
|| snap.hudMs() > 0
|
||||
|| snap.effectMs() > 0
|
||||
|| snap.textMs() > 0;
|
||||
}
|
||||
|
||||
private static String togglesSummary() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (OptimizeToggles.entityLod()) append(sb, "entity_lod@" + OptimizeToggles.entityLodDistance());
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||
* {@link BubbleColumnCache} first; on a miss, the original runs and the
|
||||
* result is stored.
|
||||
*
|
||||
* <p>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 BubbleColumnCache} so the mixin stays a thin
|
||||
* wrapper and the cache is unit-testable without Minecraft types.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
|||
/**
|
||||
* Stage 3: per-particle light cache + far-particle physics skip.
|
||||
*
|
||||
* <p>Two optimizations in one mixin (matches ZakoOpt):
|
||||
* <p>Two optimizations in one mixin:
|
||||
* <ul>
|
||||
* <li><b>Light cache</b> — vanilla {@code getLightCoords(partialTick)} walks
|
||||
* the chunk's light section data per call, called per particle per frame.
|
||||
|
|
@ -34,8 +34,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
|||
* duration of {@code move}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.particle.ParticleMixin} (ZakoOpt,
|
||||
* LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||
* Light-cache gated on the {@code microOpts} toggle (Stage 1 wiring);
|
||||
* physics-skip also gated on {@code microOpts} since both are micro-opts.
|
||||
* When the toggle is off, the original code runs unchanged.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package dev.loki.lovisual.mixins.optimize.stats;
|
||||
|
||||
import dev.loki.lovisual.render.engine.optimize.profile.DrawStats;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Counts the four places {@code GlCommandEncoder} actually issues a draw —
|
||||
* {@code executeDraw}, {@code executeDraws}, {@code executeDrawIndirect} and
|
||||
* {@code executeDrawMultiple} do not call each other, so exactly one of them
|
||||
* sees every submission.
|
||||
*
|
||||
* <p>Part of Stage 3 step 1 in {@code mod/docs/optimize-layer-design.md};
|
||||
* the counter is a boolean test unless F3 is open with Optimize on.
|
||||
*
|
||||
*/
|
||||
@Mixin(targets = "com.mojang.blaze3d.opengl.GlCommandEncoder")
|
||||
public abstract class GlCommandEncoderStatsMixin {
|
||||
|
||||
@Inject(method = "executeDraw", at = @At("HEAD"))
|
||||
private void lovisual$countDraw(CallbackInfo ci) {
|
||||
DrawStats.draw();
|
||||
}
|
||||
|
||||
@Inject(method = "executeDraws", at = @At("HEAD"))
|
||||
private void lovisual$countDraws(CallbackInfo ci) {
|
||||
DrawStats.draw();
|
||||
}
|
||||
|
||||
@Inject(method = "executeDrawIndirect", at = @At("HEAD"))
|
||||
private void lovisual$countDrawIndirect(CallbackInfo ci) {
|
||||
DrawStats.draw();
|
||||
}
|
||||
|
||||
@Inject(method = "executeDrawMultiple", at = @At("HEAD"))
|
||||
private void lovisual$countDrawMultiple(CallbackInfo ci) {
|
||||
DrawStats.draw();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package dev.loki.lovisual.mixins.optimize.stats;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import dev.loki.lovisual.render.engine.optimize.profile.DrawStats;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Counts {@code RenderPass.setPipeline} — one per state selection, repeats
|
||||
* included — and feeds the pipeline name to the per-frame top list, so F3
|
||||
* shows which pipelines a frame spends its draws on. That is the input for
|
||||
* the "sort draws by pipeline" idea in
|
||||
* {@code mod/docs/optimize-layer-design.md}.
|
||||
*
|
||||
* <p>Part of Stage 3 step 1 in {@code mod/docs/optimize-layer-design.md}.
|
||||
*
|
||||
*/
|
||||
@Mixin(targets = "com.mojang.blaze3d.opengl.GlRenderPass")
|
||||
public abstract class GlRenderPassStatsMixin {
|
||||
|
||||
@Inject(method = "setPipeline", at = @At("HEAD"))
|
||||
private void lovisual$countPipeline(RenderPipeline pipeline, CallbackInfo ci) {
|
||||
if (pipeline == null || pipeline.getLocation() == null) {
|
||||
return;
|
||||
}
|
||||
DrawStats.pipeline(pipeline.getLocation().getPath());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package dev.loki.lovisual.mixins.optimize.stats;
|
||||
|
||||
import com.mojang.blaze3d.opengl.GlStateManager;
|
||||
import dev.loki.lovisual.render.engine.optimize.profile.DrawStats;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Counts framebuffer binds that actually reach the driver: the injection sits
|
||||
* on the {@code glBindFramebuffer} calls inside {@code _glBindFramebuffer}, so
|
||||
* binds GlStateManager's read/write cache absorbs are not counted. A target of
|
||||
* {@code GL_FRAMEBUFFER} runs both branches and is counted twice — two GL calls
|
||||
* really happen.
|
||||
*
|
||||
* <p>Part of Stage 3 step 1 in {@code mod/docs/optimize-layer-design.md}.
|
||||
*
|
||||
*/
|
||||
@Mixin(GlStateManager.class)
|
||||
public abstract class GlStateManagerStatsMixin {
|
||||
|
||||
@Inject(
|
||||
method = "_glBindFramebuffer",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lorg/lwjgl/opengl/GL33C;glBindFramebuffer(II)V"
|
||||
)
|
||||
)
|
||||
private static void lovisual$countBind(int target, int framebuffer, CallbackInfo ci) {
|
||||
DrawStats.bind();
|
||||
}
|
||||
}
|
||||
|
|
@ -27,8 +27,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
|||
* entity was seen this frame.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Ported from {@code zako.opt.mixin.entity.LevelRendererOutlineMixin}
|
||||
* (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||
*
|
||||
* <p>Gated on the {@code microOpts} toggle via {@link OutlineTracker#enabled()}.
|
||||
* When the toggle is off, all hooks fall through to the vanilla call.
|
||||
|
|
@ -50,9 +48,9 @@ public abstract class LevelRendererOutlineMixin {
|
|||
// We can't tell from here whether 'texture' is the entity-outline target
|
||||
// (it's one of several cleared textures in addMainPass), so we mark the
|
||||
// frame as glowing unconditionally — the doEntityOutline inject will then
|
||||
// run. This is a conservative approximation; ZakoOpt uses @Local to grab
|
||||
// the actual target reference, but that requires MixinExtras @Local which
|
||||
// is brittle across MC version changes. The cost of the conservative path
|
||||
// run. This is a conservative approximation (grabbing the actual target
|
||||
// reference would need MixinExtras @Local, which is brittle across MC
|
||||
// version changes). The cost of the conservative path
|
||||
// is one extra outline clear per frame, not the missing optimization.
|
||||
OutlineTracker.markGlowing();
|
||||
original.call(encoder, texture, color);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,16 @@ package dev.loki.lovisual.mixins.render.item;
|
|||
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import dev.loki.lovisual.render.engine.optimize.entity.ItemLod;
|
||||
import java.util.List;
|
||||
import net.minecraft.client.renderer.feature.ItemFeatureRenderer;
|
||||
import net.minecraft.client.renderer.rendertype.RenderType;
|
||||
import net.minecraft.client.resources.model.geometry.BakedQuad;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import org.joml.Matrix4f;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
|
@ -56,4 +62,20 @@ public abstract class ItemFeatureRendererMixin {
|
|||
// corrupt the following world translucent composite, so item layers keep their own type.
|
||||
return original;
|
||||
}
|
||||
|
||||
@WrapOperation(
|
||||
method = {"prepareMainSubmit", "prepareOutlineSubmit", "prepareFoilSubmit"},
|
||||
at = @At(value = "INVOKE",
|
||||
target = "Lnet/minecraft/client/renderer/feature/ItemFeatureRenderer$Submit;quads()Ljava/util/List;")
|
||||
)
|
||||
private List<BakedQuad> lovisual$itemLod(ItemFeatureRenderer.Submit submit, Operation<List<BakedQuad>> original) {
|
||||
List<BakedQuad> quads = original.call(submit);
|
||||
ItemDisplayContext context = submit.displayContext();
|
||||
if (context.firstPerson() || context == ItemDisplayContext.GUI || !ItemLod.enabled()) {
|
||||
return quads;
|
||||
}
|
||||
Matrix4f m = submit.pose().pose();
|
||||
float dSq = m.m30() * m.m30() + m.m31() * m.m31() + m.m32() * m.m32();
|
||||
return dSq > ItemLod.distanceSq() ? ItemLod.flat(quads) : quads;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package dev.loki.lovisual.mixins.render.item.bounds;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.client.resources.model.cuboid.ItemTransform;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3fc;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
/**
|
||||
* Stage 3 step 5: exposes the layer internals {@link ItemStackRenderStateMixin}
|
||||
* keys the shared {@code ItemBounds} cache on — the extents supplier identity,
|
||||
* the item transform and the local matrix whose identity bit short-circuits
|
||||
* the cache.
|
||||
*/
|
||||
@Mixin(ItemStackRenderState.LayerRenderState.class)
|
||||
public interface ItemLayerAccessor {
|
||||
|
||||
@Accessor("extents")
|
||||
Supplier<Vector3fc[]> lovisual$extents();
|
||||
|
||||
@Accessor("itemTransform")
|
||||
ItemTransform lovisual$itemTransform();
|
||||
|
||||
@Accessor("localTransform")
|
||||
Matrix4f lovisual$localTransform();
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package dev.loki.lovisual.mixins.render.item.bounds;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import dev.loki.lovisual.render.engine.optimize.entity.ItemBounds;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import org.joml.Matrix4fc;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
/**
|
||||
* Stage 3 step 5: single-layer item model bounds served from
|
||||
* {@link ItemBounds} instead of recomputed every frame. The vanilla path
|
||||
* walks the layer's extents supplier; the cache key is that supplier's
|
||||
* identity, which is stable per baked model. Multi-layer items and items
|
||||
* with a non-identity local transform keep the vanilla computation.
|
||||
*/
|
||||
@Mixin(ItemStackRenderState.class)
|
||||
public abstract class ItemStackRenderStateMixin {
|
||||
|
||||
@Shadow
|
||||
private int activeLayerCount;
|
||||
|
||||
@Shadow
|
||||
private ItemStackRenderState.LayerRenderState[] layers;
|
||||
|
||||
@WrapMethod(method = "getModelBoundingBox")
|
||||
private AABB lovisual$sharedBounds(Operation<AABB> original) {
|
||||
if (!ItemBounds.enabled() || activeLayerCount != 1) {
|
||||
return original.call();
|
||||
}
|
||||
ItemLayerAccessor layer = (ItemLayerAccessor) layers[0];
|
||||
if ((layer.lovisual$localTransform().properties() & Matrix4fc.PROPERTY_IDENTITY) == 0) {
|
||||
return original.call();
|
||||
}
|
||||
return ItemBounds.get(layer.lovisual$extents(), layer.lovisual$itemTransform(), original::call);
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ import dev.loki.lovisual.features.module.modules.player.interact.NoDelay;
|
|||
import dev.loki.lovisual.features.module.modules.player.interact.NoInteract;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam;
|
||||
import dev.loki.lovisual.features.relations.StaffTracker;
|
||||
import dev.loki.lovisual.render.engine.optimize.entity.AnimFreeze;
|
||||
import dev.loki.lovisual.render.helpers.util.TickDelta;
|
||||
import dev.loki.lovisual.util.combat.tracking.AntiBotTracker;
|
||||
import dev.loki.lovisual.util.session.SessionChanger;
|
||||
|
|
@ -257,6 +258,11 @@ public class MinecraftMixin implements MinecraftGameConfigHolder {
|
|||
AntiBotTracker.INSTANCE.reset();
|
||||
}
|
||||
|
||||
@Inject(method = "renderFrame", at = @At("HEAD"))
|
||||
private void lovisual$animFreezeFrame(boolean tick, CallbackInfo ci) {
|
||||
AnimFreeze.frame++;
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "renderFrame",
|
||||
at = @At(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import dev.loki.lovisual.render.engine.core.policy.LightPolicy;
|
|||
import dev.loki.lovisual.render.engine.core.policy.fog.VanillaWorldFogProvider;
|
||||
import dev.loki.lovisual.render.engine.depth.WorldSceneDepth;
|
||||
import dev.loki.lovisual.render.engine.framegraph.LoVisualFrameGraph;
|
||||
import dev.loki.lovisual.render.engine.optimize.profile.FrameProfiler;
|
||||
import dev.loki.lovisual.render.engine.profiler.frame.FrameStutterProfiler;
|
||||
import dev.loki.lovisual.render.engine.profiler.frame.RenderFrameProfiler;
|
||||
import dev.loki.lovisual.render.engine.rhi.resource.LoVisualRhi;
|
||||
|
|
@ -220,6 +221,7 @@ public enum LoVisualRenderSystem {
|
|||
TextRenderSystem.beginFrame();
|
||||
sodiumFrame = SODIUM.beginFrame();
|
||||
RenderFrameProfiler.beginFrame(frameId);
|
||||
FrameProfiler.beginFrame();
|
||||
frameOpen = true;
|
||||
submissionEnded = false;
|
||||
lifecycle = FrameLifecycle.RECORDING;
|
||||
|
|
@ -319,6 +321,7 @@ public enum LoVisualRenderSystem {
|
|||
FrameStutterProfiler.onFramePresented(rhiStatsSnapshot(), uniformStatsSnapshot(), resourceStatsSnapshot());
|
||||
}
|
||||
RenderFrameProfiler.endFrame(null, null);
|
||||
FrameProfiler.endFrame();
|
||||
lifecycle = FrameLifecycle.PRESENTED;
|
||||
} finally {
|
||||
currentContext = null;
|
||||
|
|
|
|||
|
|
@ -8,9 +8,11 @@ package dev.loki.lovisual.render.engine.optimize;
|
|||
* future mixins) — never by the module class itself.
|
||||
*
|
||||
* <p>Stage 1 of {@code mod/docs/optimize-layer-design.md}: backend-independent
|
||||
* logic only. GL/Vulkan-specific toggles (frame fence, lazy clear, FBO share,
|
||||
* TBO cache, skin atlas, HUD cache) live in their own holders added in Stages
|
||||
* 3-4 alongside the mixins that consume them.
|
||||
* logic plus the toggle snapshot. Lazy clear and the TBO cache ride
|
||||
* {@link #microOpts()}; FBO share (Stage 3 step 2) has its own
|
||||
* {@link #fboShare()} flag. GL/Vulkan-specific helpers that need their own
|
||||
* holder (skin atlas, HUD cache) arrive in later Stage 3 steps alongside the
|
||||
* mixins that consume them.
|
||||
*
|
||||
* <p>All module settings are resolved into volatile primitive fields once per frame.
|
||||
* System-property overrides are not supported here (the {@code Optimize} module's
|
||||
|
|
@ -31,6 +33,7 @@ public enum OptimizeToggles {
|
|||
private static volatile boolean blockEntityCache;
|
||||
private static volatile boolean preparedTextCache;
|
||||
private static volatile boolean microOpts;
|
||||
private static volatile boolean fboShare;
|
||||
|
||||
public static boolean entityLod() { return entityLod; }
|
||||
public static int entityLodDistance() { return entityLodDistance; }
|
||||
|
|
@ -43,6 +46,7 @@ public enum OptimizeToggles {
|
|||
public static boolean blockEntityCache() { return blockEntityCache; }
|
||||
public static boolean preparedTextCache() { return preparedTextCache; }
|
||||
public static boolean microOpts() { return microOpts; }
|
||||
public static boolean fboShare() { return fboShare; }
|
||||
|
||||
/**
|
||||
* Push a full snapshot. Called by {@code Optimize.onEnable/onDisable/onFrame}
|
||||
|
|
@ -55,7 +59,8 @@ public enum OptimizeToggles {
|
|||
boolean spawnerTickCache,
|
||||
boolean blockEntityCache,
|
||||
boolean preparedTextCache,
|
||||
boolean microOpts) {
|
||||
boolean microOpts,
|
||||
boolean fboShare) {
|
||||
OptimizeToggles.entityLod = entityLod;
|
||||
OptimizeToggles.entityLodDistance = entityLodDistance;
|
||||
OptimizeToggles.particleLod = particleLod;
|
||||
|
|
@ -67,10 +72,11 @@ public enum OptimizeToggles {
|
|||
OptimizeToggles.blockEntityCache = blockEntityCache;
|
||||
OptimizeToggles.preparedTextCache = preparedTextCache;
|
||||
OptimizeToggles.microOpts = microOpts;
|
||||
OptimizeToggles.fboShare = fboShare;
|
||||
}
|
||||
|
||||
/** Reset to "nothing on" — used by {@code Optimize.onDisable}. */
|
||||
public static void reset() {
|
||||
apply(false, 0, false, false, false, false, 0, false, false, false, false);
|
||||
apply(false, 0, false, false, false, false, 0, false, false, false, false, false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,208 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.entity;
|
||||
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.MeshData;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import dev.loki.lovisual.features.module.modules.misc.optimize.entity.StateEntity;
|
||||
import dev.loki.lovisual.render.engine.optimize.gl.RawVertices;
|
||||
import net.minecraft.client.model.Model;
|
||||
import net.minecraft.client.renderer.entity.state.EntityRenderState;
|
||||
import net.minecraft.client.renderer.rendertype.RenderType;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Stage 3 step 4: far-entity animation freeze. A distant entity's model
|
||||
* vertices are captured once per game tick and, on the frames in between,
|
||||
* replayed as raw vertex copies shifted to the entity's current position —
|
||||
* the model is no longer rebuilt (and re-animated) every frame, only moved.
|
||||
*
|
||||
* <p>Distance gate: farther than {@link #distance()} blocks from the camera
|
||||
* (squared comparison against the pose translation). Close entities render
|
||||
* through the vanilla path untouched.
|
||||
*
|
||||
* <p>The capture is keyed per {@code (model, renderType, sprite)} triplet per
|
||||
* entity — one entity may submit the same model several times per frame
|
||||
* (body + armor layers); the n-th submission reuses the n-th capture. Parts
|
||||
* that are rebuilt every frame elsewhere (e.g. armor trim shimmer) are NOT
|
||||
* replayed — callers decide via {@link #freezes}, so those parts stay in sync
|
||||
* with the frozen rest instead of drifting.
|
||||
*
|
||||
* <p>Requires the raw-vertex push path ({@code RawVertices} + Sodium or the
|
||||
* vanilla {@code RawBuffer} mixin); when neither consumer accepts raw bytes,
|
||||
* {@link #render} returns {@code false} and vanilla runs.
|
||||
*
|
||||
* <p>Toggle: {@link #configure(boolean, int)}, pushed by the Optimize module
|
||||
* once per frame. The frame counter advances from the {@code renderFrame}
|
||||
* hook in {@code MinecraftMixin}.
|
||||
*/
|
||||
public enum AnimFreeze {
|
||||
;
|
||||
|
||||
/** Advances once per rendered frame; captures are reused within one frame. */
|
||||
public static volatile long frame;
|
||||
|
||||
private static final ByteBufferBuilder SCRATCH = new ByteBufferBuilder(16384);
|
||||
private static ByteBuffer emitBuffer = MemoryUtil.memAlloc(16384);
|
||||
|
||||
private static volatile boolean enabled;
|
||||
private static volatile int distance = 16;
|
||||
|
||||
/** Push a full snapshot — called by the Optimize module once per frame. */
|
||||
public static void configure(boolean enable, int distanceBlocks) {
|
||||
enabled = enable;
|
||||
distance = distanceBlocks;
|
||||
}
|
||||
|
||||
/** Reset to "off" — used by {@code Optimize.onDisable}. */
|
||||
public static void reset() {
|
||||
configure(false, 0);
|
||||
}
|
||||
|
||||
public static boolean enabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public static int distance() {
|
||||
return distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when this (state, pose) pair belongs to a far entity whose parts
|
||||
* must all replay the frozen capture — parts built every frame elsewhere
|
||||
* consult this so they don't drift off the frozen rest.
|
||||
*/
|
||||
public static boolean freezes(Object state, Matrix4f pose) {
|
||||
if (!enabled || !(state instanceof EntityRenderState s)
|
||||
|| ((StateEntity) s).lovisual$entity() == null) {
|
||||
return false;
|
||||
}
|
||||
float d = distance;
|
||||
return beyondDistance(pose.m30(), pose.m31(), pose.m32(), d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture-once-per-tick / replay-in-between for one model submission.
|
||||
*
|
||||
* @return {@code false} when not frozen (caller renders as usual)
|
||||
*/
|
||||
public static boolean render(Entity entity, Model<?> model, RenderType type, TextureAtlasSprite sprite,
|
||||
Matrix4f pose, VertexConsumer target, Consumer<VertexConsumer> draw) {
|
||||
float x = pose.m30(), y = pose.m31(), z = pose.m32();
|
||||
float d = distance;
|
||||
if (!enabled || !beyondDistance(x, y, z, d) || !RawVertices.accepts(target, type.format())) {
|
||||
return false;
|
||||
}
|
||||
AnimHolder owner = (AnimHolder) entity;
|
||||
Holder holder = owner.lovisual$anim();
|
||||
if (holder == null) {
|
||||
holder = new Holder();
|
||||
owner.lovisual$anim(holder);
|
||||
}
|
||||
Capture capture = find(holder, model, type, sprite);
|
||||
long tick = entity.level().getGameTime();
|
||||
if (capture.tick != tick) {
|
||||
BufferBuilder builder = new BufferBuilder(SCRATCH, type.primitiveTopology(), type.format());
|
||||
draw.accept(sprite == null ? builder : sprite.wrap(builder));
|
||||
capture.count = 0;
|
||||
try (MeshData mesh = builder.build()) {
|
||||
if (mesh != null) {
|
||||
ByteBuffer src = mesh.vertexBuffer();
|
||||
int bytes = src.remaining();
|
||||
if (capture.data == null || capture.data.capacity() < bytes) {
|
||||
capture.data = ByteBuffer.allocateDirect(bytes * 2);
|
||||
}
|
||||
MemoryUtil.memCopy(MemoryUtil.memAddress(src), MemoryUtil.memAddress0(capture.data), bytes);
|
||||
capture.count = mesh.drawState().vertexCount();
|
||||
}
|
||||
}
|
||||
capture.tick = tick;
|
||||
capture.x = x;
|
||||
capture.y = y;
|
||||
capture.z = z;
|
||||
}
|
||||
if (capture.count > 0) {
|
||||
emit(capture, type.format(), x - capture.x, y - capture.y, z - capture.z, target);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Squared distance from the origin — shared by {@link #freezes} and {@link #render}. */
|
||||
static boolean beyondDistance(float x, float y, float z, float dist) {
|
||||
return x * x + y * y + z * z > dist * dist;
|
||||
}
|
||||
|
||||
/** Per-entity bag of captures. */
|
||||
public static final class Holder {
|
||||
final List<Capture> captures = new ArrayList<>(4);
|
||||
|
||||
Holder() {
|
||||
}
|
||||
}
|
||||
|
||||
static final class Capture {
|
||||
Object model;
|
||||
RenderType type;
|
||||
TextureAtlasSprite sprite;
|
||||
int ordinal;
|
||||
long usedFrame = -1;
|
||||
long tick = Long.MIN_VALUE;
|
||||
ByteBuffer data;
|
||||
int count;
|
||||
float x, y, z;
|
||||
}
|
||||
|
||||
/** The same model may be submitted several times per entity; the n-th call this frame reuses the n-th capture. */
|
||||
private static Capture find(Holder holder, Object model, RenderType type, TextureAtlasSprite sprite) {
|
||||
int ordinal = 0;
|
||||
for (Capture c : holder.captures) {
|
||||
if (c.model == model && c.type == type && c.sprite == sprite && c.usedFrame == frame) {
|
||||
ordinal++;
|
||||
}
|
||||
}
|
||||
for (Capture c : holder.captures) {
|
||||
if (c.model == model && c.type == type && c.sprite == sprite && c.ordinal == ordinal) {
|
||||
c.usedFrame = frame;
|
||||
return c;
|
||||
}
|
||||
}
|
||||
Capture c = new Capture();
|
||||
c.model = model;
|
||||
c.type = type;
|
||||
c.sprite = sprite;
|
||||
c.ordinal = ordinal;
|
||||
c.usedFrame = frame;
|
||||
holder.captures.add(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
private static void emit(Capture capture, VertexFormat format, float dx, float dy, float dz, VertexConsumer target) {
|
||||
int stride = format.getVertexSize();
|
||||
int pos = format.getElement(DefaultVertexFormat.POSITION_SEMANTIC_NAME).offset();
|
||||
int size = capture.count * stride;
|
||||
if (emitBuffer.capacity() < size) {
|
||||
emitBuffer = MemoryUtil.memRealloc(emitBuffer, size * 2);
|
||||
}
|
||||
long dst = MemoryUtil.memAddress0(emitBuffer);
|
||||
MemoryUtil.memCopy(MemoryUtil.memAddress0(capture.data), dst, size);
|
||||
for (long v = dst + pos, end = dst + size; v < end; v += stride) {
|
||||
MemoryUtil.memPutFloat(v, MemoryUtil.memGetFloat(v) + dx);
|
||||
MemoryUtil.memPutFloat(v + 4, MemoryUtil.memGetFloat(v + 4) + dy);
|
||||
MemoryUtil.memPutFloat(v + 8, MemoryUtil.memGetFloat(v + 8) + dz);
|
||||
}
|
||||
try (MemoryStack stack = MemoryStack.stackPush()) {
|
||||
RawVertices.push(target, stack, dst, capture.count, format);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.entity;
|
||||
|
||||
/**
|
||||
* Mixin-contract: per-entity storage for frozen animation captures
|
||||
* ({@link AnimFreeze.Holder}). Implemented on {@code Entity} by
|
||||
* {@code EntityAnimHolderMixin}; render code reaches it through the
|
||||
* {@code EntityRenderState} back-reference ({@code StateEntity}).
|
||||
*/
|
||||
public interface AnimHolder {
|
||||
|
||||
AnimFreeze.Holder lovisual$anim();
|
||||
|
||||
void lovisual$anim(AnimFreeze.Holder holder);
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.entity;
|
||||
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import net.minecraft.client.resources.model.cuboid.ItemTransform;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import org.joml.Vector3fc;
|
||||
|
||||
/**
|
||||
* Stage 3 step 5: shared model bounds for single-layer items. Render states
|
||||
* are rebuilt every frame, so a cache stored on the state itself dies with
|
||||
* it; the extents supplier identity is stable per baked model and works as
|
||||
* a long-lived key here.
|
||||
*/
|
||||
public enum ItemBounds {
|
||||
;
|
||||
|
||||
private static final Map<Supplier<Vector3fc[]>, Entry> BOUNDS = new IdentityHashMap<>();
|
||||
|
||||
private static volatile boolean enabled;
|
||||
|
||||
public static void configure(boolean on) {
|
||||
enabled = on;
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
enabled = false;
|
||||
BOUNDS.clear();
|
||||
}
|
||||
|
||||
public static boolean enabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public static AABB get(Supplier<Vector3fc[]> extents, ItemTransform transform, Supplier<AABB> compute) {
|
||||
Entry e = BOUNDS.get(extents);
|
||||
if (e != null && e.transform() == transform) {
|
||||
return e.bounds();
|
||||
}
|
||||
AABB bounds = compute.get();
|
||||
// wholesale clear instead of LRU; models only change on resource reload
|
||||
if (BOUNDS.size() > 4096) {
|
||||
BOUNDS.clear();
|
||||
}
|
||||
BOUNDS.put(extents, new Entry(transform, bounds));
|
||||
return bounds;
|
||||
}
|
||||
|
||||
private record Entry(ItemTransform transform, AABB bounds) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.entity;
|
||||
|
||||
import com.google.common.collect.MapMaker;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.client.resources.model.geometry.BakedQuad;
|
||||
import net.minecraft.core.Direction;
|
||||
import org.joml.Vector3fc;
|
||||
|
||||
/**
|
||||
* Stage 3 step 5: far-item level of detail. Distant flat items (generated
|
||||
* item models with a few large Z faces plus thin edge strips) drop their
|
||||
* side geometry — at 8+ blocks the two big faces read the same on screen.
|
||||
*
|
||||
* <p>The quad list handed to the item renderer is a reused per-render-state
|
||||
* buffer, so the flat-list cache is keyed by the model's first quad with
|
||||
* weak keys; a model rebuild invalidates the entry for free.
|
||||
*/
|
||||
public enum ItemLod {
|
||||
;
|
||||
|
||||
private static final float EDGE = 1f / 16f + 1e-4f;
|
||||
private static final Map<BakedQuad, Flat> FLAT = new MapMaker().weakKeys().makeMap();
|
||||
|
||||
private static volatile boolean enabled;
|
||||
private static volatile float distanceSq;
|
||||
|
||||
public static void configure(boolean on, int distance) {
|
||||
enabled = on;
|
||||
distanceSq = (float) distance * (float) distance;
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
enabled = false;
|
||||
distanceSq = 0f;
|
||||
FLAT.clear();
|
||||
}
|
||||
|
||||
public static boolean enabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public static float distanceSq() {
|
||||
return distanceSq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the model's Z faces only when the whole model is flat (every
|
||||
* quad is an Z-axis face or thinner than 1/16 block) and has at least
|
||||
* eight quads; anything else is returned unchanged.
|
||||
*/
|
||||
public static List<BakedQuad> flat(List<BakedQuad> quads) {
|
||||
if (quads.isEmpty()) {
|
||||
return quads;
|
||||
}
|
||||
Flat cached = FLAT.get(quads.getFirst());
|
||||
if (cached == null || cached.sourceSize() != quads.size()) {
|
||||
cached = new Flat(quads.size(), compute(quads));
|
||||
FLAT.put(quads.getFirst(), cached);
|
||||
}
|
||||
return cached.quads() == null ? quads : cached.quads();
|
||||
}
|
||||
|
||||
private record Flat(int sourceSize, List<BakedQuad> quads) {
|
||||
}
|
||||
|
||||
private static List<BakedQuad> compute(List<BakedQuad> quads) {
|
||||
if (quads.size() < 8) {
|
||||
return null;
|
||||
}
|
||||
for (BakedQuad q : quads) {
|
||||
if (q.direction().getAxis() != Direction.Axis.Z && !thin(q)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
List<BakedQuad> faces = List.copyOf(
|
||||
quads.stream().filter(q -> q.direction().getAxis() == Direction.Axis.Z).toList());
|
||||
return faces.isEmpty() ? null : faces;
|
||||
}
|
||||
|
||||
private static boolean thin(BakedQuad q) {
|
||||
float minZ = Float.MAX_VALUE;
|
||||
float maxZ = -Float.MAX_VALUE;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Vector3fc p = q.position(i);
|
||||
minZ = Math.min(minZ, p.z());
|
||||
maxZ = Math.max(maxZ, p.z());
|
||||
}
|
||||
return maxZ - minZ <= EDGE;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.gl;
|
||||
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
|
||||
/**
|
||||
* Stage 3 step 7: a {@code BufferBuilder} that accepts ready-made vertex
|
||||
* bytes pushed straight from memory — what Sodium's {@code VertexBufferWriter}
|
||||
* does, reimplemented for the vanilla builder so optimizations work without
|
||||
* Sodium.
|
||||
*
|
||||
* Implemented on {@code BufferBuilder} by {@code BufferBuilderRawMixin}.
|
||||
*/
|
||||
public interface RawBuffer {
|
||||
|
||||
/** True when this buffer's vertex format is exactly {@code format}. */
|
||||
boolean lovisual$accepts(VertexFormat format);
|
||||
|
||||
/**
|
||||
* Copy {@code count} whole vertices from {@code src} (a native address)
|
||||
* into the buffer.
|
||||
*/
|
||||
void lovisual$push(long src, int count);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.gl;
|
||||
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
|
||||
/**
|
||||
* Stage 3 step 7: dispatches ready vertex bytes into a consumer — through
|
||||
* Sodium's {@code VertexBufferWriter} when Sodium is installed, else into a
|
||||
* vanilla {@code BufferBuilder} via {@link RawBuffer}.
|
||||
*
|
||||
*
|
||||
* <p>Optional-dependency pattern: when Sodium is absent the {@code SODIUM}
|
||||
* flag short-circuits before {@link SodiumVertices} is ever touched, so the
|
||||
* Sodium API classes are never loaded.
|
||||
*/
|
||||
public enum RawVertices {
|
||||
;
|
||||
|
||||
private static final boolean SODIUM = FabricLoader.getInstance().isModLoaded("sodium");
|
||||
|
||||
/** True when this consumer can receive raw vertices in {@code format}. */
|
||||
public static boolean accepts(VertexConsumer consumer, VertexFormat format) {
|
||||
return SODIUM
|
||||
? SodiumVertices.accepts(consumer)
|
||||
: consumer instanceof RawBuffer raw && raw.lovisual$accepts(format);
|
||||
}
|
||||
|
||||
/** Push {@code count} whole vertices from native address {@code src}. */
|
||||
public static void push(VertexConsumer consumer, MemoryStack stack, long src, int count, VertexFormat format) {
|
||||
if (SODIUM) {
|
||||
SodiumVertices.push(consumer, stack, src, count, format);
|
||||
} else {
|
||||
((RawBuffer) consumer).lovisual$push(src, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.gl;
|
||||
|
||||
import com.mojang.blaze3d.opengl.GlStateManager;
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
|
||||
import org.lwjgl.opengl.ARBDirectStateAccess;
|
||||
import org.lwjgl.opengl.GL;
|
||||
import org.lwjgl.opengl.GL11C;
|
||||
|
||||
/**
|
||||
* Stage 3 step 2: depth-texture id → FBO that already has that texture
|
||||
* attached at mip 0. Vanilla's {@code clearDepthTexture} attaches the depth
|
||||
* texture to a scratch FBO, clears, detaches and unbinds every time; with a
|
||||
* cached FBO the clear is a single {@code glClearNamedFramebufferfv}.
|
||||
*
|
||||
*
|
||||
* <p>{@link #remember}/{@link #forget} are driven by
|
||||
* {@code mixins/optimize/gl/fbo/FrameBufferCacheMixin} on every
|
||||
* {@code getFbo}/{@code destroyFbo}; {@link #clear} is called (gated on the
|
||||
* {@code fbo_share} toggle) from {@code GlCommandEncoderDepthClearMixin}.
|
||||
* Without GL 4.5 / {@code ARB_direct_state_access} the map still tracks, but
|
||||
* {@code clear} returns {@code false} and the vanilla path runs.
|
||||
*
|
||||
* <p>GL-only: on Vulkan this class is never reached (the mixins that feed it
|
||||
* target {@code com.mojang.blaze3d.opengl.*}).
|
||||
*/
|
||||
public enum SharedDepthFbos {
|
||||
;
|
||||
|
||||
private static final Int2IntOpenHashMap FBO_BY_DEPTH = new Int2IntOpenHashMap();
|
||||
private static final float[] VALUE = new float[1];
|
||||
private static Boolean dsa;
|
||||
|
||||
/** Remember that {@code fbo} has {@code depthId} attached at mip 0. */
|
||||
public static void remember(int depthId, int fbo) {
|
||||
FBO_BY_DEPTH.put(depthId, fbo);
|
||||
}
|
||||
|
||||
/** Drop every mapping pointing at {@code fbo} — its id may be reused by the next FBO. */
|
||||
public static void forget(int fbo) {
|
||||
FBO_BY_DEPTH.values().removeIf(f -> f == fbo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the depth texture in place through its cached FBO.
|
||||
*
|
||||
* @param depthId the GL id of the depth texture
|
||||
* @param depth the clear value (0..1)
|
||||
* @return {@code true} when the clear ran (caller should skip vanilla);
|
||||
* {@code false} to fall through to the vanilla attach/clear/detach
|
||||
*/
|
||||
public static boolean clear(int depthId, double depth) {
|
||||
int fbo = FBO_BY_DEPTH.getOrDefault(depthId, 0);
|
||||
if (fbo == 0) {
|
||||
return false;
|
||||
}
|
||||
if (dsa == null) {
|
||||
dsa = GL.getCapabilities().OpenGL45 || GL.getCapabilities().GL_ARB_direct_state_access;
|
||||
}
|
||||
if (!dsa) {
|
||||
return false;
|
||||
}
|
||||
// glClearNamedFramebuffer obeys the depth write mask and scissor test,
|
||||
// like glClear in the vanilla path — but make both states explicit.
|
||||
GlStateManager._depthMask(true);
|
||||
GlStateManager._disableScissorTest();
|
||||
VALUE[0] = (float) depth;
|
||||
ARBDirectStateAccess.glClearNamedFramebufferfv(fbo, GL11C.GL_DEPTH, 0, VALUE);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Test helpers (headless-safe: never touch GL) ---
|
||||
|
||||
/** Test-only: drop all mappings and the cached DSA capability. */
|
||||
static void resetForTesting() {
|
||||
FBO_BY_DEPTH.clear();
|
||||
dsa = null;
|
||||
}
|
||||
|
||||
/** Test-only: number of tracked depth textures. */
|
||||
static int sizeForTesting() {
|
||||
return FBO_BY_DEPTH.size();
|
||||
}
|
||||
|
||||
/** Test-only: the FBO currently mapped for {@code depthId}, or 0. */
|
||||
static int fboForTesting(int depthId) {
|
||||
return FBO_BY_DEPTH.getOrDefault(depthId, 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.gl;
|
||||
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
|
||||
/**
|
||||
* Stage 3 step 7: the only class touching Sodium's {@code VertexBufferWriter}
|
||||
* API — kept separate so it is loaded only when Sodium is installed
|
||||
* ({@link RawVertices} never references it on the vanilla path).
|
||||
*
|
||||
*/
|
||||
final class SodiumVertices {
|
||||
|
||||
// tryOf is an interface instanceof plus an interface call, which thrashes HotSpot's
|
||||
// per-class type check cache when a handful of consumer classes alternate; most calls
|
||||
// ask about the same buffer as the last one
|
||||
private static Object lastWriter;
|
||||
|
||||
private SodiumVertices() {
|
||||
}
|
||||
|
||||
static boolean accepts(VertexConsumer consumer) {
|
||||
if (consumer == lastWriter) {
|
||||
return true;
|
||||
}
|
||||
if (VertexBufferWriter.tryOf(consumer) == null) {
|
||||
return false;
|
||||
}
|
||||
lastWriter = consumer;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void push(VertexConsumer consumer, MemoryStack stack, long src, int count, VertexFormat format) {
|
||||
(consumer == lastWriter
|
||||
? (VertexBufferWriter) consumer
|
||||
: VertexBufferWriter.of(consumer)).push(stack, src, count, format);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.hud;
|
||||
|
||||
import com.mojang.blaze3d.GpuFormat;
|
||||
import com.mojang.blaze3d.PrimitiveTopology;
|
||||
import com.mojang.blaze3d.pipeline.BlendFunction;
|
||||
import com.mojang.blaze3d.pipeline.ColorTargetState;
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import com.mojang.blaze3d.pipeline.RenderTarget;
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.systems.RenderPass;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.textures.FilterMode;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalDouble;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.BindGroupLayouts;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/**
|
||||
* Stage 3 step 6: the HUD is drawn into its own texture at the monitor
|
||||
* refresh rate; frames in between only blend that texture in. Crosshair and
|
||||
* camera overlays (vignette, pumpkin...) blend with the world, so they stay
|
||||
* live every frame.
|
||||
*
|
||||
* <p>Disabled outright when a foreign HUD cache (axolotlclient, exordium,
|
||||
* gnetum) or VulkanMod is loaded — two caches stacked on one HUD break each
|
||||
* other. Off by default: the LoVisual deferred-HUD marker path draws its own
|
||||
* ranges straight into the main target and does not go through the cache.
|
||||
*/
|
||||
public enum HudCache {
|
||||
;
|
||||
|
||||
// the HUD texture holds premultiplied colour: GUI blending onto transparent black
|
||||
// leaves colour * alpha; write mask 7 = RGB only, alpha is not written
|
||||
private static final RenderPipeline COMPOSITE = RenderPipeline.builder()
|
||||
.withBindGroupLayout(BindGroupLayouts.GLOBALS)
|
||||
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hud_composite"))
|
||||
.withVertexShader("core/screenquad")
|
||||
.withFragmentShader("core/blit_screen")
|
||||
.withBindGroupLayout(BindGroupLayouts.IN_SAMPLER)
|
||||
.withColorTargetState(new ColorTargetState(
|
||||
Optional.of(BlendFunction.TRANSLUCENT_PREMULTIPLIED_ALPHA), GpuFormat.RGBA8_UNORM, 7))
|
||||
.withPrimitiveTopology(PrimitiveTopology.TRIANGLES)
|
||||
.build();
|
||||
|
||||
private static final Vector4f CLEAR = new Vector4f(0, 0, 0, 0);
|
||||
private static final boolean FOREIGN_HUD = isModLoaded("axolotlclient")
|
||||
|| isModLoaded("exordium")
|
||||
|| isModLoaded("gnetum");
|
||||
private static final boolean VULKAN = isModLoaded("vulkanmod");
|
||||
|
||||
private static TextureTarget target;
|
||||
// the game's own main target, taken before the HUD: mods (Xaero's minimap) swap
|
||||
// their own one in while they draw
|
||||
private static RenderTarget main;
|
||||
private static volatile boolean enabled;
|
||||
private static boolean active;
|
||||
private static boolean refresh;
|
||||
private static boolean drawing;
|
||||
private static long lastRefresh;
|
||||
private static volatile boolean suppressLiveParts;
|
||||
|
||||
public static boolean suppressLiveParts() {
|
||||
return suppressLiveParts;
|
||||
}
|
||||
|
||||
public static void suppressLiveParts(boolean value) {
|
||||
suppressLiveParts = value;
|
||||
}
|
||||
|
||||
public static void configure(boolean on) {
|
||||
enabled = on && !FOREIGN_HUD && !VULKAN;
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
enabled = false;
|
||||
active = false;
|
||||
refresh = false;
|
||||
drawing = false;
|
||||
suppressLiveParts = false;
|
||||
lastRefresh = 0;
|
||||
main = null;
|
||||
if (target != null) {
|
||||
target.destroyBuffers();
|
||||
target = null;
|
||||
}
|
||||
HudWorth.reset();
|
||||
MonitorRate.reset();
|
||||
}
|
||||
|
||||
// once per frame, before any GUI extraction
|
||||
public static void beginFrame(boolean renderLevel) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
boolean was = active;
|
||||
long now = System.nanoTime();
|
||||
active = enabled
|
||||
&& HudWorth.update(now, MonitorRate.intervalNs(now))
|
||||
&& renderLevel && mc.level != null
|
||||
&& mc.gui.screen() == null && mc.gui.overlay() == null;
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
main = mc.gameRenderer.mainRenderTarget();
|
||||
int w = mc.getWindow().getWidth();
|
||||
int h = mc.getWindow().getHeight();
|
||||
refresh = !was || target == null || target.width != w || target.height != h
|
||||
|| now - lastRefresh >= MonitorRate.intervalNs(now);
|
||||
if (refresh) {
|
||||
lastRefresh = now;
|
||||
if (target == null) {
|
||||
target = new TextureTarget("lovisual hud", w, h, true, GpuFormat.RGBA8_UNORM);
|
||||
} else if (target.width != w || target.height != h) {
|
||||
target.resize(w, h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean active() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public static boolean skipExtraction() {
|
||||
return active && !refresh;
|
||||
}
|
||||
|
||||
public static RenderTarget drawTarget(RenderTarget requested) {
|
||||
return drawing && requested == main ? target : null;
|
||||
}
|
||||
|
||||
// depth is cleared to 0: 26.x renders with reversed depth
|
||||
public static void drawIntoTexture(Runnable draw) {
|
||||
RenderSystem.getDevice().createCommandEncoder()
|
||||
.clearColorAndDepthTextures(target.getColorTexture(), CLEAR, target.getDepthTexture(), 0.0);
|
||||
drawing = true;
|
||||
try {
|
||||
draw.run();
|
||||
} finally {
|
||||
drawing = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void composite() {
|
||||
RenderTarget main = Minecraft.getInstance().gameRenderer.mainRenderTarget();
|
||||
try (RenderPass pass = RenderSystem.getDevice().createCommandEncoder()
|
||||
.createRenderPass(() -> "lovisual hud composite", main.getColorTextureView(), Optional.empty(),
|
||||
main.useDepth ? main.getDepthTextureView() : null, OptionalDouble.empty())) {
|
||||
pass.setPipeline(COMPOSITE);
|
||||
RenderSystem.bindDefaultUniforms(pass);
|
||||
pass.bindTexture("InSampler", target.getColorTextureView(),
|
||||
RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST));
|
||||
pass.draw(3, 1, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isModLoaded(String id) {
|
||||
return FabricLoader.getInstance().isModLoaded(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.hud;
|
||||
|
||||
/**
|
||||
* Stage 3 step 6: the HUD cache only saves work when several frames share one
|
||||
* HUD refresh; near one frame per refresh it redraws the HUD anyway and just
|
||||
* adds a full-screen clear and blend, which costs real FPS on integrated GPUs
|
||||
* and 60 Hz monitors. A hysteresis band (off below 1.5, on above 2.0 frames
|
||||
* per refresh) keeps it from flipping every frame.
|
||||
*/
|
||||
public enum HudWorth {
|
||||
;
|
||||
|
||||
private static final double OFF_BELOW = 1.5;
|
||||
private static final double ON_ABOVE = 2.0;
|
||||
|
||||
private static double frameNs;
|
||||
private static long last;
|
||||
private static volatile boolean worth = true;
|
||||
|
||||
public static boolean update(long now, long refreshNs) {
|
||||
long dt = now - last;
|
||||
last = now;
|
||||
if (dt > 0 && dt < 1_000_000_000L) {
|
||||
frameNs = frameNs == 0 ? dt : frameNs * 0.95 + dt * 0.05;
|
||||
}
|
||||
double framesPerRefresh = frameNs == 0 ? ON_ABOVE : refreshNs / frameNs;
|
||||
if (worth ? framesPerRefresh < OFF_BELOW : framesPerRefresh > ON_ABOVE) {
|
||||
worth = !worth;
|
||||
}
|
||||
return worth;
|
||||
}
|
||||
|
||||
public static boolean worth() {
|
||||
return worth;
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
frameNs = 0;
|
||||
last = 0;
|
||||
worth = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.hud;
|
||||
|
||||
import com.mojang.blaze3d.platform.Monitor;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
import java.util.function.IntSupplier;
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
||||
/**
|
||||
* Stage 3 step 6: nanoseconds per refresh of the monitor the window is on.
|
||||
* The GLFW query runs at most once a second — the window may have moved to
|
||||
* another monitor, so the value is re-probed on that interval.
|
||||
*/
|
||||
public enum MonitorRate {
|
||||
;
|
||||
|
||||
private static final long DEFAULT_INTERVAL = 1_000_000_000L / 144;
|
||||
|
||||
private static volatile long intervalNs = DEFAULT_INTERVAL;
|
||||
private static long nextCheck;
|
||||
private static volatile IntSupplier probe = MonitorRate::glfwRefreshRate;
|
||||
|
||||
public static long intervalNs(long now) {
|
||||
if (now >= nextCheck) {
|
||||
int hz = probe.getAsInt();
|
||||
long interval = 1_000_000_000L / Math.max(30, hz);
|
||||
if (interval != intervalNs) {
|
||||
DebugLog.info("Monitor refresh rate {} Hz", hz);
|
||||
}
|
||||
intervalNs = interval;
|
||||
nextCheck = now + 1_000_000_000L;
|
||||
}
|
||||
return intervalNs;
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
intervalNs = DEFAULT_INTERVAL;
|
||||
nextCheck = 0;
|
||||
probe = MonitorRate::glfwRefreshRate;
|
||||
}
|
||||
|
||||
static void setProbeForTesting(IntSupplier testProbe) {
|
||||
probe = testProbe;
|
||||
}
|
||||
|
||||
private static int glfwRefreshRate() {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc == null || mc.getWindow() == null) {
|
||||
return 144;
|
||||
}
|
||||
Monitor monitor = mc.getWindow().findBestMonitor();
|
||||
return monitor != null ? monitor.currentMode().getRefreshRate() : 144;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.parallel;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import dev.loki.lovisual.render.engine.optimize.parallel.ModelGeometry.Cube;
|
||||
import dev.loki.lovisual.render.engine.optimize.parallel.ParallelModels.Job;
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.util.Mth;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Quaternionf;
|
||||
import org.joml.Vector3f;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
/**
|
||||
* Writes one posed model's vertices into native memory, mirroring
|
||||
* {@code ModelPart.render}/{@code translateAndRotate}/{@code Cube.compile}
|
||||
* but reading the per-model pose snapshot instead of the shared part instance
|
||||
* — that is what makes the build safe off the render thread.
|
||||
*/
|
||||
final class ModelBuilder {
|
||||
|
||||
static final int STRIDE;
|
||||
static final int FIELDS = 9;
|
||||
|
||||
static {
|
||||
VertexFormat format = ParallelModels.FORMAT;
|
||||
STRIDE = format.getVertexSize();
|
||||
}
|
||||
|
||||
private ModelBuilder() {
|
||||
}
|
||||
|
||||
static void build(Job job) {
|
||||
PoseStack poses = ParallelModels.stacks();
|
||||
poses.last().set(job.matrix);
|
||||
long[] ptr = {job.address};
|
||||
for (int i = 0; i < job.geometry.parts.length; ) {
|
||||
i = part(job, poses, i, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
private static int normal(float c) {
|
||||
return (int) (Mth.clamp(c, -1.0F, 1.0F) * 127.0F) & 0xFF;
|
||||
}
|
||||
|
||||
private static int part(Job job, PoseStack poses, int i, long[] ptr) {
|
||||
ModelGeometry geometry = job.geometry;
|
||||
int end = geometry.end[i];
|
||||
if ((job.flags[i] & 1) == 0 || geometry.empty[i]) {
|
||||
return end;
|
||||
}
|
||||
float[] p = job.pose;
|
||||
int o = i * FIELDS;
|
||||
poses.pushPose();
|
||||
poses.translate(p[o] / 16.0F, p[o + 1] / 16.0F, p[o + 2] / 16.0F);
|
||||
if (p[o + 3] != 0.0F || p[o + 4] != 0.0F || p[o + 5] != 0.0F) {
|
||||
// what PoseStack.mulPose does; spelled out for the worker thread
|
||||
Quaternionf rotation = new Quaternionf().rotationZYX(p[o + 5], p[o + 4], p[o + 3]);
|
||||
poses.last().pose().rotate(rotation);
|
||||
poses.last().normal().rotate(rotation);
|
||||
}
|
||||
if (p[o + 6] != 1.0F || p[o + 7] != 1.0F || p[o + 8] != 1.0F) {
|
||||
poses.scale(p[o + 6], p[o + 7], p[o + 8]);
|
||||
}
|
||||
if ((job.flags[i] & 2) == 0) {
|
||||
PoseStack.Pose pose = poses.last();
|
||||
Matrix4f matrix = pose.pose();
|
||||
Vector3f v = new Vector3f();
|
||||
// what SpriteCoordinateExpander does to every vertex; the sprite's
|
||||
// bounds are final, safe to read here
|
||||
TextureAtlasSprite sprite = job.sprite;
|
||||
for (Cube cube : geometry.cubes[i]) {
|
||||
// a cube's 24 vertices share 8 corners: transform the corners once
|
||||
float[] at = ParallelModels.corners();
|
||||
if (at.length < cube.corners.length) {
|
||||
at = new float[cube.corners.length];
|
||||
ParallelModels.setCorners(at);
|
||||
}
|
||||
for (int c = 0; c < cube.corners.length; c += 3) {
|
||||
matrix.transformPosition(cube.corners[c], cube.corners[c + 1], cube.corners[c + 2], v);
|
||||
at[c] = v.x();
|
||||
at[c + 1] = v.y();
|
||||
at[c + 2] = v.z();
|
||||
}
|
||||
int k = 0;
|
||||
for (int q = 0; q < cube.sizes.length; q++) {
|
||||
Vector3f n = pose.transformNormal(cube.normals[q * 3], cube.normals[q * 3 + 1], cube.normals[q * 3 + 2], v);
|
||||
int packedNormal = normal(n.x()) | normal(n.y()) << 8 | normal(n.z()) << 16;
|
||||
for (int e = k + cube.sizes[q]; k < e; k++) {
|
||||
int c = cube.index[k] * 3;
|
||||
float u = cube.uv[k * 2];
|
||||
float w = cube.uv[k * 2 + 1];
|
||||
if (sprite != null) {
|
||||
u = sprite.getU(u);
|
||||
w = sprite.getV(w);
|
||||
}
|
||||
long v0 = ptr[0];
|
||||
MemoryUtil.memPutFloat(v0, at[c]);
|
||||
MemoryUtil.memPutFloat(v0 + 4, at[c + 1]);
|
||||
MemoryUtil.memPutFloat(v0 + 8, at[c + 2]);
|
||||
MemoryUtil.memPutInt(v0 + 12, job.color);
|
||||
MemoryUtil.memPutFloat(v0 + 16, u);
|
||||
MemoryUtil.memPutFloat(v0 + 20, w);
|
||||
MemoryUtil.memPutInt(v0 + 24, job.overlay);
|
||||
MemoryUtil.memPutInt(v0 + 28, job.light);
|
||||
MemoryUtil.memPutInt(v0 + 32, packedNormal);
|
||||
ptr[0] += STRIDE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int c = i + 1; c < end; ) {
|
||||
c = part(job, poses, c, ptr);
|
||||
}
|
||||
poses.popPose();
|
||||
return end;
|
||||
}
|
||||
|
||||
static int countVertices(ModelGeometry geometry, Job job) {
|
||||
int vertices = 0;
|
||||
for (int i = 0; i < geometry.parts.length; ) {
|
||||
if ((job.flags[i] & 1) == 0) {
|
||||
i = geometry.end[i];
|
||||
continue;
|
||||
}
|
||||
if ((job.flags[i] & 2) == 0) {
|
||||
vertices += geometry.vertices[i];
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return vertices;
|
||||
}
|
||||
|
||||
static void snapshotPose(ModelPart part, Job job, int i) {
|
||||
int o = i * FIELDS;
|
||||
job.pose[o] = part.x;
|
||||
job.pose[o + 1] = part.y;
|
||||
job.pose[o + 2] = part.z;
|
||||
job.pose[o + 3] = part.xRot;
|
||||
job.pose[o + 4] = part.yRot;
|
||||
job.pose[o + 5] = part.zRot;
|
||||
job.pose[o + 6] = part.xScale;
|
||||
job.pose[o + 7] = part.yScale;
|
||||
job.pose[o + 8] = part.zScale;
|
||||
job.flags[i] = (byte) ((part.visible ? 1 : 0) | (part.skipDraw ? 2 : 0));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.parallel;
|
||||
|
||||
import dev.loki.lovisual.mixins.optimize.entity.parallel.ModelPartAccessor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
|
||||
/**
|
||||
* The baked part tree of one model in render order — the static half of the
|
||||
* parallel model build: which parts exist, where each subtree ends and the
|
||||
* cube geometry folded onto distinct corners. Cached per model root in
|
||||
* {@link ParallelModels}' weak map, rebuilt only on resource reload.
|
||||
*/
|
||||
final class ModelGeometry {
|
||||
|
||||
final ModelPart[] parts;
|
||||
/** end[i] is where the subtree of part i stops in the flat order. */
|
||||
final int[] end;
|
||||
final int[] vertices;
|
||||
final boolean[] empty;
|
||||
final Cube[][] cubes;
|
||||
|
||||
ModelGeometry(ModelPart root) {
|
||||
List<ModelPart> list = new ArrayList<>();
|
||||
List<Integer> ends = new ArrayList<>();
|
||||
add(root, list, ends);
|
||||
int n = list.size();
|
||||
parts = list.toArray(new ModelPart[0]);
|
||||
end = new int[n];
|
||||
vertices = new int[n];
|
||||
empty = new boolean[n];
|
||||
cubes = new Cube[n][];
|
||||
for (int i = 0; i < n; i++) {
|
||||
ModelPartAccessor accessor = (ModelPartAccessor) (Object) parts[i];
|
||||
end[i] = ends.get(i);
|
||||
List<ModelPart.Cube> source = accessor.lovisual$cubes();
|
||||
cubes[i] = new Cube[source.size()];
|
||||
for (int c = 0; c < cubes[i].length; c++) {
|
||||
cubes[i][c] = new Cube(source.get(c));
|
||||
vertices[i] += cubes[i][c].index.length;
|
||||
}
|
||||
empty[i] = cubes[i].length == 0 && accessor.lovisual$children().isEmpty();
|
||||
}
|
||||
// drop the strong ref from the value back to the weak-map key, or the
|
||||
// root model part would never be collected (leak fix)
|
||||
parts[0] = null;
|
||||
}
|
||||
|
||||
Map<String, ModelPart> children(ModelPart part) {
|
||||
return ((ModelPartAccessor) (Object) part).lovisual$children();
|
||||
}
|
||||
|
||||
private void add(ModelPart part, List<ModelPart> list, List<Integer> ends) {
|
||||
int at = list.size();
|
||||
list.add(part);
|
||||
ends.add(0);
|
||||
for (ModelPart child : children(part).values()) {
|
||||
add(child, list, ends);
|
||||
}
|
||||
ends.set(at, list.size());
|
||||
}
|
||||
|
||||
/** One cube in vanilla's polygon and vertex order, folded onto distinct corners. */
|
||||
static final class Cube {
|
||||
final float[] corners;
|
||||
final int[] index;
|
||||
final float[] uv;
|
||||
final float[] normals;
|
||||
final int[] sizes;
|
||||
|
||||
Cube(ModelPart.Cube cube) {
|
||||
List<Float> corner = new ArrayList<>();
|
||||
List<Integer> idx = new ArrayList<>();
|
||||
List<Float> uvs = new ArrayList<>();
|
||||
normals = new float[cube.polygons.length * 3];
|
||||
sizes = new int[cube.polygons.length];
|
||||
for (int q = 0; q < cube.polygons.length; q++) {
|
||||
ModelPart.Polygon polygon = cube.polygons[q];
|
||||
normals[q * 3] = polygon.normal().x();
|
||||
normals[q * 3 + 1] = polygon.normal().y();
|
||||
normals[q * 3 + 2] = polygon.normal().z();
|
||||
sizes[q] = polygon.vertices().length;
|
||||
for (ModelPart.Vertex vertex : polygon.vertices()) {
|
||||
int found = -1;
|
||||
for (int c = 0; c < corner.size(); c += 3) {
|
||||
if (corner.get(c) == vertex.worldX()
|
||||
&& corner.get(c + 1) == vertex.worldY()
|
||||
&& corner.get(c + 2) == vertex.worldZ()) {
|
||||
found = c / 3;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found < 0) {
|
||||
found = corner.size() / 3;
|
||||
corner.add(vertex.worldX());
|
||||
corner.add(vertex.worldY());
|
||||
corner.add(vertex.worldZ());
|
||||
}
|
||||
idx.add(found);
|
||||
uvs.add(vertex.u());
|
||||
uvs.add(vertex.v());
|
||||
}
|
||||
}
|
||||
corners = new float[corner.size()];
|
||||
for (int c = 0; c < corners.length; c++) {
|
||||
corners[c] = corner.get(c);
|
||||
}
|
||||
index = idx.stream().mapToInt(Integer::intValue).toArray();
|
||||
uv = new float[uvs.size()];
|
||||
for (int c = 0; c < uv.length; c++) {
|
||||
uv[c] = uvs.get(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.parallel;
|
||||
|
||||
import dev.loki.lovisual.mixins.optimize.entity.parallel.ModelFeatureRendererInvoker;
|
||||
import dev.loki.lovisual.mixins.optimize.entity.parallel.RenderTypeFeatureRendererInvoker;
|
||||
import dev.loki.lovisual.render.engine.optimize.entity.AnimFreeze;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
|
||||
/**
|
||||
* Stage 3 step 8: drives one parallel batch per model group. 26.2 hands
|
||||
* models over in groups that are already ordered and batched, so a whole
|
||||
* group is one parallel run; groups below {@link ParallelModels#MIN_SUBMITS}
|
||||
* and submits that cannot go raw (block cracks, non-entity formats, frozen
|
||||
* animations) fall back to the vanilla path per model.
|
||||
*/
|
||||
public final class ParallelModelHook {
|
||||
|
||||
private static final List<ParallelModels.Job> JOBS = new ArrayList<>();
|
||||
|
||||
private ParallelModelHook() {
|
||||
}
|
||||
|
||||
/** false: too few models in this group, vanilla builds it. */
|
||||
public static boolean buildGroup(ModelFeatureRenderer renderer, List<ModelFeatureRenderer.Submit<?>> submits) {
|
||||
if (!ParallelModels.enabled() || submits.size() < ParallelModels.MIN_SUBMITS) {
|
||||
return false;
|
||||
}
|
||||
JOBS.clear();
|
||||
ParallelModels.begin();
|
||||
try {
|
||||
for (ModelFeatureRenderer.Submit<?> s : submits) {
|
||||
// block cracks go through a decal generator: those few go the vanilla way
|
||||
JOBS.add(s.sheetedDecalPose() != null || s.renderType().format() != ParallelModels.FORMAT
|
||||
|| AnimFreeze.freezes(s.state(), s.pose().pose())
|
||||
? null
|
||||
: ParallelModels.add(s.model(), s.state(), s.pose(), s.lightCoords(), s.overlayCoords(),
|
||||
s.tintedColor(), s.sprite()));
|
||||
}
|
||||
} finally {
|
||||
ParallelModels.end();
|
||||
}
|
||||
RenderTypeFeatureRendererInvoker builders = (RenderTypeFeatureRendererInvoker) renderer;
|
||||
ModelFeatureRendererInvoker vanilla = (ModelFeatureRendererInvoker) renderer;
|
||||
try (MemoryStack stack = MemoryStack.stackPush()) {
|
||||
for (int k = 0; k < submits.size(); k++) {
|
||||
ModelFeatureRenderer.Submit<?> s = submits.get(k);
|
||||
ParallelModels.Job job = JOBS.get(k);
|
||||
// one builder call per model, as vanilla does: it decides where a new draw starts
|
||||
if (job == null || !ParallelModels.emit(job, builders.lovisual$vertexBuilder(s.renderType()), stack)) {
|
||||
vanilla.lovisual$prepareModel(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.parallel;
|
||||
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
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.gl.RawVertices;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.model.Model;
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
/**
|
||||
* Stage 3 step 8: model vertices built on several threads. setupAnim writes
|
||||
* the pose into a model instance shared by every entity of that kind, so the
|
||||
* render thread poses each model and snapshots its parts, workers build the
|
||||
* vertices from the snapshots while the next models are posed, and the render
|
||||
* thread then hands them to the buffers in vanilla's order. Driven per group
|
||||
* by {@code ModelFeatureRendererMixin.buildGroup} through
|
||||
* {@link ParallelModelHook}.
|
||||
*
|
||||
* <p>Mutually exclusive with {@link AnimFreeze} (freeze replays captured
|
||||
* vertices, this builds fresh ones). Off with fewer than four cores or an
|
||||
* active Iris shader pack.
|
||||
*/
|
||||
public enum ParallelModels {
|
||||
;
|
||||
|
||||
public static final VertexFormat FORMAT = DefaultVertexFormat.ENTITY;
|
||||
public static final int MIN_SUBMITS = 24;
|
||||
private static final int PAGE = 1 << 20;
|
||||
private static final boolean CORES_OK = Runtime.getRuntime().availableProcessors() >= 4;
|
||||
|
||||
private static final Map<ModelPart, ModelGeometry> GEOMETRY = new WeakHashMap<>();
|
||||
private static final List<Job> BUILT = new ArrayList<>();
|
||||
private static final ThreadLocal<PoseStack> STACKS = ThreadLocal.withInitial(PoseStack::new);
|
||||
private static final ThreadLocal<float[]> CORNERS = ThreadLocal.withInitial(() -> new float[24]);
|
||||
// vertex memory handed out while models are still being posed, so the total
|
||||
// is not known up front; reused every frame
|
||||
private static final List<Long> PAGES = new ArrayList<>();
|
||||
private static final List<Integer> PAGE_SIZES = new ArrayList<>();
|
||||
private static int page;
|
||||
private static int pageUsed;
|
||||
private static Workers.Stream stream;
|
||||
private static volatile boolean enabled;
|
||||
|
||||
public static void configure(boolean on) {
|
||||
enabled = on;
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
enabled = false;
|
||||
stream = null;
|
||||
BUILT.clear();
|
||||
page = 0;
|
||||
pageUsed = 0;
|
||||
for (int i = 0; i < PAGES.size(); i++) {
|
||||
MemoryUtil.nmemFree(PAGES.get(i));
|
||||
}
|
||||
PAGES.clear();
|
||||
PAGE_SIZES.clear();
|
||||
GEOMETRY.clear();
|
||||
}
|
||||
|
||||
public static boolean enabled() {
|
||||
return enabled && CORES_OK && !AnimFreeze.enabled() && !irisShaderPack();
|
||||
}
|
||||
|
||||
public static void begin() {
|
||||
BUILT.clear();
|
||||
page = 0;
|
||||
pageUsed = 0;
|
||||
stream = Workers.stream(i -> ModelBuilder.build(BUILT.get(i)));
|
||||
}
|
||||
|
||||
// render thread: poses the model and queues its vertices; the caller has
|
||||
// already checked the vertex format and that nothing else (outline, block
|
||||
// cracks) has to see the same model
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public static Job add(Model model, Object state, PoseStack.Pose pose, int light, int overlay,
|
||||
int color, TextureAtlasSprite sprite) {
|
||||
model.setupAnim(state);
|
||||
ModelPart root = model.root();
|
||||
ModelGeometry geometry = GEOMETRY.computeIfAbsent(root, ModelGeometry::new);
|
||||
int n = geometry.parts.length;
|
||||
Job job = new Job(geometry, pose, light, overlay,
|
||||
Integer.reverseBytes(Integer.rotateLeft(color, 8)), sprite, new float[n * ModelBuilder.FIELDS], new byte[n]);
|
||||
for (int i = 0; i < n; i++) {
|
||||
ModelPart p = i == 0 ? root : geometry.parts[i];
|
||||
ModelBuilder.snapshotPose(p, job, i);
|
||||
}
|
||||
job.vertices = ModelBuilder.countVertices(geometry, job);
|
||||
job.address = reserve(job.vertices * ModelBuilder.STRIDE);
|
||||
BUILT.add(job);
|
||||
stream.publish(BUILT.size());
|
||||
return job;
|
||||
}
|
||||
|
||||
// must run even when posing failed: helpers spin until the stream is finished
|
||||
public static void end() {
|
||||
Throwable error = stream.finish(BUILT.size());
|
||||
stream = null;
|
||||
if (error != null) {
|
||||
throw new RuntimeException("lovisual parallel models", error);
|
||||
}
|
||||
}
|
||||
|
||||
// false: the consumer cannot take raw vertices, the caller renders this model the vanilla way
|
||||
public static boolean emit(Job job, VertexConsumer consumer, MemoryStack stack) {
|
||||
if (job.vertices == 0) {
|
||||
return true;
|
||||
}
|
||||
if (!RawVertices.accepts(consumer, FORMAT)) {
|
||||
return false;
|
||||
}
|
||||
stack.push();
|
||||
try {
|
||||
RawVertices.push(consumer, stack, job.address, job.vertices, FORMAT);
|
||||
} finally {
|
||||
stack.pop();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static PoseStack stacks() {
|
||||
return STACKS.get();
|
||||
}
|
||||
|
||||
static float[] corners() {
|
||||
return CORNERS.get();
|
||||
}
|
||||
|
||||
static void setCorners(float[] at) {
|
||||
CORNERS.set(at);
|
||||
}
|
||||
|
||||
private static long reserve(int bytes) {
|
||||
if (page < PAGES.size() && pageUsed + bytes > PAGE_SIZES.get(page)) {
|
||||
page++;
|
||||
pageUsed = 0;
|
||||
}
|
||||
if (page == PAGES.size()) {
|
||||
int size = Math.max(PAGE, bytes);
|
||||
PAGES.add(MemoryUtil.nmemAlloc(size));
|
||||
PAGE_SIZES.add(size);
|
||||
} else if (bytes > PAGE_SIZES.get(page)) {
|
||||
MemoryUtil.nmemFree(PAGES.get(page));
|
||||
PAGES.set(page, MemoryUtil.nmemAlloc(bytes));
|
||||
PAGE_SIZES.set(page, bytes);
|
||||
}
|
||||
long address = PAGES.get(page) + pageUsed;
|
||||
pageUsed += bytes;
|
||||
return address;
|
||||
}
|
||||
|
||||
private static boolean irisShaderPack() {
|
||||
try {
|
||||
if (!FabricLoader.getInstance().isModLoaded("iris")) {
|
||||
return false;
|
||||
}
|
||||
return net.irisshaders.iris.api.v0.IrisApi.getInstance().isShaderPackInUse();
|
||||
} catch (LinkageError | RuntimeException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Job {
|
||||
final ModelGeometry geometry;
|
||||
final PoseStack.Pose matrix;
|
||||
final int light;
|
||||
final int overlay;
|
||||
final int color;
|
||||
final TextureAtlasSprite sprite;
|
||||
final float[] pose;
|
||||
final byte[] flags;
|
||||
int vertices;
|
||||
long address;
|
||||
|
||||
Job(ModelGeometry geometry, PoseStack.Pose matrix, int light, int overlay, int color,
|
||||
TextureAtlasSprite sprite, float[] pose, byte[] flags) {
|
||||
this.geometry = geometry;
|
||||
this.matrix = matrix;
|
||||
this.light = light;
|
||||
this.overlay = overlay;
|
||||
this.color = color;
|
||||
this.sprite = sprite;
|
||||
this.pose = pose;
|
||||
this.flags = flags;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.profile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Per-frame draw counters for the F3 overlay — step 1 of Stage 3 in
|
||||
* {@code mod/docs/optimize-layer-design.md}.
|
||||
*
|
||||
* <p>Answers the question the "additional GL optimisations" section asks
|
||||
* before anything gets turned on: how many draw calls, pipeline selections
|
||||
* and framebuffer binds a frame really costs, and which pipelines dominate
|
||||
* them. Sorting draws, batching HUD and dropping redundant FBO switches are
|
||||
* all decisions that need these three numbers first.
|
||||
*
|
||||
* <p>Counting stays off unless the Optimize module is on <em>and</em> F3 is
|
||||
* open, so on a normal frame each hook is a single boolean test.
|
||||
*
|
||||
*
|
||||
* <p>Render-thread only: counters are touched by the thread that draws the
|
||||
* frame, the published snapshot is one volatile field so the F3 read may
|
||||
* happen anywhere.
|
||||
*/
|
||||
public final class DrawStats {
|
||||
|
||||
/** Length of one measurement window — averages are published once a second. */
|
||||
static final long WINDOW_NS = 1_000_000_000L;
|
||||
|
||||
/** How many pipelines are listed in {@link Snapshot#top()}. */
|
||||
static final int TOP = 5;
|
||||
|
||||
/**
|
||||
* One second of counting, already averaged per frame.
|
||||
*
|
||||
* @param draws GL draw submissions
|
||||
* @param pipelines {@code RenderPass.setPipeline} calls — state changes
|
||||
* @param binds {@code glBindFramebuffer} calls that reached the driver
|
||||
* @param frames frames in the window; {@code 0} means "nothing measured yet"
|
||||
* @param top busiest pipelines, busiest first, as {@code "<path> <per-frame avg>"}
|
||||
*/
|
||||
public record Snapshot(int draws, int pipelines, int binds, int frames, List<String> top) {
|
||||
public Snapshot {
|
||||
top = List.copyOf(top);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Snapshot EMPTY = new Snapshot(0, 0, 0, 0, List.of());
|
||||
|
||||
private static volatile Snapshot snapshot = EMPTY;
|
||||
|
||||
private static boolean active;
|
||||
private static int draws;
|
||||
private static int pipelines;
|
||||
private static int binds;
|
||||
private static int frames;
|
||||
private static long windowStartNs;
|
||||
private static final Map<String, Integer> BY_PIPELINE = new HashMap<>();
|
||||
|
||||
private DrawStats() {
|
||||
}
|
||||
|
||||
public static void draw() {
|
||||
if (active) draws++;
|
||||
}
|
||||
|
||||
/** @param location pipeline location path, already cut down to a short name */
|
||||
public static void pipeline(String location) {
|
||||
if (active) {
|
||||
pipelines++;
|
||||
BY_PIPELINE.merge(location, 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
|
||||
public static void bind() {
|
||||
if (active) binds++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the frame that just finished. {@code shown} is false while F3 is
|
||||
* closed or the module is off — the first false call stops counting and
|
||||
* empties the published snapshot, the next true call starts a fresh window.
|
||||
*/
|
||||
public static void endFrame(boolean shown) {
|
||||
endFrame(shown, System.nanoTime());
|
||||
}
|
||||
|
||||
static void endFrame(boolean shown, long nowNs) {
|
||||
if (!shown) {
|
||||
if (active) {
|
||||
active = false;
|
||||
snapshot = EMPTY;
|
||||
BY_PIPELINE.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!active) {
|
||||
active = true;
|
||||
startWindow(nowNs);
|
||||
return;
|
||||
}
|
||||
frames++;
|
||||
if (nowNs - windowStartNs >= WINDOW_NS) {
|
||||
snapshot = publish(draws, pipelines, binds, frames, BY_PIPELINE, TOP);
|
||||
startWindow(nowNs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Averages the window's counters over its frames and picks the busiest
|
||||
* pipelines. Pure — the part of {@link #endFrame(boolean, long)} worth a test.
|
||||
*
|
||||
* @param byPipeline pipeline location → calls in the window, unordered
|
||||
* @param topN how many entries {@link Snapshot#top()} keeps
|
||||
*/
|
||||
static Snapshot publish(int draws, int pipelines, int binds, int frames,
|
||||
Map<String, Integer> byPipeline, int topN) {
|
||||
int perFrame = Math.max(frames, 1);
|
||||
List<String> top = new ArrayList<>(Math.min(topN, byPipeline.size()));
|
||||
byPipeline.entrySet().stream()
|
||||
.sorted((a, b) -> Integer.compare(b.getValue(), a.getValue()))
|
||||
.limit(topN)
|
||||
.forEach(e -> top.add(e.getKey() + " " + avg(e.getValue(), perFrame)));
|
||||
return new Snapshot(avg(draws, perFrame), avg(pipelines, perFrame), avg(binds, perFrame), frames, top);
|
||||
}
|
||||
|
||||
private static int avg(int total, int frames) {
|
||||
return (int) Math.round(total / (double) frames);
|
||||
}
|
||||
|
||||
/** Latest published window. */
|
||||
public static Snapshot snapshot() {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static void startWindow(long nowNs) {
|
||||
draws = pipelines = binds = frames = 0;
|
||||
BY_PIPELINE.clear();
|
||||
windowStartNs = nowNs;
|
||||
}
|
||||
|
||||
/** Test-only: stop counting, drop counters and the published snapshot. */
|
||||
static void reset() {
|
||||
active = false;
|
||||
snapshot = EMPTY;
|
||||
startWindow(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ public enum FrameBudget {
|
|||
/** Multiplier applied to baseline caps when AGGRESSIVE mode is on. */
|
||||
static final double AGGRESSIVE_SCALE = 0.6;
|
||||
|
||||
// Defaults match ZakoOpt's stage-1 caps so the behavior is familiar.
|
||||
// Stage-1 baseline caps.
|
||||
private static final int BASE_ENTITY_LOD_DISTANCE = 35;
|
||||
private static final int BASE_PARTICLE_NEAR_BLOCKS = 16; // NEAR_SQ = 16²
|
||||
private static final int BASE_PARTICLE_MID_BLOCKS = 32; // MID_SQ = 32²
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import java.util.Map;
|
|||
* <li>{@code -Dlovisual.opt.block_entity_cache=true|false}</li>
|
||||
* <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.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>
|
||||
|
|
@ -47,8 +48,7 @@ public enum OptimizePropertyBridge {
|
|||
|
||||
/**
|
||||
* Read the current {@code -Dlovisual.opt.*} properties into a fresh snapshot.
|
||||
* The Optimize module calls this once per frame (mirrors ZakoOpt's
|
||||
* {@code ZakoOptConfig.refresh()}).
|
||||
* The Optimize module calls this once per frame.
|
||||
*/
|
||||
public static Snapshot resolve() {
|
||||
Snapshot s = new Snapshot();
|
||||
|
|
@ -61,6 +61,7 @@ public enum OptimizePropertyBridge {
|
|||
s.blockEntityCache = boolOverride("block_entity_cache");
|
||||
s.preparedTextCache = boolOverride("prepared_text_cache");
|
||||
s.microOpts = boolOverride("micro_opts");
|
||||
s.fboShare = boolOverride("fbo_share");
|
||||
|
||||
s.entityLodDistance = intOverride("entity_lod_distance", -1);
|
||||
s.spawnerDistance = intOverride("spawner_distance", -1);
|
||||
|
|
@ -93,7 +94,7 @@ 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",
|
||||
"block_entity_cache", "prepared_text_cache", "micro_opts", "fbo_share",
|
||||
"entity_lod_distance", "spawner_distance", "budget_mode", "target_fps"
|
||||
};
|
||||
|
||||
|
|
@ -144,6 +145,7 @@ public enum OptimizePropertyBridge {
|
|||
public Boolean blockEntityCache;
|
||||
public Boolean preparedTextCache;
|
||||
public Boolean microOpts;
|
||||
public Boolean fboShare;
|
||||
public int entityLodDistance = -1;
|
||||
public int spawnerDistance = -1;
|
||||
public FrameBudget.Mode budgetMode; // null = use module value
|
||||
|
|
@ -183,6 +185,7 @@ public enum OptimizePropertyBridge {
|
|||
m.put("block_entity_cache", blockEntityCache);
|
||||
m.put("prepared_text_cache", preparedTextCache);
|
||||
m.put("micro_opts", microOpts);
|
||||
m.put("fbo_share", fboShare);
|
||||
m.put("entity_lod_distance", entityLodDistance);
|
||||
m.put("spawner_distance", spawnerDistance);
|
||||
m.put("budget_mode", budgetMode);
|
||||
|
|
|
|||
|
|
@ -37,18 +37,31 @@
|
|||
"optimize.block.BlockEntityMixin",
|
||||
"optimize.block.BlockEntityRenderStateMixin",
|
||||
"optimize.block.ChestRendererMixin",
|
||||
"optimize.entity.EntityNameMixin",
|
||||
"optimize.entity.EntityRenderStateEntityMixin",
|
||||
"optimize.entity.LivingEntityLodMixin",
|
||||
"optimize.entity.SodiumEntityRendererMixin",
|
||||
"optimize.entity.EntityNameMixin",
|
||||
"optimize.entity.EntityRenderStateEntityMixin",
|
||||
"optimize.entity.LivingEntityLodMixin",
|
||||
"optimize.entity.SodiumEntityRendererMixin",
|
||||
"optimize.entity.anim.EntityAnimHolderMixin",
|
||||
"optimize.entity.anim.ModelFeatureRendererMixin",
|
||||
"optimize.entity.parallel.ModelFeatureRendererInvoker",
|
||||
"optimize.entity.parallel.ModelPartAccessor",
|
||||
"optimize.entity.parallel.RenderTypeFeatureRendererInvoker",
|
||||
"optimize.entity.renderer.EntityRendererNameMixin",
|
||||
"optimize.entity.renderer.EntityRendererStateMixin",
|
||||
"optimize.gl.GlCommandEncoderClearMixin",
|
||||
"optimize.gl.GlCommandEncoderTexBufferMixin",
|
||||
"optimize.gl.RenderTypesMixin",
|
||||
"optimize.gl.GlBufferCloseMixin",
|
||||
"optimize.gl.GlCommandEncoderClearMixin",
|
||||
"optimize.gl.GlCommandEncoderTexBufferMixin",
|
||||
"optimize.gl.RenderTypesMixin",
|
||||
"optimize.gl.fbo.FrameBufferCacheMixin",
|
||||
"optimize.gl.fbo.GlCommandEncoderDepthClearMixin",
|
||||
"optimize.gl.raw.BufferBuilderRawMixin",
|
||||
"optimize.gl.raw.VertexConsumerUtilsMixin",
|
||||
"optimize.hud.OptimizeDebugHudMixin",
|
||||
"optimize.particle.BubbleColumnUpParticleMixin",
|
||||
"optimize.particle.ParticleMixin",
|
||||
"optimize.stats.GlCommandEncoderStatsMixin",
|
||||
"optimize.stats.GlRenderPassStatsMixin",
|
||||
"optimize.stats.GlStateManagerStatsMixin",
|
||||
"optimize.world.LevelRendererOutlineMixin",
|
||||
"accessors.blaze3d.GlTextureInvoker",
|
||||
"accessors.blaze3d.GpuDeviceAccessor",
|
||||
|
|
@ -129,6 +142,11 @@
|
|||
"hud.GuiGraphicsMixin",
|
||||
"hud.GuiMixin",
|
||||
"hud.GuiRendererMixin",
|
||||
"hud.cache.GameRendererHudMixin",
|
||||
"hud.cache.GuiHudMixin",
|
||||
"hud.cache.GuiRendererTargetMixin",
|
||||
"hud.cache.HudLiveMixin",
|
||||
"hud.cache.invoker.HudInvoker",
|
||||
"hud.accessibility.NarratorManagerMixin",
|
||||
"hud.accessibility.OptionsNarratorMixin",
|
||||
"hud.bars.BossHealthOverlayMixin",
|
||||
|
|
@ -172,6 +190,8 @@
|
|||
"render.fog.WaterFogEnvironmentMixin",
|
||||
"render.fogmod.PowderSnowFogModifierMixin",
|
||||
"render.item.EquipmentLayerRendererMixin",
|
||||
"render.item.bounds.ItemLayerAccessor",
|
||||
"render.item.bounds.ItemStackRenderStateMixin",
|
||||
"render.models.AvatarCustomModelMixin",
|
||||
"render.models.LivingEntityCustomModelMixin",
|
||||
"render.placement.submit.BlockEntityPlaceMixin",
|
||||
|
|
|
|||
|
|
@ -52,21 +52,21 @@ public final class SpawnerCacheTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
assertFalse(SpawnerCache.enabled());
|
||||
|
||||
OptimizeToggles.apply(
|
||||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
true, false, false, false);
|
||||
true, false, false, false, false);
|
||||
assertTrue(SpawnerCache.enabled());
|
||||
|
||||
OptimizeToggles.apply(
|
||||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
assertFalse(SpawnerCache.enabled());
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ public final class SpawnerCacheTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
true, false, false, false);
|
||||
true, false, false, false, false);
|
||||
FakeSpawner spawner = new FakeSpawner();
|
||||
assertNull(SpawnerCache.get(spawner, 100L));
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ public final class SpawnerCacheTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
true, false, false, false);
|
||||
true, false, false, false, false);
|
||||
FakeSpawner spawner = new FakeSpawner();
|
||||
EntityRenderState state = new EntityRenderState();
|
||||
SpawnerCache.put(spawner, 42L, state);
|
||||
|
|
@ -100,7 +100,7 @@ public final class SpawnerCacheTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
true, false, false, false);
|
||||
true, false, false, false, false);
|
||||
FakeSpawner spawner = new FakeSpawner();
|
||||
EntityRenderState state = new EntityRenderState();
|
||||
SpawnerCache.put(spawner, 42L, state);
|
||||
|
|
@ -114,7 +114,7 @@ public final class SpawnerCacheTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
true, false, false, false);
|
||||
true, false, false, false, false);
|
||||
FakeSpawner spawner = new FakeSpawner();
|
||||
EntityRenderState first = new EntityRenderState();
|
||||
EntityRenderState second = new EntityRenderState();
|
||||
|
|
@ -130,7 +130,7 @@ public final class SpawnerCacheTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
true, false, false, false);
|
||||
true, false, false, false, false);
|
||||
FakeSpawner spawner = new FakeSpawner();
|
||||
EntityRenderState state = new EntityRenderState();
|
||||
SpawnerCache.put(spawner, 100L, state);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public final class EntityLodTest {
|
|||
false, 35,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
assertFalse(EntityLod.far(10_000_000));
|
||||
assertFalse(EntityLod.farPlayer(10_000_000));
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ public final class EntityLodTest {
|
|||
true, 35,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
// d=35 -> d²=1225: 1225 is NOT greater than 1225, so not far.
|
||||
assertFalse(EntityLod.far(35 * 35));
|
||||
// d² just above threshold: should be far.
|
||||
|
|
@ -48,7 +48,7 @@ public final class EntityLodTest {
|
|||
true, 16,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
assertEquals(16 * 16, EntityLod.farThresholdSq(), 0.0001);
|
||||
assertFalse(EntityLod.far(16 * 16));
|
||||
assertTrue(EntityLod.far(16 * 16 + 1));
|
||||
|
|
@ -60,7 +60,7 @@ public final class EntityLodTest {
|
|||
true, 35,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
// Stage 1 collapses player LOD into entity LOD — same toggle, same threshold.
|
||||
for (double dSq : new double[]{0, 100, 1224, 1226, 10_000}) {
|
||||
assertEquals(EntityLod.far(dSq), EntityLod.farPlayer(dSq),
|
||||
|
|
@ -74,14 +74,14 @@ public final class EntityLodTest {
|
|||
true, 35,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
assertTrue(EntityLod.far(10_000));
|
||||
// Toggle off mid-test — same distance should now be "not far".
|
||||
OptimizeToggles.apply(
|
||||
false, 35,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
assertFalse(EntityLod.far(10_000));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ public final class GlRenderBackendOpsTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
RenderBackendOps ops = RenderBackendOps.current();
|
||||
assertFalse(ops.lazyClearSupported());
|
||||
assertFalse(ops.skipBindFramebufferZero(0));
|
||||
|
|
@ -48,7 +48,7 @@ public final class GlRenderBackendOpsTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, true);
|
||||
false, false, false, true, false);
|
||||
RenderBackendOps ops = RenderBackendOps.current();
|
||||
assertTrue(ops.lazyClearSupported());
|
||||
assertTrue(ops.skipBindFramebufferZero(0));
|
||||
|
|
@ -62,7 +62,7 @@ public final class GlRenderBackendOpsTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
RenderBackendOps ops = RenderBackendOps.current();
|
||||
assertFalse(ops.tboCacheSupported());
|
||||
// Even after marking attached, lookup should be a no-op (returns false because not supported).
|
||||
|
|
@ -76,7 +76,7 @@ public final class GlRenderBackendOpsTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, true);
|
||||
false, false, false, true, false);
|
||||
RenderBackendOps ops = RenderBackendOps.current();
|
||||
ops.trackBoundTboTexture(42);
|
||||
assertFalse(ops.tboAlreadyAttached(0x8229, 100),
|
||||
|
|
@ -92,7 +92,7 @@ public final class GlRenderBackendOpsTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, true);
|
||||
false, false, false, true, false);
|
||||
RenderBackendOps ops = RenderBackendOps.current();
|
||||
ops.trackBoundTboTexture(42);
|
||||
ops.markTboAttached(0x8229, 100);
|
||||
|
|
@ -108,7 +108,7 @@ public final class GlRenderBackendOpsTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, true);
|
||||
false, false, false, true, false);
|
||||
RenderBackendOps ops = RenderBackendOps.current();
|
||||
ops.trackBoundTboTexture(42);
|
||||
ops.markTboAttached(0x8229, 100);
|
||||
|
|
@ -126,7 +126,7 @@ public final class GlRenderBackendOpsTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, true);
|
||||
false, false, false, true, false);
|
||||
RenderBackendOps ops = RenderBackendOps.current();
|
||||
ops.trackBoundTboTexture(42);
|
||||
ops.markTboAttached(0x8229, 100);
|
||||
|
|
@ -145,7 +145,7 @@ public final class GlRenderBackendOpsTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, true);
|
||||
false, false, false, true, false);
|
||||
RenderBackendOps ops = RenderBackendOps.current();
|
||||
RenderBackendOps.Identifier id = new RenderBackendOps.Identifier("minecraft", "textures/entity/skin.png");
|
||||
assertNull(ops.renderTypeFor(id), "Fresh cache should miss");
|
||||
|
|
@ -161,7 +161,7 @@ public final class GlRenderBackendOpsTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
RenderBackendOps ops = RenderBackendOps.current();
|
||||
assertFalse(ops.renderTypesCacheSupported());
|
||||
RenderBackendOps.Identifier id = new RenderBackendOps.Identifier("minecraft", "test");
|
||||
|
|
@ -182,4 +182,32 @@ public final class GlRenderBackendOpsTest {
|
|||
assertFalse(a.equals(d));
|
||||
assertEquals("mc:path", a.toString());
|
||||
}
|
||||
|
||||
// --- FBO share (Stage 3 step 2) ---
|
||||
|
||||
@Test
|
||||
void fboShareDisabledWhenToggleOff() {
|
||||
OptimizeToggles.apply(
|
||||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false, false);
|
||||
RenderBackendOps ops = RenderBackendOps.current();
|
||||
assertFalse(ops.fboShareSupported());
|
||||
// Toggle off short-circuits before SharedDepthFbos — safe without GL.
|
||||
assertFalse(ops.clearDepthInPlace(1, 1.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fboShareSupportedWhenToggleOn() {
|
||||
OptimizeToggles.apply(
|
||||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false, true);
|
||||
RenderBackendOps ops = RenderBackendOps.current();
|
||||
assertTrue(ops.fboShareSupported());
|
||||
// No mapping for this depth id — clear() falls through to vanilla.
|
||||
assertFalse(ops.clearDepthInPlace(1, 1.0));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ public final class ParticleLodTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
Object p = new Object();
|
||||
assertTrue(ParticleLod.keep(p, 0));
|
||||
assertTrue(ParticleLod.keep(p, 100));
|
||||
|
|
@ -36,7 +36,7 @@ public final class ParticleLodTest {
|
|||
false, 0,
|
||||
true, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
Object p = new Object();
|
||||
// Strictly under 16 blocks: always keep (NEAR_SQ = 16² = 256).
|
||||
assertTrue(ParticleLod.keep(p, 0));
|
||||
|
|
@ -51,7 +51,7 @@ public final class ParticleLodTest {
|
|||
false, 0,
|
||||
true, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
// At exactly 16² (the boundary), the particle is no longer "near" — its
|
||||
// keep decision is delegated to the mid-range thinning. Over many
|
||||
// particles, about half should survive.
|
||||
|
|
@ -70,7 +70,7 @@ public final class ParticleLodTest {
|
|||
false, 0,
|
||||
true, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
double midSq = 24 * 24; // Between 16² and 32².
|
||||
Set<Boolean> seen = new HashSet<>();
|
||||
for (int i = 0; i < 256; i++) {
|
||||
|
|
@ -88,7 +88,7 @@ public final class ParticleLodTest {
|
|||
false, 0,
|
||||
true, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
double farSq = 48 * 48; // Beyond 32².
|
||||
int kept = 0;
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
|
|
@ -106,7 +106,7 @@ public final class ParticleLodTest {
|
|||
false, 0,
|
||||
true, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
Object p = new Object();
|
||||
double farSq = 48 * 48;
|
||||
boolean first = ParticleLod.keep(p, farSq);
|
||||
|
|
@ -122,7 +122,7 @@ public final class ParticleLodTest {
|
|||
false, 0,
|
||||
true, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
Object p = new Object();
|
||||
// particle at (50, 0, 0), camera at (0, 0, 0): distance² = 2500.
|
||||
boolean objectApi = ParticleLod.keep(p, 50 * 50);
|
||||
|
|
@ -137,7 +137,7 @@ public final class ParticleLodTest {
|
|||
false, 0,
|
||||
true, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
Object p = new Object();
|
||||
assertTrue(ParticleLod.keep(p, 15 * 15));
|
||||
assertTrue(ParticleLod.keep(p, 0));
|
||||
|
|
@ -149,7 +149,7 @@ public final class ParticleLodTest {
|
|||
false, 0,
|
||||
true, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
Object p = new Object();
|
||||
boolean first = ParticleLod.keep(p, 48 * 48);
|
||||
// Toggle off — every distance should now keep.
|
||||
|
|
@ -157,7 +157,7 @@ public final class ParticleLodTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
assertTrue(ParticleLod.keep(p, 48 * 48));
|
||||
assertTrue(ParticleLod.keep(p, 0));
|
||||
// first is just whatever it was — we only care that the new state is "keep everything".
|
||||
|
|
|
|||
|
|
@ -82,4 +82,37 @@ public final class WorkersTest {
|
|||
assertNull(err);
|
||||
assertEquals(blocks, counter.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void streamPublishesAndFinishes() {
|
||||
int blocks = 48;
|
||||
AtomicInteger counter = new AtomicInteger();
|
||||
Workers.Stream stream = Workers.stream(b -> counter.incrementAndGet());
|
||||
for (int i = 1; i <= blocks; i++) {
|
||||
stream.publish(i);
|
||||
}
|
||||
Throwable err = stream.finish(blocks);
|
||||
assertNull(err);
|
||||
assertEquals(blocks, counter.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void streamFinishesWithNoItems() {
|
||||
Workers.Stream stream = Workers.stream(b -> { });
|
||||
Throwable err = stream.finish(0);
|
||||
assertNull(err);
|
||||
}
|
||||
|
||||
@Test
|
||||
void streamReturnsFirstFailure() {
|
||||
int blocks = 12;
|
||||
Workers.Stream stream = Workers.stream(b -> {
|
||||
if (b == 2) {
|
||||
throw new IllegalStateException("stream item 2 failed");
|
||||
}
|
||||
});
|
||||
Throwable err = stream.finish(blocks);
|
||||
assertNotNull(err);
|
||||
assertTrue(err instanceof IllegalStateException);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ public final class StableTextTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
Recorder inner = new Recorder();
|
||||
FormattedCharSequence marked = StableText.mark(inner);
|
||||
assertTrue(marked instanceof StableText,
|
||||
|
|
@ -49,7 +49,7 @@ public final class StableTextTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
Recorder inner = new Recorder();
|
||||
FormattedCharSequence marked = StableText.mark(inner);
|
||||
assertTrue(StableText.isStable(marked));
|
||||
|
|
@ -61,7 +61,7 @@ public final class StableTextTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
Recorder plain = new Recorder();
|
||||
assertFalse(StableText.isStable(plain));
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ public final class StableTextTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, true);
|
||||
false, false, false, true, false);
|
||||
Recorder inner = new Recorder();
|
||||
FormattedCharSequence marked = StableText.mark(inner);
|
||||
StableText.clearTracked();
|
||||
|
|
@ -88,7 +88,7 @@ public final class StableTextTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
Recorder inner = new Recorder();
|
||||
FormattedCharSequence marked = StableText.mark(inner);
|
||||
assertTrue(StableText.isStable(marked));
|
||||
|
|
@ -103,7 +103,7 @@ public final class StableTextTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
Recorder inner = new Recorder();
|
||||
FormattedCharSequence marked = StableText.mark(inner);
|
||||
// Pass a sink that just returns true; the marked wrapper should delegate to inner.
|
||||
|
|
@ -117,7 +117,7 @@ public final class StableTextTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
assertEquals(0, StableText.trackedCount());
|
||||
StableText.mark(new Recorder());
|
||||
StableText.mark(new Recorder());
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public final class OutlineTrackerTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, false);
|
||||
false, 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.
|
||||
|
|
@ -39,7 +39,7 @@ public final class OutlineTrackerTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, true);
|
||||
false, false, false, true, false);
|
||||
OutlineTracker.beginFrame();
|
||||
assertTrue(OutlineTracker.shouldSkipOutlineRender(),
|
||||
"Without glowing entities, doEntityOutline should be skipped");
|
||||
|
|
@ -51,7 +51,7 @@ public final class OutlineTrackerTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, true);
|
||||
false, false, false, true, false);
|
||||
OutlineTracker.beginFrame();
|
||||
OutlineTracker.markGlowing();
|
||||
assertFalse(OutlineTracker.shouldSkipOutlineRender(),
|
||||
|
|
@ -65,7 +65,7 @@ public final class OutlineTrackerTest {
|
|||
false, 0,
|
||||
false, false, false,
|
||||
false, 0,
|
||||
false, false, false, true);
|
||||
false, false, false, true, false);
|
||||
OutlineTracker.markGlowing();
|
||||
assertTrue(OutlineTracker.glowingThisFrame());
|
||||
OutlineTracker.beginFrame();
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public final class OptimizeTogglesTest {
|
|||
assertFalse(OptimizeToggles.blockEntityCache());
|
||||
assertFalse(OptimizeToggles.preparedTextCache());
|
||||
assertFalse(OptimizeToggles.microOpts());
|
||||
assertFalse(OptimizeToggles.fboShare());
|
||||
assertEquals(0, OptimizeToggles.entityLodDistance());
|
||||
assertEquals(0, OptimizeToggles.spawnerDistance());
|
||||
}
|
||||
|
|
@ -36,7 +37,7 @@ public final class OptimizeTogglesTest {
|
|||
true,
|
||||
false,
|
||||
true,
|
||||
true);
|
||||
true, true);
|
||||
assertTrue(OptimizeToggles.entityLod());
|
||||
assertEquals(35, OptimizeToggles.entityLodDistance());
|
||||
assertFalse(OptimizeToggles.particleLod());
|
||||
|
|
@ -48,12 +49,14 @@ public final class OptimizeTogglesTest {
|
|||
assertFalse(OptimizeToggles.blockEntityCache());
|
||||
assertTrue(OptimizeToggles.preparedTextCache());
|
||||
assertTrue(OptimizeToggles.microOpts());
|
||||
assertTrue(OptimizeToggles.fboShare());
|
||||
|
||||
OptimizeToggles.reset();
|
||||
assertFalse(OptimizeToggles.entityLod());
|
||||
assertEquals(0, OptimizeToggles.entityLodDistance());
|
||||
assertFalse(OptimizeToggles.spawnerCull());
|
||||
assertFalse(OptimizeToggles.microOpts());
|
||||
assertFalse(OptimizeToggles.fboShare());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -65,7 +68,7 @@ public final class OptimizeTogglesTest {
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false);
|
||||
false, false);
|
||||
assertFalse(OptimizeToggles.entityLod());
|
||||
assertFalse(OptimizeToggles.particleLod());
|
||||
assertFalse(OptimizeToggles.spawnerCull());
|
||||
|
|
@ -73,6 +76,7 @@ public final class OptimizeTogglesTest {
|
|||
assertFalse(OptimizeToggles.blockEntityCache());
|
||||
assertFalse(OptimizeToggles.preparedTextCache());
|
||||
assertFalse(OptimizeToggles.microOpts());
|
||||
assertFalse(OptimizeToggles.fboShare());
|
||||
assertEquals(0, OptimizeToggles.entityLodDistance());
|
||||
assertEquals(0, OptimizeToggles.spawnerDistance());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.entity;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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 AnimFreeze} pure logic — the toggle snapshot and the
|
||||
* squared-distance gate. The capture/replay path needs a live render
|
||||
* pipeline (vertex buffers, Sodium or the raw-buffer mixin) and is
|
||||
* covered by an in-game check.
|
||||
*/
|
||||
public final class AnimFreezeTest {
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
AnimFreeze.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void offUntilConfigured() {
|
||||
assertFalse(AnimFreeze.enabled());
|
||||
AnimFreeze.configure(true, 24);
|
||||
assertTrue(AnimFreeze.enabled());
|
||||
assertEquals(24, AnimFreeze.distance());
|
||||
AnimFreeze.reset();
|
||||
assertFalse(AnimFreeze.enabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void freezesRejectsNonRenderState() {
|
||||
// Not an EntityRenderState (and even a far pose) — never freezes.
|
||||
AnimFreeze.configure(true, 16);
|
||||
assertFalse(AnimFreeze.freezes(new Object(), farPose()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void beyondDistanceIsSquared() {
|
||||
assertTrue(AnimFreeze.beyondDistance(17f, 0f, 0f, 16f), "just past the gate");
|
||||
assertFalse(AnimFreeze.beyondDistance(16f, 0f, 0f, 16f), "exactly at the gate");
|
||||
assertFalse(AnimFreeze.beyondDistance(3f, 4f, 0f, 16f), "5 blocks away, well inside");
|
||||
// diagonal: 12²+12² = 288 < 16² = 256 is false… 12²+12²=288 > 256 is true
|
||||
assertTrue(AnimFreeze.beyondDistance(12f, 12f, 0f, 16f), "diagonal past the gate");
|
||||
assertFalse(AnimFreeze.beyondDistance(0f, 0f, 0f, 0f), "at the camera");
|
||||
}
|
||||
|
||||
private static org.joml.Matrix4f farPose() {
|
||||
org.joml.Matrix4f pose = new org.joml.Matrix4f();
|
||||
pose.translate(100f, 64f, 100f);
|
||||
return pose;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.entity;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
import net.minecraft.client.resources.model.cuboid.ItemTransform;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector3fc;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Verifies {@link ItemBounds} caching — same extents supplier + same transform
|
||||
* hits the cache, a different transform or a reset recomputes.
|
||||
*/
|
||||
public final class ItemBoundsTest {
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
ItemBounds.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void configureAndReset() {
|
||||
assertFalse(ItemBounds.enabled());
|
||||
ItemBounds.configure(true);
|
||||
assertTrue(ItemBounds.enabled());
|
||||
ItemBounds.reset();
|
||||
assertFalse(ItemBounds.enabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cachesPerExtentsAndTransform() {
|
||||
ItemBounds.configure(true);
|
||||
AtomicInteger computes = new AtomicInteger();
|
||||
Supplier<Vector3fc[]> extents = () -> new Vector3fc[]{new Vector3f(), new Vector3f(1, 1, 1)};
|
||||
Supplier<AABB> compute = () -> {
|
||||
computes.incrementAndGet();
|
||||
return new AABB(0, 0, 0, 1, 1, 1);
|
||||
};
|
||||
|
||||
AABB first = ItemBounds.get(extents, ItemTransform.NO_TRANSFORM, compute);
|
||||
AABB second = ItemBounds.get(extents, ItemTransform.NO_TRANSFORM, compute);
|
||||
assertSame(first, second);
|
||||
assertEquals(1, computes.get(), "second lookup must not recompute");
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentTransformRecomputes() {
|
||||
ItemBounds.configure(true);
|
||||
AtomicInteger computes = new AtomicInteger();
|
||||
Supplier<Vector3fc[]> extents = () -> new Vector3fc[]{new Vector3f(), new Vector3f(1, 1, 1)};
|
||||
Supplier<AABB> compute = () -> {
|
||||
computes.incrementAndGet();
|
||||
return new AABB(0, 0, 0, 1, 1, 1);
|
||||
};
|
||||
|
||||
ItemBounds.get(extents, ItemTransform.NO_TRANSFORM, compute);
|
||||
ItemTransform other = new ItemTransform(new Vector3f(1, 0, 0), new Vector3f(), new Vector3f(1, 1, 1));
|
||||
ItemBounds.get(extents, other, compute);
|
||||
assertEquals(2, computes.get(), "a new transform must invalidate the entry");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetClearsCache() {
|
||||
ItemBounds.configure(true);
|
||||
AtomicInteger computes = new AtomicInteger();
|
||||
Supplier<Vector3fc[]> extents = () -> new Vector3fc[]{new Vector3f(), new Vector3f(1, 1, 1)};
|
||||
Supplier<AABB> compute = () -> {
|
||||
computes.incrementAndGet();
|
||||
return new AABB(0, 0, 0, 1, 1, 1);
|
||||
};
|
||||
|
||||
ItemBounds.get(extents, ItemTransform.NO_TRANSFORM, compute);
|
||||
ItemBounds.reset();
|
||||
ItemBounds.configure(true);
|
||||
ItemBounds.get(extents, ItemTransform.NO_TRANSFORM, compute);
|
||||
assertEquals(2, computes.get(), "reset must drop the cached entry");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.entity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.client.resources.model.geometry.BakedQuad;
|
||||
import net.minecraft.core.Direction;
|
||||
import org.joml.Vector3f;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Verifies {@link ItemLod} pure logic — the toggle snapshot and the flat-model
|
||||
* flattening rules — with hand-built quads, no render pipeline needed.
|
||||
*/
|
||||
public final class ItemLodTest {
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
ItemLod.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void configureAndReset() {
|
||||
assertFalse(ItemLod.enabled());
|
||||
ItemLod.configure(true, 8);
|
||||
assertTrue(ItemLod.enabled());
|
||||
assertEquals(64f, ItemLod.distanceSq(), 0.001f);
|
||||
ItemLod.reset();
|
||||
assertFalse(ItemLod.enabled());
|
||||
assertEquals(0f, ItemLod.distanceSq(), 0.001f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void flatKeepsOnlyZFacesOfFlatModel() {
|
||||
List<BakedQuad> flatModel = new ArrayList<>();
|
||||
flatModel.add(quad(Direction.NORTH, 0f, 0f)); // big front face
|
||||
flatModel.add(quad(Direction.SOUTH, 1f, 1f)); // big back face
|
||||
for (int i = 0; i < 8; i++) {
|
||||
flatModel.add(quad(Direction.UP, 0.94f, 1f)); // thin edge strips
|
||||
}
|
||||
|
||||
List<BakedQuad> result = ItemLod.flat(flatModel);
|
||||
assertEquals(2, result.size(), "only the two Z faces survive");
|
||||
assertSame(flatModel.get(0), result.get(0));
|
||||
assertSame(flatModel.get(1), result.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void flatLeavesThickModelUnchanged() {
|
||||
List<BakedQuad> model = new ArrayList<>();
|
||||
model.add(quad(Direction.NORTH, 0f, 0f));
|
||||
model.add(quad(Direction.SOUTH, 1f, 1f));
|
||||
for (int i = 0; i < 6; i++) {
|
||||
model.add(quad(Direction.UP, 0.94f, 1f));
|
||||
}
|
||||
model.add(quad(Direction.UP, 0f, 1f)); // thick non-Z quad — full cube side
|
||||
|
||||
assertSame(model, ItemLod.flat(model), "thick model is returned as-is");
|
||||
}
|
||||
|
||||
@Test
|
||||
void flatLeavesSmallModelsUnchanged() {
|
||||
// A plain baked box is six quads — below the flattening threshold.
|
||||
List<BakedQuad> box = new ArrayList<>();
|
||||
box.add(quad(Direction.NORTH, 0f, 0f));
|
||||
box.add(quad(Direction.SOUTH, 1f, 1f));
|
||||
box.add(quad(Direction.UP, 0f, 1f));
|
||||
box.add(quad(Direction.DOWN, 0f, 1f));
|
||||
box.add(quad(Direction.EAST, 0f, 1f));
|
||||
box.add(quad(Direction.WEST, 0f, 1f));
|
||||
|
||||
assertSame(box, ItemLod.flat(box));
|
||||
}
|
||||
|
||||
@Test
|
||||
void flatCachesPerFirstQuad() {
|
||||
List<BakedQuad> model = new ArrayList<>();
|
||||
model.add(quad(Direction.NORTH, 0f, 0f));
|
||||
model.add(quad(Direction.SOUTH, 1f, 1f));
|
||||
for (int i = 0; i < 8; i++) {
|
||||
model.add(quad(Direction.UP, 0.94f, 1f));
|
||||
}
|
||||
|
||||
List<BakedQuad> first = ItemLod.flat(model);
|
||||
List<BakedQuad> second = ItemLod.flat(model);
|
||||
assertSame(first, second, "same list instance comes from the cache");
|
||||
}
|
||||
|
||||
private static BakedQuad quad(Direction dir, float z0, float z1) {
|
||||
return new BakedQuad(
|
||||
new Vector3f(0f, 0f, z0), new Vector3f(1f, 0f, z0),
|
||||
new Vector3f(1f, 1f, z1), new Vector3f(0f, 1f, z1),
|
||||
0L, 0L, 0L, 0L, dir, null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.gl;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
/**
|
||||
* Verifies {@link SharedDepthFbos} bookkeeping — the headless-safe paths
|
||||
* only. The DSA clear path needs a live GL context and is covered by an
|
||||
* in-game check (owner runs {@code runClient}); without a cached FBO or a
|
||||
* known depth id, {@code clear} returns {@code false} before touching GL.
|
||||
*/
|
||||
public final class SharedDepthFbosTest {
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
SharedDepthFbos.resetForTesting();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rememberTracksDepthToFboMapping() {
|
||||
SharedDepthFbos.remember(10, 100);
|
||||
SharedDepthFbos.remember(11, 101);
|
||||
assertEquals(2, SharedDepthFbos.sizeForTesting());
|
||||
assertEquals(100, SharedDepthFbos.fboForTesting(10));
|
||||
assertEquals(101, SharedDepthFbos.fboForTesting(11));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rememberSameDepthTwiceKeepsLastFbo() {
|
||||
SharedDepthFbos.remember(10, 100);
|
||||
SharedDepthFbos.remember(10, 102);
|
||||
assertEquals(1, SharedDepthFbos.sizeForTesting());
|
||||
assertEquals(102, SharedDepthFbos.fboForTesting(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forgetRemovesOnlyMappingsPointingAtThatFbo() {
|
||||
SharedDepthFbos.remember(10, 100);
|
||||
SharedDepthFbos.remember(11, 100);
|
||||
SharedDepthFbos.remember(12, 101);
|
||||
SharedDepthFbos.forget(100);
|
||||
assertEquals(1, SharedDepthFbos.sizeForTesting());
|
||||
assertEquals(0, SharedDepthFbos.fboForTesting(10));
|
||||
assertEquals(0, SharedDepthFbos.fboForTesting(11));
|
||||
assertEquals(101, SharedDepthFbos.fboForTesting(12));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forgetUnknownFboIsNoop() {
|
||||
SharedDepthFbos.remember(10, 100);
|
||||
SharedDepthFbos.forget(999);
|
||||
assertEquals(1, SharedDepthFbos.sizeForTesting());
|
||||
assertEquals(100, SharedDepthFbos.fboForTesting(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearWithoutMappingFallsThroughToVanilla() {
|
||||
SharedDepthFbos.remember(10, 100);
|
||||
// Unknown depth id: no GL touched, caller keeps the vanilla path.
|
||||
assertFalse(SharedDepthFbos.clear(42, 1.0));
|
||||
// Empty map after forget: same.
|
||||
SharedDepthFbos.forget(100);
|
||||
assertFalse(SharedDepthFbos.clear(10, 1.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetDropsEverything() {
|
||||
SharedDepthFbos.remember(10, 100);
|
||||
SharedDepthFbos.resetForTesting();
|
||||
assertEquals(0, SharedDepthFbos.sizeForTesting());
|
||||
assertFalse(SharedDepthFbos.clear(10, 1.0));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.hud;
|
||||
|
||||
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 HudWorth} hysteresis — off below 1.5 frames per refresh,
|
||||
* on above 2.0, stable inside the band.
|
||||
*/
|
||||
public final class HudWorthTest {
|
||||
|
||||
private static final long REFRESH_60HZ = 1_000_000_000L / 60; // ~16.67 ms
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
HudWorth.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void startsWorth() {
|
||||
assertTrue(HudWorth.worth());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fastFramesTurnItOff() {
|
||||
// ~1 ms frames at 60 Hz → framesPerRefresh ≈ 16.7, far above the band: stays on
|
||||
long t = 0;
|
||||
for (int i = 0; i < 100; i++) {
|
||||
t += 1_000_000L;
|
||||
assertTrue(HudWorth.update(t, REFRESH_60HZ));
|
||||
}
|
||||
|
||||
// now ~16 ms frames → framesPerRefresh ≈ 1.04, below the off threshold
|
||||
for (int i = 0; i < 200; i++) {
|
||||
t += 16_000_000L;
|
||||
HudWorth.update(t, REFRESH_60HZ);
|
||||
}
|
||||
assertFalse(HudWorth.worth(), "near one frame per refresh the cache must turn off");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hysteresisBandKeepsState() {
|
||||
// drive it off with ~16 ms frames
|
||||
long t = 0;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
t += 16_000_000L;
|
||||
HudWorth.update(t, REFRESH_60HZ);
|
||||
}
|
||||
assertFalse(HudWorth.worth());
|
||||
|
||||
// ~10 ms frames → framesPerRefresh ≈ 1.67, inside the band: stays off
|
||||
for (int i = 0; i < 200; i++) {
|
||||
t += 10_000_000L;
|
||||
HudWorth.update(t, REFRESH_60HZ);
|
||||
}
|
||||
assertFalse(HudWorth.worth(), "inside the hysteresis band the state must not flip");
|
||||
|
||||
// ~5 ms frames → framesPerRefresh ≈ 3.3, above the on threshold: back on
|
||||
for (int i = 0; i < 200; i++) {
|
||||
t += 5_000_000L;
|
||||
HudWorth.update(t, REFRESH_60HZ);
|
||||
}
|
||||
assertTrue(HudWorth.worth(), "well above the band the cache must turn back on");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetRestoresWorth() {
|
||||
long t = 0;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
t += 16_000_000L;
|
||||
HudWorth.update(t, REFRESH_60HZ);
|
||||
}
|
||||
assertFalse(HudWorth.worth());
|
||||
HudWorth.reset();
|
||||
assertTrue(HudWorth.worth());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.hud;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Verifies {@link MonitorRate} pure logic — the refresh-rate clamp, the
|
||||
* once-per-second probe caching and the reset — through the test probe hook;
|
||||
* the GLFW path needs a live window.
|
||||
*/
|
||||
public final class MonitorRateTest {
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
MonitorRate.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void clampsLowRefreshRatesToThirty() {
|
||||
MonitorRate.setProbeForTesting(() -> 10);
|
||||
assertEquals(1_000_000_000L / 30, MonitorRate.intervalNs(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesProbedRate() {
|
||||
MonitorRate.setProbeForTesting(() -> 60);
|
||||
assertEquals(1_000_000_000L / 60, MonitorRate.intervalNs(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void probesAtMostOncePerSecond() {
|
||||
AtomicInteger probes = new AtomicInteger();
|
||||
MonitorRate.setProbeForTesting(() -> {
|
||||
probes.incrementAndGet();
|
||||
return 60;
|
||||
});
|
||||
|
||||
MonitorRate.intervalNs(0L);
|
||||
MonitorRate.intervalNs(500_000_000L); // still inside the 1 s window
|
||||
assertEquals(1, probes.get(), "second call inside the window must not re-probe");
|
||||
|
||||
MonitorRate.intervalNs(1_000_000_000L); // window elapsed
|
||||
assertEquals(2, probes.get(), "a new second must re-probe");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetRestoresDefaultInterval() {
|
||||
MonitorRate.setProbeForTesting(() -> 30);
|
||||
MonitorRate.intervalNs(0L);
|
||||
MonitorRate.reset();
|
||||
assertTrue(MonitorRate.intervalNs(0L) > 0);
|
||||
|
||||
// after reset the production probe is back — in headless tests it
|
||||
// falls back to the 144 Hz default instead of touching GLFW
|
||||
assertEquals(1_000_000_000L / 144, MonitorRate.intervalNs(0L));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.parallel;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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 ParallelModels} gating — the toggle snapshot, the cores
|
||||
* floor and the AnimFreeze interlock. The build itself needs a live render
|
||||
* pipeline (posed models, raw vertex consumers) and is covered by an
|
||||
* in-game check.
|
||||
*/
|
||||
public final class ParallelModelsTest {
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
ParallelModels.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void offUntilConfigured() {
|
||||
assertFalse(ParallelModels.enabled());
|
||||
ParallelModels.configure(true);
|
||||
assertTrue(ParallelModels.enabled(), "4+ cores, no Iris, no anim freeze");
|
||||
ParallelModels.reset();
|
||||
assertFalse(ParallelModels.enabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void animFreezeDisablesParallelBuild() {
|
||||
ParallelModels.configure(true);
|
||||
dev.loki.lovisual.render.engine.optimize.entity.AnimFreeze.configure(true, 16);
|
||||
assertFalse(ParallelModels.enabled(), "freeze replays captures, parallel builds fresh — exclusive");
|
||||
dev.loki.lovisual.render.engine.optimize.entity.AnimFreeze.reset();
|
||||
assertTrue(ParallelModels.enabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void minSubmitsMatchesReferenceThreshold() {
|
||||
assertEquals(24, ParallelModels.MIN_SUBMITS);
|
||||
assertTrue(ParallelModels.FORMAT.getVertexSize() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetFreesWithoutErrorWhenUnused() {
|
||||
ParallelModels.configure(true);
|
||||
// begin/add/end need real models; reset alone must stay safe on any machine
|
||||
ParallelModels.reset();
|
||||
assertFalse(ParallelModels.enabled());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
package dev.loki.lovisual.render.engine.optimize.profile;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Verifies {@link DrawStats} — the F3 draw/pipeline/framebuffer counters.
|
||||
*
|
||||
* <p>Stage 3 step 1 of {@code mod/docs/optimize-layer-design.md}. Everything
|
||||
* runs on the JUnit thread, which stands in for the render thread; time is
|
||||
* passed into {@link DrawStats#endFrame(boolean, long)} so the one-second
|
||||
* window can be driven without sleeping.
|
||||
*/
|
||||
public final class DrawStatsTest {
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
DrawStats.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nothingCountsBeforeTheFirstShownFrame() {
|
||||
DrawStats.draw();
|
||||
DrawStats.pipeline("pipeline/a");
|
||||
DrawStats.bind();
|
||||
|
||||
DrawStats.Snapshot s = DrawStats.snapshot();
|
||||
assertEquals(0, s.frames());
|
||||
assertEquals(0, s.draws());
|
||||
assertTrue(s.top().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nothingIsPublishedInsideTheWindow() {
|
||||
DrawStats.endFrame(true, 0L);
|
||||
DrawStats.draw();
|
||||
|
||||
DrawStats.endFrame(true, DrawStats.WINDOW_NS - 1);
|
||||
|
||||
assertEquals(0, DrawStats.snapshot().frames());
|
||||
assertEquals(0, DrawStats.snapshot().draws());
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishesOnceTheWindowElapses() {
|
||||
DrawStats.endFrame(true, 0L);
|
||||
DrawStats.draw();
|
||||
DrawStats.draw();
|
||||
DrawStats.pipeline("pipeline/a");
|
||||
DrawStats.bind();
|
||||
|
||||
DrawStats.endFrame(true, DrawStats.WINDOW_NS);
|
||||
|
||||
DrawStats.Snapshot s = DrawStats.snapshot();
|
||||
assertEquals(1, s.frames());
|
||||
assertEquals(2, s.draws());
|
||||
assertEquals(1, s.pipelines());
|
||||
assertEquals(1, s.binds());
|
||||
assertEquals(List.of("pipeline/a 1"), s.top());
|
||||
}
|
||||
|
||||
@Test
|
||||
void closingF3StopsCountingAndClearsTheSnapshot() {
|
||||
DrawStats.endFrame(true, 0L);
|
||||
DrawStats.draw();
|
||||
DrawStats.endFrame(true, DrawStats.WINDOW_NS);
|
||||
assertEquals(1, DrawStats.snapshot().draws());
|
||||
|
||||
DrawStats.endFrame(false, DrawStats.WINDOW_NS + 1);
|
||||
assertEquals(0, DrawStats.snapshot().frames());
|
||||
assertTrue(DrawStats.snapshot().top().isEmpty());
|
||||
|
||||
// a frame drawn while F3 is closed must not land anywhere
|
||||
DrawStats.draw();
|
||||
DrawStats.bind();
|
||||
assertEquals(0, DrawStats.snapshot().draws());
|
||||
assertEquals(0, DrawStats.snapshot().binds());
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishAveragesOverFramesAndSortsTopBusiestFirst() {
|
||||
Map<String, Integer> byPipeline = new LinkedHashMap<>();
|
||||
byPipeline.put("pipeline/rare", 2);
|
||||
byPipeline.put("pipeline/busy", 40);
|
||||
byPipeline.put("pipeline/middle", 10);
|
||||
|
||||
DrawStats.Snapshot s = DrawStats.publish(100, 50, 10, 4, byPipeline, 2);
|
||||
|
||||
assertEquals(25, s.draws()); // 100 / 4
|
||||
assertEquals(13, s.pipelines()); // round(50 / 4)
|
||||
assertEquals(3, s.binds()); // round(10 / 4)
|
||||
assertEquals(4, s.frames());
|
||||
assertEquals(List.of("pipeline/busy 10", "pipeline/middle 3"), s.top());
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishWithNoFramesDoesNotDivideByZero() {
|
||||
DrawStats.Snapshot s = DrawStats.publish(7, 3, 1, 0, Map.of(), DrawStats.TOP);
|
||||
|
||||
assertEquals(0, s.frames());
|
||||
assertEquals(7, s.draws());
|
||||
assertEquals(3, s.pipelines());
|
||||
assertEquals(1, s.binds());
|
||||
assertTrue(s.top().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void topIsLimitedToTheConfiguredCount() {
|
||||
Map<String, Integer> byPipeline = new LinkedHashMap<>();
|
||||
for (int i = 0; i < 20; i++) {
|
||||
byPipeline.put("pipeline/" + i, i + 1);
|
||||
}
|
||||
|
||||
DrawStats.Snapshot s = DrawStats.publish(210, 210, 0, 1, byPipeline, DrawStats.TOP);
|
||||
|
||||
assertEquals(DrawStats.TOP, s.top().size());
|
||||
assertEquals("pipeline/19 20", s.top().get(0));
|
||||
assertEquals("pipeline/15 16", s.top().get(DrawStats.TOP - 1));
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ public final class OptimizePropertyBridgeTest {
|
|||
"lovisual.opt.entity_lod", "lovisual.opt.particle_lod",
|
||||
"lovisual.opt.spawner_cull", "lovisual.opt.spawner_tick_cache",
|
||||
"lovisual.opt.block_entity_cache", "lovisual.opt.prepared_text_cache",
|
||||
"lovisual.opt.micro_opts",
|
||||
"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"
|
||||
}) {
|
||||
|
|
@ -68,6 +68,19 @@ public final class OptimizePropertyBridgeTest {
|
|||
assertFalse(s.overrides(s.entityLod, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fboShareBooleanOverride() {
|
||||
System.setProperty("lovisual.opt.fbo_share", "false");
|
||||
OptimizePropertyBridge.Snapshot s = OptimizePropertyBridge.resolve();
|
||||
assertEquals(Boolean.FALSE, s.fboShare);
|
||||
assertFalse(s.overrides(s.fboShare, true));
|
||||
|
||||
System.setProperty("lovisual.opt.fbo_share", "true");
|
||||
s = OptimizePropertyBridge.resolve();
|
||||
assertEquals(Boolean.TRUE, s.fboShare);
|
||||
assertTrue(s.overrides(s.fboShare, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void distanceOverride() {
|
||||
System.setProperty("lovisual.opt.entity_lod_distance", "100");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue