chore(history): squash 59 commit(s) from 2026-09-23

- refactor(hud/weather/render): Scoreboard 931→240 + Rain 903→159 + UiMeshGeometry 956→99 (8.5.2)
- refactor(chams): Chams 985 → 129 + 4×≤195 (8.5.2)
- refactor(module): Module 1031 → 139 + 4×≤134 (8.5.2)
- refactor(tooltips): BetterTooltips 960 → 181 + 4×≤199 (8.5.2)
- refactor(clickgui): ClickGuiPickerState 970→166 + ModulesMenuScreen 966→145 (8.5.2)
- refactor(clickgui): CombatProtocolHeuristicsEditorState 932→79 + 4×≤192 (8.5.2)
- refactor(hud): CompactHudStatModel 894 → 125 + 4×≤175 (8.5.2)
- refactor(iris): ShaderPatchCompiler 843→66 + 4×≤174 (8.5.2)
- refactor(tab): CustomTabList 834→136 + 4×≤166 (8.5.2)
- refactor(relations): OnlineRelationPlayerPickerComponent 837→74 + 4×≤192 (8.5.2)
- refactor(hits): HitEffect 770→120 + 4×≤176 (8.5.2)
- refactor(predict): Predictions 815→150 + 4×≤195 (8.5.2)
- refactor(config): ConfigProfilesComponent 799→125 + 4×≤195 (8.5.2)
- refactor(sim): PlayerMovementSimulation 757→157 + 4×≤194 (8.5.2)
- refactor(predict): ProjectilePuncher 747→104 + 3×≤134 (8.5.2)
- refactor(crosshair): Crosshair 743→143 + 4×≤167 (8.5.2)
- refactor(mediaplayer): MediaPlayer 743→174 + 4×≤196 (8.5.2)
- refactor(hud): Itemizer 699→193 + Cooldowns 707→151 (8.5.2)
- refactor(hud): Potions 617→176 + 4×≤166 (8.5.2)
- refactor(hud): Admins 702→118 + 4×≤138 (8.5.2)
- refactor(hud): Keybinds 611→157 + 4×≤160 (8.5.2)
- refactor(hud): ModuleList 499→181 + 3×≤150 (8.5.2)
- refactor(hud): Radar 392→161 + 2×≤124 (8.5.2)
- refactor(hud): Fps 309→168 + 3×≤107 (8.5.2)
- refactor(hud): Memory 304→151 + 3×≤144 (8.5.2)
- refactor(hud): Tps 339→154 + 3×≤143 (8.5.2)
- refactor(hud): Ping 309→145 + 3×≤125 (8.5.2)
- refactor(hud): SpeedBps 334→156 + 3×≤142 (8.5.2)
- refactor(hud): GameTime 314→150 + 3×≤127 (8.5.2)
- refactor(hud): SystemTime 300→136 + 3×≤130 (8.5.2)
- refactor(hud): Coordinates 337→174 + 2×≤172 (8.5.2)
- refactor(hud): Armor 324→162 + 2×≤148 (8.5.2)
- refactor(hud): Inventory 476→153 + 3×≤181 (8.5.2)
- docs(todo): обновлён 8.5.2 — отмечены готовые гиганты (BetterButtons/InventorySwap/Svg/Темы/WorldParticles/Module/Chams и хвост)
- docs(api): папка combatant-client-26.2/docs + addons.md (API можно ломать при рефакторе)
- refactor(visuals): BlockHighlight 692→271 + 4×≤200 (8.5.2)
- refactor(mixins): GameRendererMixin 830→344 (хуки) + 4 хелпера (8.5.2)
- refactor(visuals): WorldParticles 821→260 + 6×≤200, режимы добиты (8.5.2)
- docs(todo): 8.5.2 — WorldParticles/GameRendererMixin/BlockHighlight отмечены ГОТОВО
- refactor(render): MeshBuilder 738→447 (фасад write-API) + 3 хелпера, FrameStats снесён (8.5.2)
- docs(todo): Фаза 10 — дизайн платформы (аккаунты/конфиги/аватарки), backlog RPC-чата
- docs(todo): Подсистема 3 — Аддоны 2.0 (скрипты/версии) + витрина аддонов + админка
- docs(todo): Подсистема 2 — RPC-чат/друзья/виджеты-телеметрия + рейт-лимиты, доп. правки Подсистемы 3
- chore: переименование combatant-client-26.2 -> mod, добавлены backend/ и frontend/
- docs(backend): implementation plan for accounts-service (Подсистема 1, часть 1)
- chore: пересоздать backend/frontend через cargo new и bun create vite + tailwind; убрать таблицу компонентов из TODO.md
- docs(backend): edition 2024 в плане вместо 2021
- chore: обновить версии до реально актуальных (проверено компиляцией)
- docs: правила платформы (лимит 250 строк, антипаттерны backend/frontend по итогам ресёрча) + frontend/ARCHITECTURE.md
- docs: добавить правило ≤4 файла на папку в правила платформы (backend/frontend)
- docs: commit messages in English from now on
- chore: gitignore .superpowers/ scratch directory
- docs: commit messages in English from now on (mod)
- feat(backend): scaffold accounts-service with health check
- chore(backend): remove target/ build artifacts from git, add gitignore
- chore: track docs/ in git (was accidentally gitignored, never committed)
- chore(backend): convert to Cargo workspace, plan full microservice layout
- feat(backend): accounts/avatars/device_links schema + migration
- feat(backend): argon2id password hashing
This commit is contained in:
loki5512344 2026-09-23 22:38:08 +02:00
parent ec10395511
commit 9d08fa910a
2239 changed files with 35265 additions and 28110 deletions

View file

@ -0,0 +1,96 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render;
import com.mojang.blaze3d.PrimitiveTopology;
import com.mojang.blaze3d.pipeline.BlendFunction;
import com.mojang.blaze3d.pipeline.ColorTargetState;
import com.mojang.blaze3d.pipeline.DepthStencilState;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.platform.CompareOp;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import net.minecraft.client.renderer.BindGroupLayouts;
import net.minecraft.client.renderer.rendertype.RenderSetup;
import net.minecraft.client.renderer.rendertype.RenderType;
import net.minecraft.resources.Identifier;
import dev.loki.lovisual.mixins.accessors.renderer.RenderLayerInvokerAccessor;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public enum LoVisualEntityRenderTypes {
;
private static final RenderPipeline ENTITY_TRANSLUCENT_NO_DEPTH_WRITE = RenderPipeline.builder()
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/entity_translucent_no_depth_write"))
.withBindGroupLayout(BindGroupLayouts.GLOBALS)
.withBindGroupLayout(BindGroupLayouts.MATRICES_PROJECTION)
.withBindGroupLayout(BindGroupLayouts.FOG)
.withBindGroupLayout(BindGroupLayouts.LIGHTING)
.withBindGroupLayout(BindGroupLayouts.SAMPLER0_SAMPLER1_SAMPLER2)
.withVertexShader("core/entity")
.withFragmentShader("core/entity")
.withVertexBinding(0, DefaultVertexFormat.ENTITY)
.withPrimitiveTopology(PrimitiveTopology.QUADS)
.withShaderDefine("ALPHA_CUTOUT", 0.1f)
.withShaderDefine("PER_FACE_LIGHTING")
.withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT))
.withDepthStencilState(new DepthStencilState(CompareOp.GREATER_THAN_OR_EQUAL, false))
.build();
private static final RenderPipeline ENTITY_TRANSLUCENT_NO_DEPTH_WRITE_NO_CULL = RenderPipeline.builder()
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/entity_translucent_no_depth_write_no_cull"))
.withBindGroupLayout(BindGroupLayouts.GLOBALS)
.withBindGroupLayout(BindGroupLayouts.MATRICES_PROJECTION)
.withBindGroupLayout(BindGroupLayouts.FOG)
.withBindGroupLayout(BindGroupLayouts.LIGHTING)
.withBindGroupLayout(BindGroupLayouts.SAMPLER0_SAMPLER1_SAMPLER2)
.withVertexShader("core/entity")
.withFragmentShader("core/entity")
.withVertexBinding(0, DefaultVertexFormat.ENTITY)
.withPrimitiveTopology(PrimitiveTopology.QUADS)
.withShaderDefine("ALPHA_CUTOUT", 0.1f)
.withShaderDefine("PER_FACE_LIGHTING")
.withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT))
.withDepthStencilState(new DepthStencilState(CompareOp.GREATER_THAN_OR_EQUAL, false))
.withCull(false)
.build();
private static final Map<Key, RenderType> ENTITY_TRANSLUCENT_CACHE = new ConcurrentHashMap<>();
public static RenderType entityTranslucentNoDepthWrite(Identifier texture, boolean cull) {
return ENTITY_TRANSLUCENT_CACHE.computeIfAbsent(new Key(texture, cull), LoVisualEntityRenderTypes::createEntityTranslucent);
}
public static List<RenderPipeline> irisMappedPipelines() {
return List.of(ENTITY_TRANSLUCENT_NO_DEPTH_WRITE, ENTITY_TRANSLUCENT_NO_DEPTH_WRITE_NO_CULL);
}
private static RenderType createEntityTranslucent(Key key) {
RenderPipeline pipeline = key.cull
? ENTITY_TRANSLUCENT_NO_DEPTH_WRITE
: ENTITY_TRANSLUCENT_NO_DEPTH_WRITE_NO_CULL;
RenderSetup setup = RenderSetup.builder(pipeline)
.withTexture("Sampler0", key.texture)
.useLightmap()
.useOverlay()
.affectsCrumbling()
.sortOnUpload()
.setOutline(RenderSetup.OutlineProperty.AFFECTS_OUTLINE)
.createRenderSetup();
return RenderLayerInvokerAccessor.callOf(
key.cull ? "lovisual_entity_translucent_no_depth_write" : "lovisual_entity_translucent_no_depth_write_no_cull",
setup
);
}
private record Key(Identifier texture, boolean cull) {
}
}

View file

@ -0,0 +1,44 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render;
public enum ShaderEspRenderContext {
;
private static final ThreadLocal<Integer> DEPTH = ThreadLocal.withInitial(() -> 0);
public static Scope enter() {
DEPTH.set(DEPTH.get() + 1);
return new Scope();
}
public static boolean isActive() {
return DEPTH.get() > 0;
}
public static final class Scope implements AutoCloseable {
private boolean closed;
private Scope() {
}
@Override
public void close() {
if (closed) {
return;
}
closed = true;
int next = Math.max(0, DEPTH.get() - 1);
if (next == 0) {
DEPTH.remove();
} else {
DEPTH.set(next);
}
}
}
}

View file

@ -0,0 +1,31 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.compat;
import dev.tr7zw.entityculling.versionless.access.Cullable;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.world.entity.Entity;
public enum EntityCullingCompat {
;
private static final String MOD_ID = "entityculling";
public static void forceVisibleForShaderEsp(Entity entity) {
if (entity == null || !FabricLoader.getInstance().isModLoaded(MOD_ID)) {
return;
}
if (!(entity instanceof Cullable cullable)) {
return;
}
cullable.setCulled(false);
cullable.setOutOfCamera(false);
cullable.setTimeout();
}
}

View file

@ -0,0 +1,110 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.effects;
import com.mojang.blaze3d.textures.GpuTextureView;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.PortalProcessor;
import net.minecraft.world.level.block.Blocks;
import dev.loki.lovisual.mixins.accessors.entity.EntityAccessor;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass;
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
import dev.loki.lovisual.render.engine.uniform.impl.world.HeatUniforms;
public final class NetherPortalRiftPass implements PostProcessPass {
private static final float FADE_IN_SECONDS = 0.25f;
private static final float FADE_OUT_SECONDS = 0.35f;
private float strength;
private long lastUpdateMs;
@Override
public boolean isActive() {
Minecraft mc = Minecraft.getInstance();
return mc != null && mc.player != null && mc.level != null;
}
@Override
public int getPriority() {
return 11;
}
@Override
public Phase getPhase() {
return Phase.POST_HAND;
}
@Override
public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.player == null || mc.level == null) return false;
if (src == null || dst == null) return false;
PortalProcessor portalManager = null;
if (mc.player instanceof EntityAccessor accessor) {
portalManager = accessor.lovisual$getPortalManager();
}
boolean active = portalManager != null && portalManager.isInsidePortalThisTick() && isInNetherPortalBlock();
float t = smoothTowards(active ? 1.0f : 0.0f);
if (t <= 0.001f) return false;
float time = (System.currentTimeMillis() % 100000L) / 1000.0f;
float intensity = 0.75f * t;
float distortion = 0.65f * t;
float scale = 1.25f;
float speed = 1.65f;
float vignetteStrength = 0.85f * t;
float vignetteRadius = 0.38f;
float vignetteSoftness = 0.28f;
HeatUniforms.update(
intensity,
distortion,
scale,
speed,
vignetteStrength,
vignetteRadius,
vignetteSoftness,
time
);
FullScreenRenderer.ensureInit();
FullScreenRenderer.begin("LoVisual Fullscreen Pass")
.attachment(dst)
.pipeline(LoVisualRenderPipelines.PORTAL_RIFT)
.uniform("Heat", HeatUniforms.get())
.sampler("u_Texture", src, PostProcessManager.getSampler())
.end();
return true;
}
private float smoothTowards(float target) {
long now = System.currentTimeMillis();
float dt = (lastUpdateMs == 0L) ? 0.016f : (now - lastUpdateMs) / 1000.0f;
lastUpdateMs = now;
float rate = target >= strength ? (1.0f / FADE_IN_SECONDS) : (1.0f / FADE_OUT_SECONDS);
float step = Mth.clamp(rate * dt, 0.0f, 1.0f);
strength += (target - strength) * step;
strength = Mth.clamp(strength, 0.0f, 1.0f);
return strength;
}
private boolean isInNetherPortalBlock() {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.level == null || mc.player == null) return false;
var pos = mc.player.blockPosition();
if (mc.level.getBlockState(pos).is(Blocks.NETHER_PORTAL)) return true;
return mc.level.getBlockState(pos.above()).is(Blocks.NETHER_PORTAL);
}
}

View file

@ -0,0 +1,80 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.effects;
import com.mojang.blaze3d.textures.GpuTextureView;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Mth;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass;
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
import dev.loki.lovisual.render.engine.uniform.impl.post.PostProcessUniforms;
public final class SleepOverlayPass implements PostProcessPass {
private static final float FADE_IN_SECONDS = 0.6f;
private static final float FADE_OUT_SECONDS = 0.6f;
private float strength;
private long lastUpdateMs;
@Override
public boolean isActive() {
Minecraft mc = Minecraft.getInstance();
return mc != null && mc.player != null && mc.level != null;
}
@Override
public int getPriority() {
return 12;
}
@Override
public Phase getPhase() {
return Phase.POST_HAND;
}
@Override
public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.player == null || mc.level == null) return false;
if (src == null || dst == null) return false;
boolean sleeping = mc.player.isSleeping();
float t = smoothTowards(sleeping ? 1.0f : 0.0f);
if (t <= 0.001f) return false;
float vignette = 1.35f * t;
float desat = 0.2f * t;
float contrast = 0.0f;
PostProcessUniforms.update(vignette, desat, contrast, 0.0f);
FullScreenRenderer.ensureInit();
FullScreenRenderer.begin("LoVisual Fullscreen Pass")
.attachment(dst)
.pipeline(LoVisualRenderPipelines.SLEEP_OVERLAY)
.uniform("PostProcess", PostProcessUniforms.get())
.sampler("u_Texture", src, PostProcessManager.getSampler())
.end();
return true;
}
private float smoothTowards(float target) {
long now = System.currentTimeMillis();
float dt = (lastUpdateMs == 0L) ? 0.016f : (now - lastUpdateMs) / 1000.0f;
lastUpdateMs = now;
float rate = target >= strength ? (1.0f / FADE_IN_SECONDS) : (1.0f / FADE_OUT_SECONDS);
float step = Mth.clamp(rate * dt, 0.0f, 1.0f);
strength += (target - strength) * step;
strength = Mth.clamp(strength, 0.0f, 1.0f);
return strength;
}
}

View file

@ -0,0 +1,29 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine;
import dev.loki.lovisual.render.engine.guard.boundary.RenderBoundaryAudit;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.svg.parser.SvgRegistry;
import dev.loki.lovisual.render.engine.visuals.LoVisualVisuals;
import dev.loki.lovisual.render.iris.runtime.IrisRuntime;
public enum LoVisualRenderEngineBootstrap {
;
public static void init() {
RenderBoundaryAudit.runOnce();
// RHI backend selection must be lazy: onInitializeClient can run before RenderSystem has
// created the final GpuDevice, which would permanently select the wrong backend on Vulkan.
Renderer2D.init();
IrisRuntime.registerLoVisualPipelines();
SvgRegistry.init();
LoVisualVisuals.init();
}
}

View file

@ -0,0 +1,63 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import dev.loki.lovisual.render.engine.core.context.ViewportContext;
/**
* Deprecated compatibility facade. Use ViewportContext directly.
*/
@Deprecated(forRemoval = false)
public enum Projection2D {
;
public static void beginUnscaled(GuiGraphicsExtractor ctx) {
ViewportContext.beginUnscaled(ctx);
}
public static void beginUnscaledLogical(GuiGraphicsExtractor ctx) {
ViewportContext.beginUnscaledLogical(ctx);
}
public static void beginScaled(GuiGraphicsExtractor ctx) {
ViewportContext.beginScaled(ctx);
}
public static void end(GuiGraphicsExtractor ctx) {
ViewportContext.end(ctx);
}
public static float getScaleFactor() {
return ViewportContext.getScaleFactor();
}
public static float getUiScale() {
return ViewportContext.getUiScale();
}
public static GuiGraphicsExtractor getCurrentContext() {
return ViewportContext.getCurrentContext();
}
public static void unscaledProjection() {
ViewportContext.unscaledProjection();
}
public static void logicalProjection() {
ViewportContext.logicalProjection();
}
public static void scaledProjection() {
ViewportContext.scaledProjection();
}
public static void projectionForSize(float width, float height) {
ViewportContext.projectionForSize(width, height);
}
}

View file

@ -0,0 +1,71 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine;
import net.minecraft.client.renderer.culling.Frustum;
import net.minecraft.world.level.material.FogType;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext;
/**
* Engine global render flags/state. Set these in render hooks.
*/
public enum RenderState {
;
public static final Quaternionf cameraRotation = new Quaternionf();
public static final Matrix4f worldProjection = new Matrix4f().identity();
public static boolean rendering3D = false;
/**
* Legacy alias for vanilla tick progress.
*/
public static float tickDelta = 0f;
public static float tickProgress = 0f;
public static float frameDeltaTicks = 0f;
public static float frameDeltaSeconds = 0f;
public static float fixedDeltaTicks = 0f;
public static Vec3 cameraPos = Vec3.ZERO;
public static float cameraYaw = 0f;
public static float cameraPitch = 0f;
public static Vec3 cameraLook = Vec3.ZERO;
public static FogType cameraSubmersion = FogType.NONE;
public static boolean worldTranslucent = false;
public static @Nullable Frustum frustum;
public static float lineWidth = 1.0f;
// Active world frame id (set on begin).
public static int activeWorldFrameId = -1;
public static void applyContext(RenderFrameContext context) {
if (context == null) return;
tickDelta = context.tickDelta();
tickProgress = context.tickProgress();
frameDeltaTicks = context.frameDeltaTicks();
frameDeltaSeconds = context.frameDeltaSeconds();
fixedDeltaTicks = context.fixedDeltaTicks();
if (context.camera() != null) {
cameraPos = context.camera().position();
cameraLook = context.camera().look();
cameraRotation.set(context.camera().rotation());
worldProjection.set(context.camera().projection());
}
}
public static void clearContextBackedState() {
rendering3D = false;
worldTranslucent = false;
activeWorldFrameId = -1;
tickDelta = 0f;
tickProgress = 0f;
frameDeltaTicks = 0f;
frameDeltaSeconds = 0f;
fixedDeltaTicks = 0f;
}
}

View file

@ -0,0 +1,60 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.animation;
import dev.loki.lovisual.render.engine.animation.text.AnimatedTextLine;
import dev.loki.lovisual.render.engine.animation.text.AnimatedTextStyle;
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public final class AnimatedClockText {
private static final DateTimeFormatter FORMAT_MINUTES = DateTimeFormatter.ofPattern("HH:mm");
private static final DateTimeFormatter FORMAT_SECONDS = DateTimeFormatter.ofPattern("HH:mm:ss");
private final AnimatedTextLine line = new AnimatedTextLine();
private AnimatedTextStyle style = AnimatedTextStyle.clockLiquidGlass();
private boolean showSeconds;
public void setStyle(AnimatedTextStyle style) {
this.style = style != null ? style : AnimatedTextStyle.clockLiquidGlass();
}
public void update(LocalTime time, boolean showSeconds, long nowMs) {
this.showSeconds = showSeconds;
LocalTime safeTime = time != null ? time : LocalTime.now();
line.setText(safeTime.format(showSeconds ? FORMAT_SECONDS : FORMAT_MINUTES), nowMs, style);
}
public String text() {
return line.text();
}
public boolean showSeconds() {
return showSeconds;
}
public float width(TextRenderer renderer, float size) {
return line.width(renderer, size, style);
}
public float height(TextRenderer renderer, float size) {
return line.height(renderer, size, style);
}
public void renderLiquidGlassCentered(TextRenderer renderer,
float centerX,
float y,
float size,
int argb,
long nowMs) {
line.renderLiquidGlassCentered(renderer, centerX, y, size, argb, nowMs, style);
}
}

View file

@ -0,0 +1,35 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.animation;
/**
* Общий набор строковых опций {@link ModeValue}-настройки цвета и парсер в
* {@link AnimatedRenderColors.Mode} — вынесено, чтобы каждый модуль с
* анимированным цветом не дублировал один и тот же switch (см. HitEffect,
* HitBubbles).
*/
public enum AnimatedColorModeOptions {
;
public static final String[] OPTIONS = {
"Static", "Rainbow", "LightRainbow", "Sky", "Fade", "DoubleColor", "Analogous", "Theme"
};
public static AnimatedRenderColors.Mode parse(String value) {
return switch (value == null ? "" : value) {
case "Rainbow" -> AnimatedRenderColors.Mode.RAINBOW;
case "LightRainbow" -> AnimatedRenderColors.Mode.LIGHT_RAINBOW;
case "Sky" -> AnimatedRenderColors.Mode.SKY;
case "Fade" -> AnimatedRenderColors.Mode.FADE;
case "DoubleColor" -> AnimatedRenderColors.Mode.DOUBLE_COLOR;
case "Analogous" -> AnimatedRenderColors.Mode.ANALOGOUS;
case "Theme" -> AnimatedRenderColors.Mode.THEME;
default -> AnimatedRenderColors.Mode.STATIC;
};
}
}

View file

@ -0,0 +1,339 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.animation;
import dev.loki.lovisual.features.theme.Theme;
import dev.loki.lovisual.features.theme.Themes;
import dev.loki.lovisual.render.engine.math.ColorMath;
import java.awt.Color;
/**
* Shared animated color resolver for visual modules and HUD text gradients.
*
* <p>Callers provide the semantic mode, speed, phase/index and the configured
* primary/secondary colors. The resolver keeps the old module color semantics
* in one place instead of duplicating ColorUtils switch blocks in every module.</p>
*/
public enum AnimatedRenderColors {
;
private static final int FULL_ALPHA = 0xFF000000;
private static final float MODULE_LIST_ANIMATED_WEIGHT = 0.86f;
private static final float MODULE_LIST_PALETTE_CYCLES_PER_SECOND = 0.025f;
public enum Mode {
STATIC,
RAINBOW,
LIGHT_RAINBOW,
SKY,
FADE,
DOUBLE_COLOR,
ANALOGOUS,
THEME
}
public static boolean usesSecondary(Mode mode) {
return mode == Mode.DOUBLE_COLOR || mode == Mode.ANALOGOUS;
}
public static boolean animated(Mode mode) {
return mode != null && mode != Mode.STATIC;
}
public static int shaderMode(Mode mode) {
return switch (mode != null ? mode : Mode.STATIC) {
case STATIC -> 0;
case RAINBOW -> 1;
case LIGHT_RAINBOW -> 2;
case SKY -> 3;
case FADE -> 4;
case DOUBLE_COLOR -> 5;
case ANALOGOUS -> 6;
case THEME -> 7;
};
}
public static float angularOffset01(Mode mode, int speed) {
if (!animated(mode)) {
return 0.0f;
}
int safeSpeed = Math.max(1, speed);
return wrap01(AnimationUtility.time(1.0f / (safeSpeed * 360.0f)));
}
public static int angularPrimaryColor(Mode mode, int primaryArgb) {
if (mode == Mode.THEME) {
return ColorMath.colorWithAlpha(Theme.theme().accent(), alpha(primaryArgb));
}
return primaryArgb;
}
public static int angularSecondaryColor(Mode mode, int primaryArgb, int secondaryArgb) {
return switch (mode != null ? mode : Mode.STATIC) {
case ANALOGOUS -> analogous(secondaryArgb);
case THEME -> ColorMath.colorWithAlpha(Theme.theme().accentSoft(), alpha(primaryArgb));
case FADE, STATIC, RAINBOW, LIGHT_RAINBOW, SKY -> primaryArgb;
case DOUBLE_COLOR -> secondaryArgb;
};
}
public static int resolve(Mode mode, int speed, int phase, int primaryArgb, int secondaryArgb) {
return resolve(mode, speed, phase, primaryArgb, secondaryArgb, false);
}
public static int resolve(Mode mode,
int speed,
int phase,
int primaryArgb,
int secondaryArgb,
boolean preservePrimaryAlpha) {
Mode resolved = mode != null ? mode : Mode.STATIC;
int safeSpeed = Math.max(1, speed);
int index = Math.floorMod(phase, 36000);
int alpha = alpha(primaryArgb);
return switch (resolved) {
case SKY -> withAlpha(skyRainbowRgb(safeSpeed, index), preservePrimaryAlpha ? alpha : 255);
case LIGHT_RAINBOW -> withAlpha(rainbowRgb(safeSpeed, index, 0.6f, 1.0f), preservePrimaryAlpha ? alpha : 255);
case RAINBOW -> withAlpha(rainbowRgb(safeSpeed, index, 1.0f, 1.0f), preservePrimaryAlpha ? alpha : 255);
case FADE -> fade(primaryArgb, safeSpeed, index, preservePrimaryAlpha ? alpha : 255);
case DOUBLE_COLOR -> twoColor(primaryArgb, secondaryArgb, safeSpeed, index);
case ANALOGOUS -> twoColorHue(primaryArgb, analogous(secondaryArgb), safeSpeed, index);
case THEME -> themeColor(safeSpeed, index, preservePrimaryAlpha ? alpha : 255);
case STATIC -> primaryArgb;
};
}
public static int mixArgb(int a, int b, float t) {
float clamped = clamp01(t);
int aa = Math.round(alpha(a) + (alpha(b) - alpha(a)) * clamped);
int rr = Math.round(red(a) + (red(b) - red(a)) * clamped);
int gg = Math.round(green(a) + (green(b) - green(a)) * clamped);
int bb = Math.round(blue(a) + (blue(b) - blue(a)) * clamped);
return argb(aa, rr, gg, bb);
}
public static int withAlpha(int argb, int alpha) {
return (argb & 0x00FFFFFF) | (clamp255(alpha) << 24);
}
public static int scaleAlpha(int argb, float scale) {
return withAlpha(argb, Math.round(alpha(argb) * clamp01(scale)));
}
public static int brighten(int argb, float factor) {
float f = Math.max(0.0f, factor);
return argb(alpha(argb), clamp255(Math.round(red(argb) * f)), clamp255(Math.round(green(argb) * f)), clamp255(Math.round(blue(argb) * f)));
}
public static int analogousColor(int argb) {
return analogous(argb);
}
public static void moduleListBlockGlyphGradient(int topArgb,
int bottomArgb,
Mode mode,
int speed,
double glyphY0,
double glyphY1,
double blockY0,
double blockY1,
int glyphIndex,
float timeSec,
boolean topRainbow,
boolean bottomRainbow,
boolean shimmer,
int[] out) {
if (out == null || out.length < 4) return;
float topT = blockProgress(glyphY0, blockY0, blockY1);
float bottomT = blockProgress(glyphY1, blockY0, blockY1);
int topColor = moduleListBlockColor(topArgb, bottomArgb, mode, speed, topT, glyphIndex, timeSec, topRainbow, bottomRainbow, shimmer);
int bottomColor = moduleListBlockColor(topArgb, bottomArgb, mode, speed, bottomT, glyphIndex, timeSec, topRainbow, bottomRainbow, shimmer);
out[0] = topColor;
out[1] = bottomColor;
out[2] = bottomColor;
out[3] = topColor;
}
public static int moduleListBlockColor(int topArgb,
int bottomArgb,
Mode mode,
int speed,
float blockProgress,
int glyphIndex,
float timeSec,
boolean topRainbow,
boolean bottomRainbow,
boolean shimmer) {
int safeSpeed = Math.max(1, speed);
float t = clamp01(blockProgress);
Mode resolved = mode != null ? mode : Mode.STATIC;
int base = mixArgb(topArgb, bottomArgb, t);
boolean rainbowEndpoints = topRainbow || bottomRainbow;
if (rainbowEndpoints) {
int endpointPalette = moduleListPaletteColor(Mode.RAINBOW, safeSpeed, t, timeSec,
topRainbow ? topArgb : bottomArgb,
bottomRainbow ? bottomArgb : topArgb);
base = mixArgb(base, endpointPalette, MODULE_LIST_ANIMATED_WEIGHT);
}
if (resolved != Mode.STATIC) {
int animated = moduleListPaletteColor(resolved, safeSpeed, t, timeSec, topArgb, bottomArgb);
base = mixArgb(base, animated, MODULE_LIST_ANIMATED_WEIGHT);
}
if (shimmer) {
float wave = 0.5f + 0.5f * (float) Math.sin(timeSec * safeSpeed * 0.16f + t * 6.2831855f + glyphIndex * 0.035f);
int dark = mixArgb(base, withAlpha(0xFF000000, alpha(base)), 0.18f);
int light = mixArgb(base, withAlpha(0xFFFFFFFF, alpha(base)), 0.28f);
base = mixArgb(dark, light, wave);
}
return base;
}
private static int moduleListPaletteColor(Mode mode,
int speed,
float blockProgress,
float timeSec,
int primaryArgb,
int secondaryArgb) {
int safeSpeed = Math.max(1, speed);
float t = clamp01(blockProgress);
float paletteOffset = animated(mode) ? paletteOffset(timeSec, safeSpeed) : 0.0f;
float paletteT = wrap01(t + paletteOffset);
int alpha = Math.round(alpha(primaryArgb) + (alpha(secondaryArgb) - alpha(primaryArgb)) * t);
return switch (mode != null ? mode : Mode.STATIC) {
case RAINBOW -> withAlpha(Color.HSBtoRGB(paletteT, 1.0f, 1.0f), alpha);
case LIGHT_RAINBOW -> withAlpha(Color.HSBtoRGB(paletteT, 0.6f, 1.0f), alpha);
case SKY -> withAlpha(Color.HSBtoRGB(skyHue(paletteT), 0.5f, 1.0f), alpha);
case FADE -> fadeThroughValue(primaryArgb, cyclicMix(paletteT), alpha);
case DOUBLE_COLOR -> mixArgb(primaryArgb, secondaryArgb, cyclicMix(paletteT));
case ANALOGOUS -> mixArgb(primaryArgb, analogous(secondaryArgb), cyclicMix(paletteT));
case THEME -> themeColor(safeSpeed, Math.round(paletteT * 360.0f), alpha);
case STATIC -> mixArgb(primaryArgb, secondaryArgb, t);
};
}
private static int themeColor(int speed, int index, int alpha) {
Themes.Theme theme = Theme.theme();
int primary = withAlpha(theme.accent(), alpha);
int secondary = withAlpha(theme.accentSoft(), alpha);
return twoColor(primary, secondary, speed, index);
}
private static float paletteOffset(float timeSec, int speed) {
return wrap01(timeSec * Math.max(1, speed) * MODULE_LIST_PALETTE_CYCLES_PER_SECOND);
}
private static float cyclicMix(float value) {
float wrapped = wrap01(value);
return wrapped < 0.5f ? wrapped * 2.0f : (1.0f - wrapped) * 2.0f;
}
private static float blockProgress(double y, double blockY0, double blockY1) {
double height = Math.max(1.0, blockY1 - blockY0);
return clamp01((float) ((y - blockY0) / height));
}
private static int fadeThroughValue(int primaryArgb, float blockProgress, int alpha) {
float[] hsb = Color.RGBtoHSB(red(primaryArgb), green(primaryArgb), blue(primaryArgb), null);
float value = 0.38f + 0.62f * clamp01(blockProgress);
int rgb = Color.HSBtoRGB(hsb[0], hsb[1], value) & 0x00FFFFFF;
return (clamp255(alpha) << 24) | rgb;
}
private static float skyHue(float t) {
float wrapped = wrap01(t);
return wrapped < 0.5f ? -wrapped : wrapped;
}
private static float wrap01(float value) {
return value - (float) Math.floor(value);
}
private static int rainbowRgb(int speed, int index, float saturation, float brightness) {
int angle = (int) ((System.currentTimeMillis() / Math.max(1, speed) + index) % 360);
return FULL_ALPHA | (Color.HSBtoRGB(angle / 360.0f, saturation, brightness) & 0x00FFFFFF);
}
private static int skyRainbowRgb(int speed, int index) {
int angle = (int) ((System.currentTimeMillis() / Math.max(1, speed) + index) % 360);
float hue = ((angle %= 360) / 360.0f) < 0.5f ? -(angle / 360.0f) : (angle / 360.0f);
return FULL_ALPHA | (Color.HSBtoRGB(hue, 0.5f, 1.0f) & 0x00FFFFFF);
}
private static int fade(int primaryArgb, int speed, int index, int alpha) {
float[] hsb = Color.RGBtoHSB(red(primaryArgb), green(primaryArgb), blue(primaryArgb), null);
int angle = (int) ((System.currentTimeMillis() / Math.max(1, speed) + index) % 360);
angle = (angle > 180 ? 360 - angle : angle) + 180;
int rgb = Color.HSBtoRGB(hsb[0], hsb[1], angle / 360.0f) & 0x00FFFFFF;
return (clamp255(alpha) << 24) | rgb;
}
private static int twoColor(int primaryArgb, int secondaryArgb, int speed, int index) {
int angle = (int) ((System.currentTimeMillis() / Math.max(1, speed) + index) % 360);
angle = (angle >= 180 ? 360 - angle : angle) * 2;
return mixArgb(primaryArgb, secondaryArgb, angle / 360.0f);
}
private static int twoColorHue(int primaryArgb, int secondaryArgb, int speed, int index) {
int angle = (int) ((System.currentTimeMillis() / Math.max(1, speed) + index) % 360);
angle = (angle >= 180 ? 360 - angle : angle) * 2;
float t = clamp01(angle / 360.0f);
float[] a = Color.RGBtoHSB(red(primaryArgb), green(primaryArgb), blue(primaryArgb), null);
float[] b = Color.RGBtoHSB(red(secondaryArgb), green(secondaryArgb), blue(secondaryArgb), null);
int rgb = Color.HSBtoRGB(lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)) & 0x00FFFFFF;
int outAlpha = Math.round(alpha(primaryArgb) + (alpha(secondaryArgb) - alpha(primaryArgb)) * t);
return (clamp255(outAlpha) << 24) | rgb;
}
private static int analogous(int argb) {
float[] hsb = Color.RGBtoHSB(red(argb), green(argb), blue(argb), null);
int rgb = Color.HSBtoRGB(hsb[0] - 0.84f, hsb[1], hsb[2]) & 0x00FFFFFF;
return (alpha(argb) << 24) | rgb;
}
private static int argb(int a, int r, int g, int b) {
return (clamp255(a) << 24) | (clamp255(r) << 16) | (clamp255(g) << 8) | clamp255(b);
}
private static int alpha(int argb) {
return (argb >>> 24) & 0xFF;
}
private static int red(int argb) {
return (argb >>> 16) & 0xFF;
}
private static int green(int argb) {
return (argb >>> 8) & 0xFF;
}
private static int blue(int argb) {
return argb & 0xFF;
}
private static int clamp255(int value) {
return Math.max(0, Math.min(255, value));
}
private static float clamp01(float value) {
return Math.max(0.0f, Math.min(1.0f, value));
}
private static float lerp(float a, float b, float t) {
return a + (b - a) * clamp01(t);
}
}

View file

@ -0,0 +1,114 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.animation;
import net.minecraft.util.Mth;
import dev.loki.lovisual.util.FastFps;
public enum AnimationUtility {
;
public static float deltaTime() {
return FastFps.getDeltaSeconds();
}
public static float time(float speed) {
return (System.currentTimeMillis() % 1_000_000L) * speed;
}
public static float clamp(float value, float min, float max) {
return Math.max(min, Math.min(max, value));
}
public static double clamp(double value, double min, double max) {
return Math.max(min, Math.min(max, value));
}
public static float clamp01(float value) {
return clamp(value, 0f, 1f);
}
public static float lerp(float start, float end, float t) {
return start + (end - start) * clamp01(t);
}
public static double lerp(double start, double end, double t) {
return start + (end - start) * clamp(t, 0.0, 1.0);
}
public static float approach(float value, float target, float speed) {
return lerp(value, target, speed);
}
public static double approach(double value, double target, double speed) {
return lerp(value, target, speed);
}
public static float approach(float value, float target, float dt, float speed) {
return approach(value, target, dt * speed);
}
public static float snap(float value, float target, float epsilon) {
return Math.abs(value - target) <= Math.max(0f, epsilon) ? target : value;
}
public static float smoothstep(float value) {
float t = clamp01(value);
return t * t * (3f - 2f * t);
}
public static float easeOutCubic(float value) {
float t = clamp01(value);
return 1f - (float) Math.pow(1f - t, 3f);
}
public static float easeInCubic(float value) {
float t = clamp01(value);
return t * t * t;
}
public static float easeInOutCubic(float value) {
float t = clamp01(value);
return t < 0.5f
? 4f * t * t * t
: 1f - (float) Math.pow(-2f * t + 2f, 3f) * 0.5f;
}
public static float easeOutBack(float value) {
return easeOutBack(value, 1.70158f);
}
public static float easeOutBack(float value, float overshoot) {
float t = clamp01(value);
float c1 = Math.max(0f, overshoot);
float c3 = c1 + 1f;
float p = t - 1f;
return 1f + c3 * p * p * p + c1 * p * p;
}
public static float easeInBack(float value) {
return easeInBack(value, 1.70158f);
}
public static float easeInBack(float value, float overshoot) {
float t = clamp01(value);
float c1 = Math.max(0f, overshoot);
float c3 = c1 + 1f;
return c3 * t * t * t - c1 * t * t;
}
public static boolean blink(long intervalMs) {
if (intervalMs <= 0L) return true;
return (System.currentTimeMillis() / intervalMs) % 2L == 0L;
}
public static float fast(float end, float start, float multiple) {
float clampedDelta = Mth.clamp(deltaTime() * multiple, 0f, 1f);
return approach(end, start, clampedDelta);
}
}

View file

@ -0,0 +1,70 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.animation.text;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
public final class AnimatedGlyphSlot {
private char current;
private char previous;
private long animationStartMs;
private boolean animating;
public AnimatedGlyphSlot(char initial) {
this.current = initial;
this.previous = initial;
this.animationStartMs = 0L;
this.animating = false;
}
public char current() {
return current;
}
public char previous() {
return previous;
}
public boolean isAnimating(long nowMs, AnimatedTextStyle style) {
if (!animating) return false;
if (progress(nowMs, style) >= 1.0f) {
animating = false;
previous = current;
return false;
}
return true;
}
public float progress(long nowMs, AnimatedTextStyle style) {
if (!animating || style == null) return 1.0f;
return AnimationUtility.clamp01((nowMs - animationStartMs) / (float) style.durationMs());
}
public void set(char next, long nowMs, AnimatedTextStyle style) {
if (next == current) return;
boolean animate = shouldAnimate(current, next, style);
previous = current;
current = next;
if (animate) {
animationStartMs = nowMs;
animating = true;
} else {
animationStartMs = 0L;
animating = false;
previous = current;
}
}
private static boolean shouldAnimate(char oldGlyph, char newGlyph, AnimatedTextStyle style) {
if (style == null || style.transition() == AnimatedTextTransition.NONE) return false;
if (oldGlyph == 0 || newGlyph == 0) return false;
if (!style.animateDigitsOnly()) return true;
return Character.isDigit(oldGlyph) && Character.isDigit(newGlyph);
}
}

View file

@ -0,0 +1,231 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.animation.text;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import dev.loki.lovisual.render.engine.color.RenderColor;
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
import dev.loki.lovisual.render.helpers.clip.ScissorFunction;
import java.util.ArrayList;
import java.util.List;
public final class AnimatedTextLine {
private final List<AnimatedGlyphSlot> slots = new ArrayList<>();
private String text = "";
public void setText(String nextText, long nowMs, AnimatedTextStyle style) {
String next = nextText != null ? nextText : "";
if (next.equals(text) && slots.size() == next.length()) return;
while (slots.size() < next.length()) {
slots.add(new AnimatedGlyphSlot(next.charAt(slots.size())));
}
while (slots.size() > next.length()) {
slots.remove(slots.size() - 1);
}
for (int i = 0; i < next.length(); i++) {
slots.get(i).set(next.charAt(i), nowMs, style);
}
text = next;
}
public String text() {
return text;
}
public float width(TextRenderer renderer, float size, AnimatedTextStyle style) {
if (renderer == null || text.isEmpty()) return 0.0f;
Metrics metrics = measure(renderer, size, style);
return metrics.totalWidth;
}
public float height(TextRenderer renderer, float size, AnimatedTextStyle style) {
if (renderer == null) return 0.0f;
renderer.begin(size, true, style != null && style.bigGlyphs());
try {
return (float) renderer.getHeight(false);
} finally {
renderer.end();
}
}
public void renderLiquidGlassCentered(TextRenderer renderer,
float centerX,
float y,
float size,
int argb,
long nowMs,
AnimatedTextStyle style) {
float w = width(renderer, size, style);
renderLiquidGlass(renderer, centerX - w * 0.5f, y, size, argb, nowMs, style);
}
public void renderLiquidGlass(TextRenderer renderer,
float x,
float y,
float size,
int argb,
long nowMs,
AnimatedTextStyle style) {
if (renderer == null || text.isEmpty()) return;
AnimatedTextStyle resolved = style != null ? style : AnimatedTextStyle.clockLiquidGlass();
Metrics metrics = measure(renderer, size, resolved);
if (metrics.totalWidth <= 0.0f || metrics.height <= 0.0f) return;
float cursorX = x;
for (int i = 0; i < slots.size(); i++) {
AnimatedGlyphSlot slot = slots.get(i);
char current = slot.current();
float slotW = metrics.slotWidths[i];
float currentW = metrics.glyphWidths[i];
float previousW = metrics.previousGlyphWidths[i];
float glyphX = cursorX + (slotW - currentW) * 0.5f;
float previousGlyphX = cursorX + (slotW - previousW) * 0.5f;
if (slot.isAnimating(nowMs, resolved)) {
renderAnimatedSlot(renderer, slot, previousGlyphX, glyphX, cursorX, y, slotW, metrics.height, size, argb, nowMs, resolved);
} else {
renderGlyph(renderer, Character.toString(current), glyphX, y, size, argb, resolved);
}
cursorX += slotW;
}
}
private void renderAnimatedSlot(TextRenderer renderer,
AnimatedGlyphSlot slot,
float previousGlyphX,
float glyphX,
float slotX,
float y,
float slotW,
float height,
float size,
int argb,
long nowMs,
AnimatedTextStyle style) {
float raw = slot.progress(nowMs, style);
float move = AnimationUtility.easeInOutCubic(raw);
float fadeIn = phase(raw, 0.08f, 0.86f);
float fadeOut = 1.0f - phase(raw, 0.14f, 0.78f);
float settle = AnimationUtility.easeOutBack(raw, 0.34f);
float offset = Math.max(1.0f, height * style.rollOffsetScale());
int oldColor = scaleAlpha(argb, fadeOut);
int newColor = scaleAlpha(argb, fadeIn);
float oldY = y;
float newY = y;
switch (style.transition()) {
case FADE -> {
oldY = y;
newY = y;
}
case ROLL_DOWN -> {
oldY = y + move * offset;
newY = y - (1.0f - settle) * offset;
}
case ROLL_UP -> {
oldY = y - move * offset;
newY = y + (1.0f - settle) * offset;
}
case NONE -> {
renderGlyph(renderer, Character.toString(slot.current()), glyphX, y, size, argb, style);
return;
}
}
float padY = height * style.clipPaddingScale();
boolean clipped = ScissorFunction.pushRaw(
slotX - 1.0f,
y - padY,
Math.max(1.0f, slotW + 2.0f),
Math.max(1.0f, height + padY * 2.0f)
);
try {
if (((oldColor >>> 24) & 0xFF) > 1) {
renderGlyph(renderer, Character.toString(slot.previous()), previousGlyphX, oldY, size, oldColor, style);
}
if (((newColor >>> 24) & 0xFF) > 1) {
renderGlyph(renderer, Character.toString(slot.current()), glyphX, newY, size, newColor, style);
}
} finally {
if (clipped) ScissorFunction.pop();
}
}
private void renderGlyph(TextRenderer renderer,
String glyph,
float x,
float y,
float size,
int argb,
AnimatedTextStyle style) {
if (glyph == null || glyph.isEmpty()) return;
renderer.setAlpha(1.0);
renderer.begin(size, false, style != null && style.bigGlyphs());
try {
renderer.renderLiquidGlass(glyph, x, y, new RenderColor(argb), false);
} finally {
renderer.end();
}
}
private Metrics measure(TextRenderer renderer, float size, AnimatedTextStyle style) {
int count = slots.size();
float[] slotWidths = new float[count];
float[] glyphWidths = new float[count];
float[] previousGlyphWidths = new float[count];
float total = 0.0f;
float height;
renderer.begin(size, true, style != null && style.bigGlyphs());
try {
float tabularDigitW = 0.0f;
if (style != null && style.tabularDigits()) {
for (char c = '0'; c <= '9'; c++) {
tabularDigitW = Math.max(tabularDigitW, (float) renderer.getWidth(Character.toString(c), false));
}
}
for (int i = 0; i < count; i++) {
AnimatedGlyphSlot slot = slots.get(i);
char glyph = slot.current();
char previous = slot.previous();
float glyphW = (float) renderer.getWidth(Character.toString(glyph), false);
float previousW = (float) renderer.getWidth(Character.toString(previous), false);
float slotW = Math.max(glyphW, previousW);
if (style != null && style.tabularDigits() && (Character.isDigit(glyph) || Character.isDigit(previous))) {
slotW = Math.max(slotW, tabularDigitW);
}
glyphWidths[i] = glyphW;
previousGlyphWidths[i] = previousW;
slotWidths[i] = slotW;
total += slotW;
}
height = (float) renderer.getHeight(false);
} finally {
renderer.end();
}
return new Metrics(slotWidths, glyphWidths, previousGlyphWidths, total, height);
}
private static float phase(float value, float from, float to) {
if (to <= from) return value >= to ? 1.0f : 0.0f;
return AnimationUtility.smoothstep((value - from) / (to - from));
}
private static int scaleAlpha(int argb, float factor) {
int a = (argb >>> 24) & 0xFF;
int scaled = Math.round(a * AnimationUtility.clamp01(factor));
return (argb & 0x00FFFFFF) | ((scaled & 0xFF) << 24);
}
private record Metrics(float[] slotWidths, float[] glyphWidths, float[] previousGlyphWidths, float totalWidth, float height) {
}
}

View file

@ -0,0 +1,74 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.animation.text;
public final class AnimatedTextStyle {
private final AnimatedTextTransition transition;
private final long durationMs;
private final float rollOffsetScale;
private final float clipPaddingScale;
private final boolean tabularDigits;
private final boolean animateDigitsOnly;
private final boolean bigGlyphs;
public AnimatedTextStyle(AnimatedTextTransition transition,
long durationMs,
float rollOffsetScale,
float clipPaddingScale,
boolean tabularDigits,
boolean animateDigitsOnly,
boolean bigGlyphs) {
this.transition = transition != null ? transition : AnimatedTextTransition.NONE;
this.durationMs = Math.max(1L, durationMs);
this.rollOffsetScale = Math.max(0.0f, rollOffsetScale);
this.clipPaddingScale = Math.max(0.0f, clipPaddingScale);
this.tabularDigits = tabularDigits;
this.animateDigitsOnly = animateDigitsOnly;
this.bigGlyphs = bigGlyphs;
}
public static AnimatedTextStyle clockLiquidGlass() {
return new AnimatedTextStyle(
AnimatedTextTransition.ROLL_UP,
520L,
0.58f,
0.18f,
true,
true,
true
);
}
public AnimatedTextTransition transition() {
return transition;
}
public long durationMs() {
return durationMs;
}
public float rollOffsetScale() {
return rollOffsetScale;
}
public float clipPaddingScale() {
return clipPaddingScale;
}
public boolean tabularDigits() {
return tabularDigits;
}
public boolean animateDigitsOnly() {
return animateDigitsOnly;
}
public boolean bigGlyphs() {
return bigGlyphs;
}
}

View file

@ -0,0 +1,15 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.animation.text;
public enum AnimatedTextTransition {
NONE,
FADE,
ROLL_UP,
ROLL_DOWN
}

View file

@ -0,0 +1,38 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.color;
public enum ColorBlendMode {
ORIGINAL("Original"),
SOFT("Soft"),
ATMOSPHERIC("Atmospheric"),
CHROMATIC("Chromatic"),
ESSENCE("Essence");
private final String id;
ColorBlendMode(String id) {
this.id = id;
}
public static ColorBlendMode fromId(String id) {
if (id == null) {
return ORIGINAL;
}
for (ColorBlendMode mode : values()) {
if (mode.id.equalsIgnoreCase(id) || mode.name().equalsIgnoreCase(id)) {
return mode;
}
}
return ORIGINAL;
}
public String id() {
return id;
}
}

View file

@ -0,0 +1,139 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.color;
public enum ColorEffects {
;
public static int argb(int rgb, float alpha) {
int a = clamp255(Math.round(clamp01(alpha) * 255.0f));
return (a << 24) | (rgb & 0x00FFFFFF);
}
public static int argbWithBaseAlpha(int baseArgb, int rgb, float alphaMultiplier) {
int baseAlpha = (baseArgb >>> 24) & 0xFF;
int alpha = clamp255(Math.round(baseAlpha * clamp01(alphaMultiplier)));
return (alpha << 24) | (rgb & 0x00FFFFFF);
}
public static int multiplyAlpha(int argb, float alphaMultiplier) {
int alpha = (argb >>> 24) & 0xFF;
int outAlpha = clamp255(Math.round(alpha * clamp01(alphaMultiplier)));
return (outAlpha << 24) | (argb & 0x00FFFFFF);
}
public static int tintRgb(int sourceRgb, int tintRgb, float strength, ColorBlendMode mode, float phase) {
float t = clamp01(strength);
if (t <= 0.0f || mode == null || mode == ColorBlendMode.ORIGINAL) {
return sourceRgb & 0x00FFFFFF;
}
int source = sourceRgb & 0x00FFFFFF;
int tint = tintRgb & 0x00FFFFFF;
return switch (mode) {
case ORIGINAL -> source;
case SOFT -> mixRgb(source, tint, t * 0.72f);
case ATMOSPHERIC -> mixRgb(source, screenRgb(source, tint), t * 0.64f);
case CHROMATIC -> chromaticTint(source, tint, t, phase);
case ESSENCE -> essenceTint(source, tint, t, phase);
};
}
public static int mixRgb(int a, int b, float amount) {
float t = clamp01(amount);
int ar = (a >>> 16) & 0xFF;
int ag = (a >>> 8) & 0xFF;
int ab = a & 0xFF;
int br = (b >>> 16) & 0xFF;
int bg = (b >>> 8) & 0xFF;
int bb = b & 0xFF;
int r = Math.round(ar + (br - ar) * t);
int g = Math.round(ag + (bg - ag) * t);
int bl = Math.round(ab + (bb - ab) * t);
return (clamp255(r) << 16) | (clamp255(g) << 8) | clamp255(bl);
}
public static int screenRgb(int a, int b) {
int ar = (a >>> 16) & 0xFF;
int ag = (a >>> 8) & 0xFF;
int ab = a & 0xFF;
int br = (b >>> 16) & 0xFF;
int bg = (b >>> 8) & 0xFF;
int bb = b & 0xFF;
int r = 255 - ((255 - ar) * (255 - br)) / 255;
int g = 255 - ((255 - ag) * (255 - bg)) / 255;
int bl = 255 - ((255 - ab) * (255 - bb)) / 255;
return (clamp255(r) << 16) | (clamp255(g) << 8) | clamp255(bl);
}
public static int overlayRgb(int a, int b) {
int ar = (a >>> 16) & 0xFF;
int ag = (a >>> 8) & 0xFF;
int ab = a & 0xFF;
int br = (b >>> 16) & 0xFF;
int bg = (b >>> 8) & 0xFF;
int bb = b & 0xFF;
int r = overlayChannel(ar, br);
int g = overlayChannel(ag, bg);
int bl = overlayChannel(ab, bb);
return (r << 16) | (g << 8) | bl;
}
public static float hash01(float seed) {
float value = (float) (Math.sin(seed * 12.9898f + 78.233f) * 43758.5453f);
return value - (float) Math.floor(value);
}
private static int chromaticTint(int source, int tint, float strength, float phase) {
int shifted = rotateRgb(tint, hash01(phase) < 0.5f ? 1 : 2);
int sourceScreen = screenRgb(source, shifted);
int colorized = mixRgb(shifted, sourceScreen, 0.42f);
return mixRgb(source, colorized, strength * 0.72f);
}
private static int essenceTint(int source, int tint, float strength, float phase) {
int warm = mixRgb(tint, rotateRgb(tint, 1), 0.22f + hash01(phase) * 0.28f);
int overlay = overlayRgb(source, warm);
int glow = screenRgb(source, warm);
int enhanced = mixRgb(overlay, glow, 0.36f);
return mixRgb(source, enhanced, strength * 0.82f);
}
private static int rotateRgb(int rgb, int steps) {
int r = (rgb >>> 16) & 0xFF;
int g = (rgb >>> 8) & 0xFF;
int b = rgb & 0xFF;
int s = Math.floorMod(steps, 3);
if (s == 1) {
return (g << 16) | (b << 8) | r;
}
if (s == 2) {
return (b << 16) | (r << 8) | g;
}
return rgb & 0x00FFFFFF;
}
private static int overlayChannel(int a, int b) {
if (a < 128) {
return clamp255((2 * a * b) / 255);
}
return clamp255(255 - (2 * (255 - a) * (255 - b)) / 255);
}
private static float clamp01(float value) {
if (value < 0.0f) return 0.0f;
if (value > 1.0f) return 1.0f;
return value;
}
private static int clamp255(int value) {
if (value < 0) return 0;
if (value > 255) return 255;
return value;
}
}

View file

@ -0,0 +1,84 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.color;
import java.awt.*;
public enum ColorUtils {
;
public static Color rainbow(int speed, int index, float saturation, float brightness, float opacity) {
int angle = (int) ((System.currentTimeMillis() / speed + index) % 360);
float hue = angle / 360f;
Color color = new Color(Color.HSBtoRGB(hue, saturation, brightness));
return new Color(color.getRed(), color.getGreen(), color.getBlue(), clampAlpha(opacity));
}
public static Color skyRainbow(int speed, int index) {
int angle = (int) ((System.currentTimeMillis() / speed + index) % 360);
float hue = ((angle %= 360) / 360.0) < 0.5 ? -((float) (angle / 360.0)) : (float) (angle / 360.0);
return Color.getHSBColor(hue, 0.5f, 1.0f);
}
public static Color fade(int speed, int index, Color color, float alpha) {
float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null);
int angle = (int) ((System.currentTimeMillis() / speed + index) % 360);
angle = (angle > 180 ? 360 - angle : angle) + 180;
Color colorHSB = new Color(Color.HSBtoRGB(hsb[0], hsb[1], angle / 360f));
return new Color(colorHSB.getRed(), colorHSB.getGreen(), colorHSB.getBlue(), clampAlpha(alpha));
}
public static Color twoColorEffect(Color c1, Color c2, double speed, double count) {
int angle = (int) (((System.currentTimeMillis()) / speed + count) % 360);
angle = (angle >= 180 ? 360 - angle : angle) * 2;
return interpolateColorC(c1, c2, angle / 360f);
}
public static Color getAnalogousColor(Color color) {
float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null);
float degree = 0.84f;
float newHueSubtracted = hsb[0] - degree;
return new Color(Color.HSBtoRGB(newHueSubtracted, hsb[1], hsb[2]));
}
public static Color interpolateColorsBackAndForth(int speed, int index, Color start, Color end, boolean trueColor) {
int angle = (int) (((System.currentTimeMillis()) / speed + index) % 360);
angle = (angle >= 180 ? 360 - angle : angle) * 2;
return trueColor ? interpolateColorHue(start, end, angle / 360f) : interpolateColorC(start, end, angle / 360f);
}
public static Color interpolateColorC(Color color1, Color color2, float amount) {
amount = Math.min(1f, Math.max(0f, amount));
return new Color(
interpolateInt(color1.getRed(), color2.getRed(), amount),
interpolateInt(color1.getGreen(), color2.getGreen(), amount),
interpolateInt(color1.getBlue(), color2.getBlue(), amount),
interpolateInt(color1.getAlpha(), color2.getAlpha(), amount)
);
}
public static Color interpolateColorHue(Color color1, Color color2, float amount) {
amount = Math.min(1f, Math.max(0f, amount));
float[] c1 = Color.RGBtoHSB(color1.getRed(), color1.getGreen(), color1.getBlue(), null);
float[] c2 = Color.RGBtoHSB(color2.getRed(), color2.getGreen(), color2.getBlue(), null);
Color result = Color.getHSBColor(interpolateFloat(c1[0], c2[0], amount), interpolateFloat(c1[1], c2[1], amount), interpolateFloat(c1[2], c2[2], amount));
return new Color(result.getRed(), result.getGreen(), result.getBlue(), interpolateInt(color1.getAlpha(), color2.getAlpha(), amount));
}
private static int interpolateInt(int oldValue, int newValue, double t) {
return (int) (oldValue + (newValue - oldValue) * t);
}
private static float interpolateFloat(float oldValue, float newValue, double t) {
return (float) (oldValue + (newValue - oldValue) * t);
}
private static int clampAlpha(float opacity) {
return Math.max(0, Math.min(255, (int) (opacity * 255)));
}
}

View file

@ -0,0 +1,47 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.color;
public class RenderColor {
public int r;
public int g;
public int b;
public int a;
public RenderColor(int r, int g, int b, int a) {
this.r = clamp(r);
this.g = clamp(g);
this.b = clamp(b);
this.a = clamp(a);
}
public RenderColor(int argb) {
this.a = (argb >>> 24) & 0xFF;
this.r = (argb >>> 16) & 0xFF;
this.g = (argb >>> 8) & 0xFF;
this.b = argb & 0xFF;
}
private static int clamp(int v) {
if (v < 0) return 0;
if (v > 255) return 255;
return v;
}
public int argb() {
return (a << 24) | (r << 16) | (g << 8) | b;
}
public RenderColor copy() {
return new RenderColor(r, g, b, a);
}
public void setAlpha(int alpha) {
this.a = clamp(alpha);
}
}

View file

@ -0,0 +1,19 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.core;
import dev.loki.lovisual.render.engine.command.core.UiCommandKind;
/**
* Normalized UI command root.
* <p>
* Renderer2D facade methods and the newer shape/effect/text/item command families can
* coexist while callers move toward explicit UI command boundaries.
*/
public interface UiCommand {
UiCommandKind kind();
}

View file

@ -0,0 +1,72 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.core;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import dev.loki.lovisual.render.engine.command.core.UiCommand;
import dev.loki.lovisual.render.engine.command.core.UiCommandStats;
import dev.loki.lovisual.render.engine.command.world.RenderCommandBuffer;
import dev.loki.lovisual.render.engine.command.world.UiStatsSnapshot;
/**
* High-level normalized UI command stream.
*/
public final class UiCommandBuffer implements RenderCommandBuffer {
private final List<UiCommand> commands = new ArrayList<>();
private final UiCommandStats stats = new UiCommandStats();
public void beginFrame(RenderFrameContext context) {
if (context != null) {
stats.beginFrame(context.frameId());
}
}
public void add(UiCommand command) {
if (command == null) return;
RenderFrameContext ctx = LoVisualRenderSystem.currentContext();
if (ctx != null) stats.beginFrame(ctx.frameId());
commands.add(command);
stats.record(command);
}
public List<UiCommand> commands() {
return commands;
}
public List<UiCommand> snapshot() {
return Collections.unmodifiableList(commands);
}
public UiCommandStats stats() {
return stats;
}
public UiStatsSnapshot statsSnapshot() {
return stats.snapshot();
}
@Override
public void clear() {
commands.clear();
}
@Override
public int size() {
return commands.size();
}
@Override
public void submit(RenderFrameContext context) {
beginFrame(context);
}
}

View file

@ -0,0 +1,20 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.core;
public enum UiCommandKind {
SHAPE,
PATH,
PRIMITIVE,
TEXTURE,
TEXT,
ITEM,
BLUR_REGION,
LIQUID_GLASS_REGION,
EFFECT_REGION
}

View file

@ -0,0 +1,59 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.core;
import dev.loki.lovisual.render.engine.command.core.UiCommand;
import dev.loki.lovisual.render.engine.command.world.UiStatsSnapshot;
public final class UiCommandStats {
private long frameId;
private int recordedCommands;
private int shapeCommands;
private int pathCommands;
private int textureCommands;
private int textCommands;
private int itemCommands;
private int effectCommands;
private int compiledBatches;
private int backendCommands;
public void beginFrame(long frameId) {
if (this.frameId == frameId) return;
this.frameId = frameId;
recordedCommands = shapeCommands = pathCommands = textureCommands = textCommands = itemCommands = effectCommands = 0;
compiledBatches = backendCommands = 0;
}
public void record(UiCommand command) {
if (command == null) return;
recordedCommands++;
switch (command.kind()) {
case SHAPE -> shapeCommands++;
case PATH -> pathCommands++;
case TEXTURE -> textureCommands++;
case TEXT -> textCommands++;
case ITEM -> itemCommands++;
case BLUR_REGION, LIQUID_GLASS_REGION, EFFECT_REGION -> effectCommands++;
case PRIMITIVE -> backendCommands++;
default -> {
}
}
}
public void addCompiledBatches(int count) {
compiledBatches += Math.max(0, count);
}
public void addBackendCommand() {
backendCommands++;
}
public UiStatsSnapshot snapshot() {
return new UiStatsSnapshot(frameId, recordedCommands, shapeCommands, pathCommands, textureCommands,
textCommands, itemCommands, effectCommands, compiledBatches, backendCommands);
}
}

View file

@ -0,0 +1,24 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.effect;
import dev.loki.lovisual.render.engine.command.core.UiCommand;
import dev.loki.lovisual.render.engine.command.core.UiCommandKind;
/**
* Explicit marker for draw work that is executed by the ordered Renderer2D batch backend.
*
* <p>The command/RHI layer can see these batches instead of treating them as hidden side
* effects inside Renderer2D. The payload is intentionally small: it is for scheduling and
* profiling boundaries, not for replaying meshes.</p>
*/
public record UiBatchBackendCommand(String backend, String batchType) implements UiCommand {
@Override
public UiCommandKind kind() {
return UiCommandKind.PRIMITIVE;
}
}

View file

@ -0,0 +1,19 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.effect;
import dev.loki.lovisual.render.engine.command.core.UiCommand;
import dev.loki.lovisual.render.engine.command.core.UiCommandKind;
public record UiEffectCommand(UiCommandKind kind, float x, float y, float width, float height,
float radius) implements UiCommand {
public UiEffectCommand {
if (kind != UiCommandKind.BLUR_REGION && kind != UiCommandKind.LIQUID_GLASS_REGION) {
throw new IllegalArgumentException("UiEffectCommand kind must be a UI effect region");
}
}
}

View file

@ -0,0 +1,19 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.effect;
import dev.loki.lovisual.render.engine.renderer.ui.draw.effect.UiEffectSpec;
import dev.loki.lovisual.render.engine.command.core.UiCommand;
import dev.loki.lovisual.render.engine.command.core.UiCommandKind;
public record UiEffectRegionCommand(UiEffectSpec effect) implements UiCommand {
@Override
public UiCommandKind kind() {
return UiCommandKind.EFFECT_REGION;
}
}

View file

@ -0,0 +1,21 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.shape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.paint.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.shape.UiShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.edge.UiStroke;
import dev.loki.lovisual.render.engine.command.core.UiCommand;
import dev.loki.lovisual.render.engine.command.core.UiCommandKind;
public record UiPathCommand(UiShape path, UiPaint paint, UiStroke stroke, boolean fill) implements UiCommand {
@Override
public UiCommandKind kind() {
return UiCommandKind.PATH;
}
}

View file

@ -0,0 +1,17 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.shape;
import dev.loki.lovisual.render.engine.command.core.UiCommand;
import dev.loki.lovisual.render.engine.command.core.UiCommandKind;
public record UiPrimitiveCommand(float x, float y, float width, float height, int argb) implements UiCommand {
@Override
public UiCommandKind kind() {
return UiCommandKind.PRIMITIVE;
}
}

View file

@ -0,0 +1,21 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.shape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.paint.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.shape.UiShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.edge.UiStroke;
import dev.loki.lovisual.render.engine.command.core.UiCommand;
import dev.loki.lovisual.render.engine.command.core.UiCommandKind;
public record UiShapeCommand(UiShape shape, UiPaint paint, UiStroke stroke, boolean fill) implements UiCommand {
@Override
public UiCommandKind kind() {
return UiCommandKind.SHAPE;
}
}

View file

@ -0,0 +1,19 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.texture;
import net.minecraft.world.item.ItemStack;
import dev.loki.lovisual.render.engine.command.core.UiCommand;
import dev.loki.lovisual.render.engine.command.core.UiCommandKind;
public record UiItemCommand(ItemStack stack, int x, int y, float scale) implements UiCommand {
@Override
public UiCommandKind kind() {
return UiCommandKind.ITEM;
}
}

View file

@ -0,0 +1,17 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.texture;
import dev.loki.lovisual.render.engine.command.core.UiCommand;
import dev.loki.lovisual.render.engine.command.core.UiCommandKind;
public record UiTextCommand(String text, float x, float y, int argb, float size) implements UiCommand {
@Override
public UiCommandKind kind() {
return UiCommandKind.TEXT;
}
}

View file

@ -0,0 +1,20 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.texture;
import com.mojang.blaze3d.textures.GpuTextureView;
import dev.loki.lovisual.render.engine.command.core.UiCommand;
import dev.loki.lovisual.render.engine.command.core.UiCommandKind;
public record UiTextureCommand(GpuTextureView texture, float x, float y, float width, float height,
int argb) implements UiCommand {
@Override
public UiCommandKind kind() {
return UiCommandKind.TEXTURE;
}
}

View file

@ -0,0 +1,30 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.texture;
import com.mojang.blaze3d.textures.GpuSampler;
import com.mojang.blaze3d.textures.GpuTextureView;
import dev.loki.lovisual.render.engine.renderer.ui.draw.paint.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.shape.UiShape;
import dev.loki.lovisual.render.engine.command.core.UiCommand;
import dev.loki.lovisual.render.engine.command.core.UiCommandKind;
public record UiTextureDrawCommand(GpuTextureView view,
GpuSampler sampler,
UiShape shape,
UiPaint tint,
float u0,
float v0,
float u1,
float v1,
boolean masked) implements UiCommand {
@Override
public UiCommandKind kind() {
return UiCommandKind.TEXTURE;
}
}

View file

@ -0,0 +1,18 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.world;
import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext;
public interface RenderCommandBuffer {
void clear();
int size();
void submit(RenderFrameContext context);
}

View file

@ -0,0 +1,20 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.world;
public record UiStatsSnapshot(long frameId,
int recordedCommands,
int shapeCommands,
int pathCommands,
int textureCommands,
int textCommands,
int itemCommands,
int effectCommands,
int compiledBatches,
int backendCommands) {
}

View file

@ -0,0 +1,42 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.command.world;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext;
import dev.loki.lovisual.render.engine.rhi.command.RhiDrawCommand;
import java.util.ArrayList;
import java.util.List;
import dev.loki.lovisual.render.engine.command.world.RenderCommandBuffer;
public final class WorldCommandBuffer implements RenderCommandBuffer {
private final List<RhiDrawCommand> commands = new ArrayList<>();
public void add(RhiDrawCommand command) {
if (command != null) commands.add(command);
}
@Override
public void clear() {
commands.clear();
}
@Override
public int size() {
return commands.size();
}
@Override
public void submit(RenderFrameContext context) {
for (RhiDrawCommand command : commands) {
LoVisualRenderSystem.rhi().drawMesh(command);
}
clear();
}
}

View file

@ -0,0 +1,54 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.compat.immediatelyfast;
import net.raphimc.immediatelyfast.ImmediatelyFast;
import net.raphimc.immediatelyfast.feature.core.ImmediatelyFastConfig;
public enum ImmediatelyFastBridge {
;
static ImmediatelyFastRuntimeSnapshot snapshot(String version) {
ImmediatelyFastConfig config = ImmediatelyFast.config;
if (config == null) {
return defaults(version, "config not initialized yet");
}
boolean enhancedBatching = config.enhanced_batching;
boolean fastTextLookup = config.fast_text_lookup;
boolean fontAtlasResizing = config.font_atlas_resizing;
boolean skipTextSorting = config.skip_text_translucency_sorting;
boolean signTextBuffering = config.experimental_sign_text_buffering;
boolean framebufferSwitching = config.avoid_redundant_framebuffer_switching;
boolean appleUploadFix = config.fix_slow_buffer_upload_on_apple_gpu;
return new ImmediatelyFastRuntimeSnapshot(
true,
version,
framebufferSwitching,
enhancedBatching,
fastTextLookup || fontAtlasResizing || skipTextSorting || signTextBuffering,
enhancedBatching || signTextBuffering || appleUploadFix,
true,
"ok"
);
}
static ImmediatelyFastRuntimeSnapshot defaults(String version, String status) {
return new ImmediatelyFastRuntimeSnapshot(
true,
version,
true,
true,
true,
true,
false,
status == null || status.isBlank() ? "using default assumptions" : status
);
}
}

View file

@ -0,0 +1,63 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.compat.immediatelyfast;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import dev.loki.lovisual.util.logging.DebugLog;
public enum ImmediatelyFastRuntime {
;
private static final String MOD_ID = "immediatelyfast";
private static volatile boolean loggedConfigFailure;
public static ImmediatelyFastRuntimeSnapshot snapshot() {
FabricLoader loader = FabricLoader.getInstance();
if (!loader.isModLoaded(MOD_ID)) {
return ImmediatelyFastRuntimeSnapshot.UNLOADED;
}
String version = loader.getModContainer(MOD_ID)
.map(ImmediatelyFastRuntime::versionOf)
.orElse("");
try {
return ImmediatelyFastBridge.snapshot(version);
} catch (LinkageError | RuntimeException t) {
if (!loggedConfigFailure) {
loggedConfigFailure = true;
DebugLog.warn("[ImmediatelyFastCompat] Runtime probe failed: " + t);
}
return ImmediatelyFastBridge.defaults(version, t.getClass().getSimpleName());
}
}
public static boolean isModLoaded() {
return FabricLoader.getInstance().isModLoaded(MOD_ID);
}
public static boolean framebufferPassesModified() {
return snapshot().framebufferPassesModified();
}
public static boolean guiBatchingModified() {
return snapshot().guiBatchingModified();
}
public static boolean textRenderingModified() {
return snapshot().textRenderingModified();
}
public static boolean transientBufferHandlingModified() {
return snapshot().transientBufferHandlingModified();
}
private static String versionOf(ModContainer mod) {
return mod.getMetadata().getVersion().getFriendlyString();
}
}

View file

@ -0,0 +1,49 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.compat.immediatelyfast;
public record ImmediatelyFastRuntimeSnapshot(
boolean modLoaded,
String version,
boolean framebufferPassesModified,
boolean guiBatchingModified,
boolean textRenderingModified,
boolean transientBufferHandlingModified,
boolean configAvailable,
String status
) {
public static final ImmediatelyFastRuntimeSnapshot UNLOADED = new ImmediatelyFastRuntimeSnapshot(
false,
"",
false,
false,
false,
false,
false,
"immediatelyfast mod not loaded"
);
private static String yn(boolean value) {
return value ? "y" : "n";
}
public String shortLine() {
if (!modLoaded) {
return "immediatelyfast: not loaded";
}
String displayVersion = version == null || version.isBlank() ? "<unknown>" : version;
String config = configAvailable ? "config" : "defaults";
return "immediatelyfast: " + displayVersion
+ ", " + config
+ ", fb/gui/text/buf "
+ yn(framebufferPassesModified)
+ "/" + yn(guiBatchingModified)
+ "/" + yn(textRenderingModified)
+ "/" + yn(transientBufferHandlingModified);
}
}

View file

@ -0,0 +1,58 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.context;
import net.minecraft.client.Minecraft;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
/**
* Camera data captured once for a LoVisual frame/phase.
*/
public record CameraContext(Vec3 position, Vec3 look, Quaternionf rotation, Matrix4f projection, Matrix4f modelView) {
public CameraContext(Vec3 position, Vec3 look, Quaternionf rotation, Matrix4f projection, Matrix4f modelView) {
this.position = position;
this.look = look;
this.rotation = new Quaternionf(rotation);
this.projection = new Matrix4f(projection);
this.modelView = new Matrix4f(modelView);
}
public static CameraContext capture(Matrix4f projection, Matrix4f modelView) {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null) {
return new CameraContext(Vec3.ZERO, Vec3.ZERO, new Quaternionf(), projection, modelView);
}
var camera = mc.gameRenderer.mainCamera();
var forward = camera.forwardVector();
return new CameraContext(
camera.position(),
new Vec3(forward.x(), forward.y(), forward.z()),
camera.rotation(),
projection,
modelView
);
}
@Override
public Quaternionf rotation() {
return new Quaternionf(rotation);
}
@Override
public Matrix4f projection() {
return new Matrix4f(projection);
}
@Override
public Matrix4f modelView() {
return new Matrix4f(modelView);
}
}

View file

@ -0,0 +1,19 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.context;
import com.mojang.blaze3d.pipeline.RenderTarget;
import net.minecraft.client.Minecraft;
import org.jetbrains.annotations.Nullable;
public record FramebufferContext(@Nullable RenderTarget mainFramebuffer) {
public static FramebufferContext capture() {
Minecraft mc = Minecraft.getInstance();
return new FramebufferContext(mc != null ? mc.gameRenderer.mainRenderTarget() : null);
}
}

View file

@ -0,0 +1,298 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.context;
import dev.loki.lovisual.render.engine.Projection2D;
import com.mojang.blaze3d.ProjectionType;
import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.renderer.Projection;
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
import org.joml.Matrix4f;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.math.HudScale;
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
/**
* Unified 2D viewport/projection runtime state.
* <p>
* This replaces the old Projection2D ownership model: callers use ViewportContext for
* projection mode switches, UI scale information, DrawContext tracking and immutable
* per-frame viewport snapshots.
*/
public record ViewportContext(
int framebufferWidth,
int framebufferHeight,
float scaleFactor,
float width,
float height,
float uiScale,
ProjectionMode projectionMode,
Matrix4f projectionMatrix
) {
private static ProjectionMatrixBuffer matrixBuffer;
private static volatile ViewportContext current = fallback(ProjectionMode.SCALED);
private static float activeUiScale = 1.0f;
private static GuiGraphicsExtractor currentContext;
private static ProjectionMode activeMode = ProjectionMode.SCALED;
private static ProjectionMatrixBuffer matrixBuffer() {
ProjectionMatrixBuffer buffer = matrixBuffer;
if (buffer == null) {
buffer = new ProjectionMatrixBuffer("lovisual-projection-matrix");
matrixBuffer = buffer;
}
return buffer;
}
public static ViewportContext capture() {
return capture(activeMode);
}
public static ViewportContext capture(ProjectionMode mode) {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.getWindow() == null) {
return fallback(mode);
}
int fbw = Math.max(1, mc.getWindow().getWidth());
int fbh = Math.max(1, mc.getWindow().getHeight());
float scaleFactor = Math.max(1.0f, (float) mc.getWindow().getGuiScale());
float width;
float height;
float uiScale;
switch (mode) {
case UNSCALED -> {
width = fbw;
height = fbh;
uiScale = 1.0f;
}
case LOGICAL -> {
width = HudScale.virtualWidth(fbw, fbh);
height = HudScale.virtualHeight(fbw, fbh);
uiScale = HudScale.scale(fbw, fbh);
}
case CUSTOM -> {
ViewportContext previous = current;
width = previous != null ? previous.width : fbw;
height = previous != null ? previous.height : fbh;
uiScale = previous != null ? previous.uiScale : 1.0f;
}
case SCALED -> {
width = fbw / scaleFactor;
height = fbh / scaleFactor;
uiScale = 1.0f;
}
default -> throw new IllegalStateException("Unhandled projection mode: " + mode);
}
return new ViewportContext(fbw, fbh, scaleFactor, width, height, uiScale, mode, matrixFor(width, height));
}
public static ViewportContext current() {
return current;
}
public static GuiGraphicsExtractor getCurrentContext() {
return currentContext;
}
public static float getScaleFactor() {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.getWindow() == null) return 1.0f;
return (float) mc.getWindow().getGuiScale();
}
public static float getUiScale() {
return activeUiScale;
}
public static ProjectionMode activeMode() {
return activeMode;
}
public static void beginUnscaled(GuiGraphicsExtractor ctx) {
begin(ctx, ProjectionMode.UNSCALED);
}
public static void beginUnscaledLogical(GuiGraphicsExtractor ctx) {
begin(ctx, ProjectionMode.LOGICAL);
}
/**
* Attach the extractor and switch to logical HUD projection without creating a new
* GuiGraphics stratum. Use this only from hooks that are already inside an exact
* vanilla stratum, for example PlayerTabOverlay.extractRenderState.
*/
public static void beginCurrentStratumUnscaledLogical(GuiGraphicsExtractor ctx) {
begin(ctx, ProjectionMode.LOGICAL, false);
}
public static void beginScaled(GuiGraphicsExtractor ctx) {
begin(ctx, ProjectionMode.SCALED);
}
public static void end(GuiGraphicsExtractor ctx) {
begin(ctx, ProjectionMode.SCALED);
}
/**
* Restore vanilla scaled projection without allocating another GuiGraphics stratum.
* Use after beginCurrentStratumUnscaledLogical() from hooks that are already inside
* a vanilla-created stratum.
*/
public static void endCurrentStratum(GuiGraphicsExtractor ctx) {
begin(ctx, ProjectionMode.SCALED, false);
}
public static void unscaledProjection() {
apply(ProjectionMode.UNSCALED);
}
public static void logicalProjection() {
apply(ProjectionMode.LOGICAL);
}
public static void scaledProjection() {
apply(ProjectionMode.SCALED);
}
/**
* Apply an already captured 2D viewport/projection without causing Renderer2D flushes.
* Used by render-thread replay of 2D batches recorded during GUI extraction.
*/
public static void applyCaptured(ViewportContext next) {
if (next == null) return;
Matrix4f matrix = next.projectionMatrix != null ? new Matrix4f(next.projectionMatrix) : matrixFor(next.width, next.height);
RenderSystem.setProjectionMatrix(matrixBuffer().getBuffer(matrix), ProjectionType.ORTHOGRAPHIC);
MeshRenderer.setProjection(matrix);
activeUiScale = next.uiScale;
activeMode = next.projectionMode;
current = new ViewportContext(
next.framebufferWidth,
next.framebufferHeight,
next.scaleFactor,
next.width,
next.height,
next.uiScale,
next.projectionMode,
matrix
);
RenderState.rendering3D = false;
}
public static void projectionForSize(float width, float height) {
float safeWidth = Math.max(1.0f, width);
float safeHeight = Math.max(1.0f, height);
Minecraft mc = Minecraft.getInstance();
int fbw = mc != null && mc.getWindow() != null ? Math.max(1, mc.getWindow().getWidth()) : (int) safeWidth;
int fbh = mc != null && mc.getWindow() != null ? Math.max(1, mc.getWindow().getHeight()) : (int) safeHeight;
float scaleFactor = mc != null && mc.getWindow() != null ? Math.max(1.0f, (float) mc.getWindow().getGuiScale()) : 1.0f;
Matrix4f matrix = matrixFor(safeWidth, safeHeight);
ViewportContext next = new ViewportContext(fbw, fbh, scaleFactor, safeWidth, safeHeight, 1.0f, ProjectionMode.CUSTOM, new Matrix4f(matrix));
if (isActiveProjection(next, ProjectionMode.CUSTOM)) {
RenderSystem.setProjectionMatrix(matrixBuffer().getBuffer(matrix), ProjectionType.ORTHOGRAPHIC);
MeshRenderer.setProjection(matrix);
current = next;
RenderState.rendering3D = false;
return;
}
Renderer2D.flushBatch(Renderer2D.FlushReason.PROJECTION);
RenderSystem.setProjectionMatrix(matrixBuffer().getBuffer(matrix), ProjectionType.ORTHOGRAPHIC);
MeshRenderer.setProjection(matrix);
activeUiScale = 1.0f;
activeMode = ProjectionMode.CUSTOM;
current = next;
RenderState.rendering3D = false;
}
private static void begin(GuiGraphicsExtractor ctx, ProjectionMode mode) {
begin(ctx, mode, true);
}
private static void begin(GuiGraphicsExtractor ctx, ProjectionMode mode, boolean nextStratum) {
currentContext = ctx;
if (nextStratum && ctx != null) ctx.nextStratum();
apply(mode, true);
}
private static void apply(ProjectionMode mode) {
apply(mode, true);
}
private static void apply(ProjectionMode mode, boolean flush) {
ViewportContext next = capture(mode);
if (isActiveProjection(next, mode)) {
RenderSystem.setProjectionMatrix(matrixBuffer().getBuffer(next.projectionMatrix), ProjectionType.ORTHOGRAPHIC);
MeshRenderer.setProjection(next.projectionMatrix);
activeUiScale = next.uiScale;
activeMode = mode;
current = next;
RenderState.rendering3D = false;
return;
}
if (flush) Renderer2D.flushBatch(Renderer2D.FlushReason.PROJECTION);
RenderSystem.setProjectionMatrix(matrixBuffer().getBuffer(next.projectionMatrix), ProjectionType.ORTHOGRAPHIC);
MeshRenderer.setProjection(next.projectionMatrix);
activeUiScale = next.uiScale;
activeMode = mode;
current = next;
RenderState.rendering3D = false;
}
private static boolean isActiveProjection(ViewportContext next, ProjectionMode mode) {
ViewportContext previous = current;
return previous != null
&& activeMode == mode
&& previous.framebufferWidth == next.framebufferWidth
&& previous.framebufferHeight == next.framebufferHeight
&& Float.compare(previous.scaleFactor, next.scaleFactor) == 0
&& Float.compare(previous.width, next.width) == 0
&& Float.compare(previous.height, next.height) == 0
&& Float.compare(previous.uiScale, next.uiScale) == 0;
}
private static ViewportContext fallback(ProjectionMode mode) {
Matrix4f matrix = matrixFor(1.0f, 1.0f);
return new ViewportContext(1, 1, 1.0f, 1.0f, 1.0f, 1.0f, mode, matrix);
}
private static Matrix4f matrixFor(float width, float height) {
Projection projection = new Projection();
projection.setupOrtho(-1000.0f, 1000.0f, width, height, true);
return projection.getMatrix(new Matrix4f());
}
public enum ProjectionMode {
/**
* Framebuffer pixel coordinates.
*/
UNSCALED,
/**
* LoVisual logical HUD coordinates from HudScale.
*/
LOGICAL,
/**
* Vanilla scaled GUI coordinates.
*/
SCALED,
/**
* Explicit caller-provided projection size.
*/
CUSTOM
}
}

View file

@ -0,0 +1,18 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.frame;
/**
* Backend-visible lifecycle state for one Minecraft-presented frame.
*/
public enum FrameLifecycle {
IDLE,
RECORDING,
SUBMITTED,
PRESENTED
}

View file

@ -0,0 +1,80 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.frame;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.core.policy.DepthProvider;
import dev.loki.lovisual.render.engine.core.policy.fog.FogProvider;
import dev.loki.lovisual.render.engine.core.policy.LightPolicy;
import dev.loki.lovisual.render.engine.core.policy.VisibilityProvider;
import dev.loki.lovisual.render.sodium.bridge.SodiumFrameContext;
import dev.loki.lovisual.render.engine.core.context.CameraContext;
import dev.loki.lovisual.render.engine.core.context.FramebufferContext;
import dev.loki.lovisual.render.engine.core.context.ViewportContext;
import dev.loki.lovisual.render.engine.core.frame.RenderPhase;
/**
* Immutable per-phase context. Old RenderState should eventually become only a shim over this.
*/
public record RenderFrameContext(long frameId, float tickProgress, float frameDeltaTicks, float fixedDeltaTicks,
RenderPhase phase, CameraContext camera, ViewportContext viewport,
FramebufferContext framebuffer, DepthProvider depthProvider, FogProvider fogProvider,
LightPolicy lightPolicy, VisibilityProvider visibilityProvider,
SodiumFrameContext sodium) {
private static final float TICKS_PER_SECOND = 20.0f;
/**
* Legacy alias for render interpolation progress.
*/
public float tickDelta() {
return tickProgress;
}
/**
* Fractional progress inside the current game tick, used for world/entity interpolation.
*/
@Override
public float tickProgress() {
return tickProgress;
}
/**
* Time since previous rendered frame, in vanilla tick units.
*/
@Override
public float frameDeltaTicks() {
return frameDeltaTicks;
}
public float frameDeltaSeconds() {
return frameDeltaTicks / TICKS_PER_SECOND;
}
/**
* Vanilla fixed delta in tick units, clamped by RenderTickCounter.Dynamic.
*/
@Override
public float fixedDeltaTicks() {
return fixedDeltaTicks;
}
public RenderFrameContext withPhase(RenderPhase newPhase) {
return new RenderFrameContext(frameId, tickProgress, frameDeltaTicks, fixedDeltaTicks,
newPhase, camera, viewport, framebuffer,
depthProvider, fogProvider, lightPolicy, visibilityProvider, sodium);
}
public RenderFrameContext withTiming(float newTickProgress,
float newFrameDeltaTicks,
float newFixedDeltaTicks) {
return new RenderFrameContext(frameId, newTickProgress, newFrameDeltaTicks, newFixedDeltaTicks,
phase, camera, viewport, framebuffer,
depthProvider, fogProvider, lightPolicy, visibilityProvider, sodium);
}
}

View file

@ -0,0 +1,33 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.frame;
/**
* Explicit render phase used by the LoVisual frame graph.
* This is intentionally independent from Fabric/Minecraft event names: event hooks map into these phases.
*/
public enum RenderPhase {
NONE,
WORLD_BEFORE_ENTITIES,
WORLD_AFTER_ENTITIES,
WORLD_BEFORE_TRANSLUCENT,
WORLD_AFTER_TRANSLUCENT,
WORLD_POST_PRE_HAND,
WORLD_POST_HAND,
HUD_CAPTURE,
HUD_MAIN,
HUD_EFFECTS,
/**
* Dedicated topmost in-game screen layer. Used for LoVisual ClickGUI so the whole
* vanilla InGameHud is below it even on Minecraft versions where Screen rendering
* and HUD rendering interleave differently.
*/
SCREEN_TOP,
SCREEN,
LEGACY_SHADOW_TERRAIN
}

View file

@ -0,0 +1,48 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.frame;
import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext;
import dev.loki.lovisual.render.engine.core.frame.RenderPhase;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
/**
* AutoCloseable phase guard. Every render hook should use this instead of raw enterPhase().
* It restores the previous RenderFrameContext phase and closes profiler scopes even on exceptions.
*/
public final class RenderPhaseScope implements AutoCloseable {
private final RenderPhase phase;
private final RenderFrameContext previous;
private final AutoCloseable profilerScope;
private boolean closed;
public RenderPhaseScope(RenderPhase phase, RenderFrameContext previous, AutoCloseable profilerScope) {
this.phase = phase == null ? RenderPhase.NONE : phase;
this.previous = previous;
this.profilerScope = profilerScope;
}
public RenderPhase phase() {
return phase;
}
@Override
public void close() {
if (closed) return;
closed = true;
try {
if (profilerScope != null) {
profilerScope.close();
}
} catch (Exception ignored) {
// Profiling must never break render lifecycle recovery.
} finally {
LoVisualRenderSystem.restorePhase(previous);
}
}
}

View file

@ -0,0 +1,18 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.policy;
import com.mojang.blaze3d.textures.GpuTextureView;
import org.jetbrains.annotations.Nullable;
@FunctionalInterface
public interface DepthProvider {
DepthProvider NONE = () -> null;
@Nullable GpuTextureView depthAttachment();
}

View file

@ -0,0 +1,16 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.policy;
/**
* Placeholder for LoVisual lighting policy. Kept explicit so fullbright/ambient/shaderpack hooks do not leak into renderers.
*/
public interface LightPolicy {
LightPolicy VANILLA = new LightPolicy() {
};
}

View file

@ -0,0 +1,40 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.policy;
import net.minecraft.world.phys.AABB;
import dev.loki.lovisual.render.engine.core.policy.VisibilityQuery;
public interface VisibilityProvider {
VisibilityProvider ALWAYS_VISIBLE = new VisibilityProvider() {
@Override
public boolean isBoxVisible(AABB box) {
return true;
}
@Override
public boolean isBoxVisible(AABB box, VisibilityQuery query) {
return true;
}
@Override
public String debugName() {
return "always_visible";
}
};
boolean isBoxVisible(AABB box);
default boolean isBoxVisible(AABB box, VisibilityQuery query) {
return isBoxVisible(box);
}
default String debugName() {
return getClass().getSimpleName();
}
}

View file

@ -0,0 +1,30 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.policy;
import net.minecraft.world.phys.AABB;
/**
* Per-call visibility policy. Lets important/special overlays opt out of Sodium section culling.
*/
public record VisibilityQuery(AABB box,
boolean useSectionVisibility,
boolean alwaysVisible,
String reason) {
public static VisibilityQuery worldOverlay(AABB box) {
return new VisibilityQuery(box, true, false, "world_overlay");
}
public static VisibilityQuery alwaysVisible(AABB box, String reason) {
return new VisibilityQuery(box, false, true, reason != null ? reason : "always_visible");
}
public static VisibilityQuery noSectionVisibility(AABB box, String reason) {
return new VisibilityQuery(box, false, false, reason != null ? reason : "section_visibility_disabled");
}
}

View file

@ -0,0 +1,17 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.policy.fog;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import org.jetbrains.annotations.Nullable;
public interface FogProvider {
public FogProvider NONE = () -> null;
@Nullable GpuBufferSlice fogUniformSlice();
}

View file

@ -0,0 +1,33 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.policy.fog;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.fog.FogRenderer;
import org.jetbrains.annotations.Nullable;
import dev.loki.lovisual.mixins.accessors.renderer.GameRendererAccessor;
import dev.loki.lovisual.render.engine.core.policy.fog.FogProvider;
/**
* Vanilla world fog provider used by world pass compilation until Stage 8 pipeline metadata is global.
*/
public final class VanillaWorldFogProvider implements FogProvider {
public static final VanillaWorldFogProvider INSTANCE = new VanillaWorldFogProvider();
private VanillaWorldFogProvider() {
}
@Override
public @Nullable GpuBufferSlice fogUniformSlice() {
Minecraft mc = Minecraft.getInstance();
if (mc == null || !(mc.gameRenderer instanceof GameRendererAccessor accessor)) return null;
FogRenderer fogRenderer = accessor.lovisual$getFogRenderer();
return fogRenderer != null ? fogRenderer.getBuffer(FogRenderer.FogMode.WORLD) : null;
}
}

View file

@ -0,0 +1,374 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.system;
import com.mojang.blaze3d.systems.RenderSystem;
import dev.loki.lovisual.render.engine.compat.immediatelyfast.ImmediatelyFastRuntime;
import dev.loki.lovisual.render.engine.compat.immediatelyfast.ImmediatelyFastRuntimeSnapshot;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.render.iris.runtime.IrisRuntime;
import dev.loki.lovisual.render.iris.runtime.IrisRuntimeSnapshot;
import dev.loki.lovisual.render.sodium.interop.SodiumTerrainInteropStatsSnapshot;
import dev.loki.lovisual.render.sodium.visibility.SodiumVisibilityStatsSnapshot;
import org.joml.Matrix4f;
import dev.loki.lovisual.render.engine.RenderState;
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.profiler.frame.FrameStutterProfiler;
import dev.loki.lovisual.render.engine.profiler.frame.RenderFrameProfiler;
import dev.loki.lovisual.render.engine.rhi.resource.LoVisualRhi;
import dev.loki.lovisual.render.engine.rhi.stats.RhiStatsSnapshot;
import dev.loki.lovisual.render.engine.rhi.backend.SodiumGlBackend;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.LoVisualVulkanBackend;
import dev.loki.lovisual.render.engine.rhi.resource.manager.RenderResourceManager;
import dev.loki.lovisual.render.engine.rhi.resource.manager.RenderResourceStatsSnapshot;
import dev.loki.lovisual.render.engine.rhi.uniform.LoVisualUniformAllocator;
import dev.loki.lovisual.render.engine.rhi.uniform.stats.UniformAllocatorStatsSnapshot;
import dev.loki.lovisual.render.engine.text.command.TextCommandStatsSnapshot;
import dev.loki.lovisual.render.engine.text.render.TextRenderSystem;
import dev.loki.lovisual.render.engine.world.stats.WorldRenderStatsSnapshot;
import dev.loki.lovisual.render.sodium.bridge.SodiumFrameContext;
import dev.loki.lovisual.render.sodium.bridge.SodiumRenderBridge;
import dev.loki.lovisual.util.logging.DebugLog;
import java.util.Locale;
import dev.loki.lovisual.render.engine.core.context.CameraContext;
import dev.loki.lovisual.render.engine.core.context.FramebufferContext;
import dev.loki.lovisual.render.engine.core.context.ViewportContext;
import dev.loki.lovisual.render.engine.core.frame.FrameLifecycle;
import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext;
import dev.loki.lovisual.render.engine.core.frame.RenderPhase;
import dev.loki.lovisual.render.engine.core.frame.RenderPhaseScope;
/**
* Single owner of LoVisual render lifecycle.
* - one LoVisual frame per Minecraft-presented frame;
* - many scoped RenderPhase entries inside that frame;
* - backend submission is flushed before framePresented(), not by random renderer classes;
* - RenderState is a compatibility shim, not the lifecycle owner.
*/
public enum LoVisualRenderSystem {
;
private static final SodiumRenderBridge SODIUM = new SodiumRenderBridge();
private static final LoVisualFrameGraph FRAME_GRAPH = new LoVisualFrameGraph();
private static final LoVisualUniformAllocator UNIFORMS = new LoVisualUniformAllocator();
private static LoVisualRhi rhi;
private static BackendKind backendKind = BackendKind.UNKNOWN;
private static RenderFrameContext currentContext;
private static long frameId;
private static boolean initialized;
private static boolean frameOpen;
private static boolean submissionEnded;
private static FrameLifecycle lifecycle = FrameLifecycle.IDLE;
private enum BackendKind {
UNKNOWN,
GL,
VULKAN
}
public static void init() {
if (initialized) {
ensureBackendMatchesDevice();
return;
}
BackendKind desired = detectBackendKind();
backendKind = desired == BackendKind.UNKNOWN ? BackendKind.GL : desired;
rhi = createBackend(backendKind);
initialized = true;
DebugLog.renderThreadOnChange(
"lovisual.rhi.backend",
backendKind,
"[LoVisualRHI] backend initialized: %s",
backendKind
);
ensureBackendMatchesDevice();
}
private static LoVisualRhi createBackend(BackendKind kind) {
if (kind == BackendKind.VULKAN) {
return new LoVisualVulkanBackend();
}
return new SodiumGlBackend();
}
private static void ensureBackendMatchesDevice() {
if (!initialized || rhi == null) return;
BackendKind desired = detectBackendKind();
if (desired == BackendKind.UNKNOWN || desired == backendKind) return;
if (frameOpen) {
DebugLog.warnOnChange(
"lovisual.rhi.backend.switch.deferred",
backendKind + "->" + desired + "|frameOpen",
"[LoVisualRHI] backend switch deferred until frame end: %s -> %s",
backendKind,
desired
);
return;
}
LoVisualRhi previous = rhi;
BackendKind previousKind = backendKind;
LoVisualRhi next;
try {
next = createBackend(desired);
} catch (Throwable t) {
DebugLog.error("[LoVisualRHI] failed to create backend " + desired, t);
return;
}
rhi = next;
backendKind = desired;
try {
previous.close();
} catch (Throwable t) {
DebugLog.warnOnChange(
"lovisual.rhi.backend.old.close.failed",
previousKind + "|" + desired + "|" + t.getClass().getSimpleName(),
"[LoVisualRHI] old backend close failed during switch %s -> %s: %s: %s",
previousKind,
desired,
t.getClass().getSimpleName(),
t.getMessage()
);
}
DebugLog.renderThreadOnChange(
"lovisual.rhi.backend",
backendKind,
"[LoVisualRHI] backend switched: %s -> %s",
previousKind,
backendKind
);
}
private static BackendKind detectBackendKind() {
String backendName = null;
try {
var device = RenderSystem.tryGetDevice();
if (device != null && device.getDeviceInfo() != null) {
backendName = device.getDeviceInfo().backendName();
}
} catch (Throwable ignored) {
// Fall through to RenderSystem backend description.
}
if (backendName == null || backendName.isBlank()) {
try {
backendName = RenderSystem.getBackendDescription();
} catch (Throwable ignored) {
backendName = null;
}
}
if (backendName == null || backendName.isBlank()) return BackendKind.UNKNOWN;
String normalized = backendName.toLowerCase(Locale.ROOT);
if (normalized.contains("vulkan")) return BackendKind.VULKAN;
if (normalized.contains("opengl") || normalized.contains("gl")) return BackendKind.GL;
return BackendKind.UNKNOWN;
}
public static LoVisualRhi rhi() {
init();
ensureBackendMatchesDevice();
return rhi;
}
public static SodiumRenderBridge sodium() {
return SODIUM;
}
public static RenderResourceManager resources() {
return rhi().resources();
}
public static LoVisualFrameGraph frameGraph() {
return FRAME_GRAPH;
}
public static LoVisualUniformAllocator uniforms() {
return UNIFORMS;
}
public static RenderFrameContext currentContext() {
return currentContext;
}
public static FrameLifecycle lifecycle() {
return lifecycle;
}
/**
* Opens the LoVisual frame if needed, otherwise refreshes camera/viewport data without advancing frameId.
*/
public static RenderFrameContext beginFrame(float tickDelta, Matrix4f projection, Matrix4f modelView) {
return beginFrame(tickDelta, tickDelta, tickDelta, projection, modelView);
}
public static RenderFrameContext beginFrame(float tickProgress,
float frameDeltaTicks,
float fixedDeltaTicks,
Matrix4f projection,
Matrix4f modelView) {
LoVisualRhi activeRhi = rhi();
SodiumFrameContext sodiumFrame;
if (!frameOpen) {
frameId++;
activeRhi.beginFrame(frameId);
UNIFORMS.beginFrame(frameId);
TextRenderSystem.beginFrame();
sodiumFrame = SODIUM.beginFrame();
RenderFrameProfiler.beginFrame(frameId);
frameOpen = true;
submissionEnded = false;
lifecycle = FrameLifecycle.RECORDING;
} else {
sodiumFrame = SODIUM.currentFrameContext();
}
RenderPhase phase = currentContext != null ? currentContext.phase() : RenderPhase.NONE;
RenderFrameContext ctx = new RenderFrameContext(
frameId,
tickProgress,
frameDeltaTicks,
fixedDeltaTicks,
phase,
CameraContext.capture(projection, modelView),
ViewportContext.capture(),
FramebufferContext.capture(),
WorldSceneDepth::mainDepthView,
VanillaWorldFogProvider.INSTANCE,
LightPolicy.VANILLA,
SODIUM.visibilityProvider(),
sodiumFrame
);
currentContext = ctx;
RenderState.applyContext(ctx);
return ctx;
}
/**
* Refreshes only timing data for phases that do not own the world camera/projection
* themselves, for example Fabric HUD callbacks.
*/
public static RenderFrameContext updateFrameTiming(float tickProgress,
float frameDeltaTicks,
float fixedDeltaTicks) {
RenderFrameContext base = ensureFrameContext();
currentContext = base.withTiming(tickProgress, frameDeltaTicks, fixedDeltaTicks);
RenderState.applyContext(currentContext);
return currentContext;
}
public static RenderFrameContext ensureFrameContext() {
if (currentContext != null) return currentContext;
return beginFrame(RenderState.tickProgress, RenderState.frameDeltaTicks, RenderState.fixedDeltaTicks, new Matrix4f(), new Matrix4f(RenderSystem.getModelViewStack()));
}
public static RenderPhaseScope phase(RenderPhase phase) {
return phase(phase, null);
}
public static RenderPhaseScope phase(RenderPhase phase, String label) {
RenderFrameContext before = currentContext;
RenderFrameContext base = ensureFrameContext();
RenderPhase next = phase == null ? RenderPhase.NONE : phase;
currentContext = base.withPhase(next);
RenderState.applyContext(currentContext);
return new RenderPhaseScope(next, before, RenderFrameProfiler.phase(next, label));
}
public static void restorePhase(RenderFrameContext previous) {
currentContext = previous;
if (currentContext != null) {
RenderState.applyContext(currentContext);
} else {
RenderState.clearContextBackedState();
}
}
public static void endRenderSubmission() {
if (!initialized || !frameOpen || submissionEnded) return;
try {
TextRenderSystem.flush();
rhi.endRenderSubmission();
lifecycle = FrameLifecycle.SUBMITTED;
} finally {
submissionEnded = true;
}
}
/**
* Called after the window surface has been presented. Rendering submission must already
* be closed before GpuSurface.blitFromTexture(), otherwise late LoVisual draws miss the
* presented frame and can leave stale main-target contents for the next frame.
*/
public static void onFramePresented() {
if (!initialized) return;
try {
if (!submissionEnded) {
// Fallback for unusual renderFrame paths. Normal visible frames close in
// GpuSurfaceMixin#lovisual$beforeSurfaceBlit, before the surface copy.
endRenderSubmission();
}
rhi.framePresented();
UNIFORMS.onFramePresented();
if (FrameStutterProfiler.isEnabled()) {
FrameStutterProfiler.onFramePresented(rhiStatsSnapshot(), uniformStatsSnapshot(), resourceStatsSnapshot());
}
RenderFrameProfiler.endFrame(null, null);
lifecycle = FrameLifecycle.PRESENTED;
} finally {
currentContext = null;
frameOpen = false;
submissionEnded = false;
lifecycle = FrameLifecycle.IDLE;
RenderState.clearContextBackedState();
}
}
public static RhiStatsSnapshot rhiStatsSnapshot() {
return rhi().stats().snapshot();
}
public static SodiumVisibilityStatsSnapshot sodiumVisibilityStatsSnapshot() {
return SODIUM.visibilityStatsSnapshot();
}
public static SodiumTerrainInteropStatsSnapshot sodiumTerrainStatsSnapshot() {
return SODIUM.terrainInterop().statsSnapshot();
}
public static IrisRuntimeSnapshot irisSnapshot() {
return IrisRuntime.snapshot();
}
public static ImmediatelyFastRuntimeSnapshot immediatelyFastSnapshot() {
return ImmediatelyFastRuntime.snapshot();
}
public static UniformAllocatorStatsSnapshot uniformStatsSnapshot() {
return UNIFORMS.statsSnapshot();
}
public static RenderResourceStatsSnapshot resourceStatsSnapshot() {
return resources().statsSnapshot();
}
public static WorldRenderStatsSnapshot worldRenderStatsSnapshot() {
return Renderer3D.worldStatsSnapshot();
}
public static TextCommandStatsSnapshot textStatsSnapshot() {
return TextRenderSystem.statsSnapshot();
}
}

View file

@ -0,0 +1,73 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.core.system;
import org.joml.Matrix4f;
import org.joml.Matrix4fc;
import net.minecraft.world.phys.Vec3;
public enum LoVisualWorldMatrices {
;
private static final Matrix4f POSITION = new Matrix4f();
private static final Matrix4f RENDER_PROJECTION = new Matrix4f();
private static final Matrix4f FRUSTUM_PROJECTION = new Matrix4f();
private static Vec3 CAMERA_POS;
private static boolean valid;
public static void reset() {
valid = false;
}
public static void capture(Matrix4fc positionMatrix, Matrix4fc renderProjectionMatrix, Matrix4fc frustumProjectionMatrix) {
capture(positionMatrix, renderProjectionMatrix, frustumProjectionMatrix, null);
}
public static void capture(Matrix4fc positionMatrix, Matrix4fc renderProjectionMatrix, Matrix4fc frustumProjectionMatrix, Vec3 cameraPos) {
if (positionMatrix == null || renderProjectionMatrix == null || frustumProjectionMatrix == null) {
valid = false;
CAMERA_POS = null;
return;
}
POSITION.set(positionMatrix);
RENDER_PROJECTION.set(renderProjectionMatrix);
FRUSTUM_PROJECTION.set(frustumProjectionMatrix);
CAMERA_POS = cameraPos;
valid = true;
}
public static void capturePosition(Matrix4fc positionMatrix, Vec3 cameraPos) {
if (!valid || positionMatrix == null) {
return;
}
POSITION.set(positionMatrix);
if (cameraPos != null) {
CAMERA_POS = cameraPos;
}
}
public static boolean isValid() {
return valid;
}
public static Matrix4f positionMatrix() {
return valid ? new Matrix4f(POSITION) : null;
}
public static Matrix4f renderProjectionMatrix() {
return valid ? new Matrix4f(RENDER_PROJECTION) : null;
}
public static Matrix4f frustumProjectionMatrix() {
return valid ? new Matrix4f(FRUSTUM_PROJECTION) : null;
}
public static Vec3 cameraPosition() {
return valid ? CAMERA_POS : null;
}
}

View file

@ -0,0 +1,336 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.debug;
import com.mojang.blaze3d.ProjectionType;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.renderer.Projection;
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
import org.joml.Matrix4f;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
import dev.loki.lovisual.render.engine.core.frame.RenderPhase;
import dev.loki.lovisual.render.engine.core.frame.RenderPhaseScope;
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.util.logging.DebugLog;
/**
* Render-thread 2D diagnostics.
*
* <p>There are two independent probes:</p>
* <ul>
* <li>GUI-state probe: writes vanilla {@link GuiGraphicsExtractor#fill} rectangles during HUD extraction.</li>
* <li>Immediate probe: draws with {@link Renderer2D} after vanilla {@code GuiRenderer.render()}.</li>
* </ul>
*
* <p>This deliberately does not move production HUD rendering. It only tells which side is broken:
* vanilla GUI/root-layer extraction or LoVisual immediate Renderer2D/RHI.</p>
*/
public enum RenderThread2DDebugRenderer {
;
private static ProjectionMatrixBuffer debugProjection;
private static final long LOG_INTERVAL_NS = 1_000_000_000L;
private static volatile long immediateFrame;
private static volatile long guiStateFrame;
private static volatile long lastImmediateNs;
private static volatile long lastGuiStateNs;
private static volatile int lastFramebufferWidth;
private static volatile int lastFramebufferHeight;
private static volatile int lastGuiWidth;
private static volatile int lastGuiHeight;
private static volatile int lastImmediateDraws;
private static volatile int lastImmediateBatches;
private static volatile int lastImmediateVertices;
private static volatile int lastImmediateIndices;
private static volatile int lastGuiStateRects;
private static volatile String lastImmediateError = "";
private static volatile String lastGuiStateError = "";
private static long lastImmediateLogNs;
private static long lastGuiStateLogNs;
private static boolean immediateFailureLogged;
private static boolean guiStateFailureLogged;
private static ProjectionMatrixBuffer debugProjection() {
ProjectionMatrixBuffer buffer = debugProjection;
if (buffer == null) {
buffer = new ProjectionMatrixBuffer("lovisual-debug-2d-render-thread-projection");
debugProjection = buffer;
}
return buffer;
}
public static void extractGuiStateProbe(GuiGraphicsExtractor ctx) {
if (!DebugLog.isRenderThreadDebugEnabled()) return;
if (ctx == null) {
recordGuiStateFailure("GuiGraphicsExtractor null");
return;
}
try {
int gw = Math.max(1, ctx.guiWidth());
int gh = Math.max(1, ctx.guiHeight());
int rects = drawGuiStateProbe(ctx, gw, gh);
guiStateFrame++;
lastGuiStateNs = System.nanoTime();
lastGuiWidth = gw;
lastGuiHeight = gh;
lastGuiStateRects = rects;
lastGuiStateError = "";
guiStateFailureLogged = false;
logGuiStateIfDue(gw, gh, rects);
} catch (Throwable t) {
recordGuiStateFailure(t.getClass().getSimpleName() + ": " + String.valueOf(t.getMessage()));
}
}
public static void renderImmediateAfterGui(DeltaTracker tickCounter) {
if (!DebugLog.isRenderThreadDebugEnabled()) return;
try {
RenderSystem.assertOnRenderThread();
} catch (Throwable t) {
recordImmediateFailure("not on render thread: " + t.getClass().getSimpleName());
return;
}
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.gameRenderer == null) {
recordImmediateFailure("minecraft/gameRenderer null");
return;
}
if (Renderer2D.COLOR == null) {
recordImmediateFailure("Renderer2D.COLOR null");
return;
}
RenderTarget target = mc.gameRenderer.mainRenderTarget();
if (target == null) {
recordImmediateFailure("main render target null");
return;
}
int fbw = Math.max(1, target.width);
int fbh = Math.max(1, target.height);
GpuBufferSlice previousProjection = RenderSystem.getProjectionMatrixBuffer();
ProjectionType previousProjectionType = RenderSystem.getProjectionType();
Matrix4f previousMeshProjection = MeshRenderer.projection();
boolean previousRendering3D = RenderState.rendering3D;
var modelView = RenderSystem.getModelViewStack();
boolean pushedModelView = false;
try (RenderPhaseScope ignored = LoVisualRenderSystem.phase(RenderPhase.HUD_MAIN, "debug:2d_immediate_after_gui")) {
modelView.pushMatrix();
pushedModelView = true;
modelView.identity();
Matrix4f projection = framebufferOrtho(fbw, fbh);
RenderSystem.setProjectionMatrix(debugProjection().getBuffer(projection), ProjectionType.ORTHOGRAPHIC);
MeshRenderer.setProjection(projection);
RenderState.rendering3D = false;
Renderer2D.BatchStats stats = Renderer2D.getBatchStats();
int beforeFrameDraws = stats.getFrameDraws();
int beforeFrameBatches = stats.getFrameBatches();
int beforeFrameVertices = stats.getFrameVertices();
int beforeFrameIndices = stats.getFrameIndices();
drawImmediateProbe(fbw, fbh);
immediateFrame++;
lastImmediateNs = System.nanoTime();
lastFramebufferWidth = fbw;
lastFramebufferHeight = fbh;
lastImmediateDraws = Math.max(0, stats.getFrameDraws() - beforeFrameDraws);
lastImmediateBatches = Math.max(0, stats.getFrameBatches() - beforeFrameBatches);
lastImmediateVertices = Math.max(0, stats.getFrameVertices() - beforeFrameVertices);
lastImmediateIndices = Math.max(0, stats.getFrameIndices() - beforeFrameIndices);
lastImmediateError = "";
immediateFailureLogged = false;
logImmediateIfDue(fbw, fbh, stats);
} catch (Throwable t) {
recordImmediateFailure(t.getClass().getSimpleName() + ": " + String.valueOf(t.getMessage()));
} finally {
if (pushedModelView) {
modelView.popMatrix();
}
MeshRenderer.setProjection(previousMeshProjection);
RenderState.rendering3D = previousRendering3D;
if (previousProjection != null && previousProjectionType != null) {
RenderSystem.setProjectionMatrix(previousProjection, previousProjectionType);
}
}
}
private static Matrix4f framebufferOrtho(int width, int height) {
Projection projection = new Projection();
projection.setupOrtho(-1000.0f, 1000.0f, Math.max(1.0f, width), Math.max(1.0f, height), true);
return projection.getMatrix(new Matrix4f());
}
private static int drawGuiStateProbe(GuiGraphicsExtractor ctx, int gw, int gh) {
int rects = 0;
ctx.nextStratum();
// Vanilla GUI/root-layer probe. It uses scaled GUI coordinates and vanilla GUI render states.
ctx.fill(2, 2, 16, 16, 0xFFFF3344);
rects++;
ctx.fill(Math.max(0, gw - 16), 2, gw - 2, 16, 0xFF44FF66);
rects++;
ctx.fill(2, Math.max(0, gh - 16), 16, gh - 2, 0xFF4488FF);
rects++;
ctx.fill(Math.max(0, gw - 16), Math.max(0, gh - 16), gw - 2, gh - 2, 0xFFFFFF44);
rects++;
ctx.fill(8, 24, Math.min(gw - 8, 190), Math.min(gh - 8, 52), 0xCC111119);
rects++;
ctx.fill(12, 28, Math.min(gw - 12, 44), Math.min(gh - 12, 48), 0xFFFF3344);
rects++;
ctx.fill(48, 28, Math.min(gw - 12, 80), Math.min(gh - 12, 48), 0xFF44FF66);
rects++;
ctx.fill(84, 28, Math.min(gw - 12, 116), Math.min(gh - 12, 48), 0xFF4488FF);
rects++;
return rects;
}
private static void drawImmediateProbe(int fbw, int fbh) {
Renderer2D renderer = Renderer2D.COLOR;
double panelX = 8.0;
double panelY = 72.0;
double panelW = 292.0;
double panelH = 92.0;
double pulse = (immediateFrame % 120L) / 119.0;
double markerX = panelX + 110.0 + pulse * 150.0;
renderer.begin();
// Framebuffer-space immediate Renderer2D/RHI probe. No text/glyph dependency.
renderer.quad(0, 0, 12, 12, 0xFFFF3355);
renderer.quad(fbw - 12.0, 0, 12, 12, 0xFF55FF66);
renderer.quad(0, fbh - 12.0, 12, 12, 0xFF5599FF);
renderer.quad(fbw - 12.0, fbh - 12.0, 12, 12, 0xFFFFFF55);
renderer.quad(panelX, panelY, panelW, panelH, 0xCC050509);
renderer.boxLines(panelX, panelY, panelW, panelH, 0xFFFF33FF);
renderer.line(panelX, panelY, panelX + panelW, panelY + panelH, 0x80FFFFFF);
renderer.line(panelX + panelW, panelY, panelX, panelY + panelH, 0x80FFFFFF);
renderer.quad(panelX + 14.0, panelY + 14.0, 24.0, 24.0, 0xFFFF3355);
renderer.quad(panelX + 44.0, panelY + 14.0, 24.0, 24.0, 0xFF55FF66);
renderer.quad(panelX + 74.0, panelY + 14.0, 24.0, 24.0, 0xFF5599FF);
renderer.quad(panelX + 14.0, panelY + 50.0, 244.0, 16.0, 0x66333333);
renderer.boxLines(panelX + 14.0, panelY + 50.0, 244.0, 16.0, 0x99FFFFFF);
renderer.quad(markerX, panelY + 51.5, 28.0, 13.0, 0xFFFFFF55);
renderer.boxLines(18.0, 18.0, Math.max(1.0, fbw - 36.0), Math.max(1.0, fbh - 36.0), 0x55FFFFFF);
renderer.render();
}
private static void logGuiStateIfDue(int gw, int gh, int rects) {
long now = System.nanoTime();
if (now - lastGuiStateLogNs < LOG_INTERVAL_NS) return;
lastGuiStateLogNs = now;
DebugLog.renderThread(
"2D debug gui_state frame=%d gui=%dx%d rects=%d err=%s",
guiStateFrame,
gw,
gh,
rects,
lastGuiStateError == null || lastGuiStateError.isBlank() ? "none" : lastGuiStateError
);
}
private static void logImmediateIfDue(int fbw, int fbh, Renderer2D.BatchStats stats) {
long now = System.nanoTime();
if (now - lastImmediateLogNs < LOG_INTERVAL_NS) return;
lastImmediateLogNs = now;
DebugLog.renderThread(
"2D debug immediate after_gui frame=%d fb=%dx%d drawDelta=%d/%d v=%d i=%d lastBatch=%d/%d reason=%s err=%s",
immediateFrame,
fbw,
fbh,
lastImmediateDraws,
lastImmediateBatches,
lastImmediateVertices,
lastImmediateIndices,
stats.getLastDraws(),
stats.getLastOrder(),
stats.getLastFlushReason(),
stats.getLastError() == null || stats.getLastError().isBlank() ? "none" : stats.getLastError()
);
}
private static void recordImmediateFailure(String reason) {
String safeReason = reason == null || reason.isBlank() ? "unknown" : reason;
lastImmediateError = safeReason;
if (!immediateFailureLogged) {
immediateFailureLogged = true;
DebugLog.renderThread("2D debug immediate skipped/failed: %s", safeReason);
}
}
private static void recordGuiStateFailure(String reason) {
String safeReason = reason == null || reason.isBlank() ? "unknown" : reason;
lastGuiStateError = safeReason;
if (!guiStateFailureLogged) {
guiStateFailureLogged = true;
DebugLog.renderThread("2D debug gui_state skipped/failed: %s", safeReason);
}
}
public static Snapshot snapshot() {
return new Snapshot(
immediateFrame,
guiStateFrame,
lastImmediateNs,
lastGuiStateNs,
lastFramebufferWidth,
lastFramebufferHeight,
lastGuiWidth,
lastGuiHeight,
lastImmediateDraws,
lastImmediateBatches,
lastImmediateVertices,
lastImmediateIndices,
lastGuiStateRects,
lastImmediateError,
lastGuiStateError
);
}
public record Snapshot(long immediateFrame,
long guiStateFrame,
long lastImmediateNs,
long lastGuiStateNs,
int framebufferWidth,
int framebufferHeight,
int guiWidth,
int guiHeight,
int immediateDraws,
int immediateBatches,
int immediateVertices,
int immediateIndices,
int guiStateRects,
String immediateError,
String guiStateError) {
}
}

View file

@ -0,0 +1,234 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.depth;
import com.mojang.blaze3d.GpuFormat;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.pipeline.TextureTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.GpuTexture;
import com.mojang.blaze3d.textures.GpuTextureView;
import net.minecraft.client.Minecraft;
import dev.loki.lovisual.mixininterface.device.IMsaaTexture;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
import dev.loki.lovisual.render.engine.msaa.MsaaWorldTarget;
/**
* Captures the main framebuffer depth BEFORE translucent layers.
* Used to render custom geometry above liquids while still depth-testing vs solids.
*/
public enum PreTranslucentDepth {
;
private static TextureTarget depthCopy;
private static GpuTextureView msaaDepthView;
private static GpuTexture msaaDepthCopy;
private static GpuTextureView msaaDepthCopyView;
private static int msaaSamples;
private static int depthCopyW = -1;
private static int depthCopyH = -1;
private static int msaaDepthCopyW = -1;
private static int msaaDepthCopyH = -1;
private static boolean captured;
private static boolean capturedMsaa;
private static boolean requestedNextFrame;
/**
* The full-screen depth copy in {@link #captureFrom} is expensive, and its capture point
* (before the translucent groups in the main pass) runs BEFORE any PRE_DEPTH consumer renders
* in the frame. Consumers ({@link #getDepthView()}, {@link #getDepthViewFor(GpuTextureView)})
* therefore declare demand here; the next frame's capture consults it via {@link #takeRequested()}.
* One-frame latency is invisible: until the copy exists, getters fall back to the live
* framebuffer/WorldSceneDepth views.
*/
public static void requestForNextFrame() {
requestedNextFrame = true;
}
/** @return and clears the demand flag set by {@link #requestForNextFrame()}. */
public static boolean takeRequested() {
boolean requested = requestedNextFrame;
requestedNextFrame = false;
return requested;
}
public static void captureFrom(RenderTarget source) {
if (source == null) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null) return;
int w = source.width;
int h = source.height;
if (w <= 0 || h <= 0) return;
captured = false;
capturedMsaa = false;
boolean isMsaa = source.getDepthTexture() instanceof IMsaaTexture msaa && msaa.lovisual$isMsaa();
if (isMsaa) {
int samples = ((IMsaaTexture) source.getDepthTexture()).lovisual$getSamples();
ensureMsaaDepthCopy(w, h, samples);
if (msaaDepthCopy != null && source.getDepthTexture() != null) {
try {
RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(
source.getDepthTexture(),
msaaDepthCopy,
0, 0, 0,
0, 0,
w, h
);
msaaDepthView = msaaDepthCopyView;
capturedMsaa = true;
if (LoVisualRenderSystem.rhi().msaa().eagerPreTranslucentDepthResolve()) {
captured = resolveMsaaDepthCopy();
}
} catch (IllegalArgumentException ignored) {
// A resize can race this capture point on some setups: framebuffer.textureHeight
// may already report the new window size while the backing MSAA depth texture
// is still the previous one. Do not crash the render thread; skip this frame and
// force a fresh allocation on the next capture.
releaseMsaaDepthCopy();
capturedMsaa = false;
msaaDepthView = null;
}
}
} else {
msaaDepthView = null;
capturedMsaa = false;
depthCopy = LoVisualRenderSystem.resources().persistentFramebuffer(
"lovisual-pretrans-depth", w, h, true, "PreTranslucentDepth");
depthCopyW = w;
depthCopyH = h;
depthCopy.copyDepthFrom(source);
captured = true;
}
}
public static void capture() {
Minecraft mc = Minecraft.getInstance();
if (mc == null) return;
RenderTarget fb = mc.gameRenderer.mainRenderTarget();
if (fb == null) return;
captureFrom(fb);
}
public static GpuTextureView getDepthView() {
requestForNextFrame();
if (MsaaWorldTarget.isActive() && capturedMsaa && msaaDepthView != null) {
return msaaDepthView;
}
if (captured && depthCopy != null) {
return depthCopy.getDepthTextureView();
}
if (capturedMsaa && msaaDepthView != null && resolveMsaaDepthCopy()) {
return depthCopy.getDepthTextureView();
}
if (capturedMsaa
&& msaaDepthView != null
&& !LoVisualRenderSystem.rhi().msaa().supportsDepthSnapshotResolve()) {
return msaaDepthView;
}
return WorldSceneDepth.mainDepthView();
}
public static GpuTextureView getDepthViewFor(GpuTextureView colorAttachment) {
requestForNextFrame();
int requiredSamples = samples(colorAttachment);
if (requiredSamples > 1
&& capturedMsaa
&& msaaDepthView != null
&& samples(msaaDepthView) == requiredSamples) {
return msaaDepthView;
}
if (requiredSamples == 1 && captured && depthCopy != null) {
return depthCopy.getDepthTextureView();
}
if (requiredSamples == 1 && capturedMsaa && msaaDepthView != null && resolveMsaaDepthCopy()) {
return depthCopy.getDepthTextureView();
}
GpuTextureView fallback = WorldSceneDepth.mainDepthView();
return samples(fallback) == requiredSamples ? fallback : null;
}
public static void reset() {
captured = false;
capturedMsaa = false;
msaaDepthView = null;
}
private static void ensureMsaaDepthCopy(int w, int h, int samples) {
if (!LoVisualRenderSystem.rhi().msaa().supported()) return;
if (msaaDepthCopy == null || w != msaaDepthCopyW || h != msaaDepthCopyH || msaaSamples != samples) {
releaseMsaaDepthCopy();
int usage = GpuTexture.USAGE_COPY_DST | GpuTexture.USAGE_COPY_SRC | GpuTexture.USAGE_RENDER_ATTACHMENT;
msaaDepthCopy = LoVisualRenderSystem.rhi().msaa().createTexture(
"lovisual-pretrans-depth-msaa",
usage,
GpuFormat.D32_FLOAT,
w,
h,
samples
);
msaaDepthCopyView = RenderSystem.getDevice().createTextureView(msaaDepthCopy);
msaaSamples = samples;
msaaDepthCopyW = w;
msaaDepthCopyH = h;
}
}
private static void ensureResolvedDepthCopy(int w, int h) {
depthCopy = LoVisualRenderSystem.resources().persistentFramebuffer(
"lovisual-pretrans-depth",
w,
h,
true,
"PreTranslucentDepth"
);
depthCopyW = w;
depthCopyH = h;
}
private static boolean resolveMsaaDepthCopy() {
if (captured) return true;
if (!capturedMsaa || msaaDepthCopyView == null || msaaDepthCopyW <= 0 || msaaDepthCopyH <= 0) {
return false;
}
if (!LoVisualRenderSystem.rhi().msaa().supportsDepthSnapshotResolve()) {
return false;
}
ensureResolvedDepthCopy(msaaDepthCopyW, msaaDepthCopyH);
captured = depthCopy != null
&& depthCopy.getDepthTextureView() != null
&& LoVisualRenderSystem.rhi().msaa().resolveDepthSnapshot(
msaaDepthCopyView,
depthCopy.getDepthTextureView()
);
return captured;
}
private static int samples(GpuTextureView view) {
if (view == null || view.texture() == null) return 1;
return view.texture() instanceof IMsaaTexture msaa && msaa.lovisual$isMsaa()
? msaa.lovisual$getSamples()
: 1;
}
private static void releaseMsaaDepthCopy() {
if (msaaDepthCopyView != null) {
msaaDepthCopyView.close();
msaaDepthCopyView = null;
}
if (msaaDepthCopy != null) {
LoVisualRenderSystem.resources().retireTexture(msaaDepthCopy);
msaaDepthCopy = null;
}
msaaDepthView = null;
msaaSamples = 0;
msaaDepthCopyW = -1;
msaaDepthCopyH = -1;
}
}

View file

@ -0,0 +1,224 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.depth;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.pipeline.TextureTarget;
import com.mojang.blaze3d.textures.GpuTextureView;
import dev.loki.lovisual.mixininterface.device.IMsaaTexture;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
/**
* Persistent copy of the depth buffers that vanilla composes through its world transparency
* frame graph. The copy is taken by a vanilla FramePass after transparency composition has
* been scheduled, so DoF samples the same scene-stage depth that contributed to final world
* color instead of relying on a single late main depth attachment.
*/
public enum WorldSceneDepth {
;
private static final Stage MAIN = new Stage("main", "lovisual-world-scene-depth-main");
private static final Stage TRANSLUCENT = new Stage("translucent", "lovisual-world-scene-depth-translucent");
private static final Stage ITEM_ENTITY = new Stage("item_entity", "lovisual-world-scene-depth-item-entity");
private static final Stage PARTICLES = new Stage("particles", "lovisual-world-scene-depth-particles");
private static final Stage WEATHER = new Stage("weather", "lovisual-world-scene-depth-weather");
private static final Stage CLOUDS = new Stage("clouds", "lovisual-world-scene-depth-clouds");
private static int width = -1;
private static int height = -1;
private static long frameSerial;
private static boolean valid;
public static void beginFrame(int w, int h) {
width = Math.max(1, w);
height = Math.max(1, h);
frameSerial++;
valid = false;
MAIN.reset();
TRANSLUCENT.reset();
ITEM_ENTITY.reset();
PARTICLES.reset();
WEATHER.reset();
CLOUDS.reset();
}
public static void captureMain(RenderTarget source) {
valid |= MAIN.capture(source, width, height);
}
/** Captures the final PRE_HAND main depth, resolving an MSAA source when necessary. */
public static boolean captureResolvedMain(RenderTarget source) {
if (source == null) {
return false;
}
int w = Math.max(1, source.width);
int h = Math.max(1, source.height);
if (width != w || height != h) {
width = w;
height = h;
}
boolean captured = MAIN.capture(source, width, height);
valid |= captured;
return captured;
}
public static void captureTranslucent(RenderTarget source) {
valid |= TRANSLUCENT.capture(source, width, height);
}
public static void captureItemEntity(RenderTarget source) {
valid |= ITEM_ENTITY.capture(source, width, height);
}
public static void captureParticles(RenderTarget source) {
valid |= PARTICLES.capture(source, width, height);
}
public static void captureWeather(RenderTarget source) {
valid |= WEATHER.capture(source, width, height);
}
public static void captureClouds(RenderTarget source) {
valid |= CLOUDS.capture(source, width, height);
}
public static boolean isValid() {
return valid && (hasMain() || hasTranslucent() || hasItemEntity() || hasParticles() || hasWeather() || hasClouds());
}
public static long frameSerial() {
return frameSerial;
}
public static int width() {
return width;
}
public static int height() {
return height;
}
public static boolean hasMain() {
return MAIN.hasDepth();
}
public static boolean hasTranslucent() {
return TRANSLUCENT.hasDepth();
}
public static boolean hasItemEntity() {
return ITEM_ENTITY.hasDepth();
}
public static boolean hasParticles() {
return PARTICLES.hasDepth();
}
public static boolean hasWeather() {
return WEATHER.hasDepth();
}
public static boolean hasClouds() {
return CLOUDS.hasDepth();
}
public static GpuTextureView mainDepthView() {
return MAIN.depthView();
}
public static GpuTextureView translucentDepthView() {
return TRANSLUCENT.depthView();
}
public static GpuTextureView itemEntityDepthView() {
return ITEM_ENTITY.depthView();
}
public static GpuTextureView particlesDepthView() {
return PARTICLES.depthView();
}
public static GpuTextureView weatherDepthView() {
return WEATHER.depthView();
}
public static GpuTextureView cloudsDepthView() {
return CLOUDS.depthView();
}
public static void reset() {
valid = false;
MAIN.reset();
TRANSLUCENT.reset();
ITEM_ENTITY.reset();
PARTICLES.reset();
WEATHER.reset();
CLOUDS.reset();
}
private static final class Stage {
private final String stageName;
private final String framebufferName;
private TextureTarget depthCopy;
private boolean captured;
private Stage(String stageName, String framebufferName) {
this.stageName = stageName;
this.framebufferName = framebufferName;
}
private boolean capture(RenderTarget source, int w, int h) {
captured = false;
if (source == null || source.getDepthTexture() == null || w <= 0 || h <= 0) {
return false;
}
try {
depthCopy = LoVisualRenderSystem.resources().persistentFramebuffer(
framebufferName,
w,
h,
true,
"WorldSceneDepth:" + stageName
);
if (depthCopy == null || depthCopy.getDepthTexture() == null) {
return false;
}
boolean msaa = source.getDepthTexture() instanceof IMsaaTexture texture && texture.lovisual$isMsaa();
if (msaa) {
if (LoVisualRenderSystem.rhi().msaa().supportsDepthSnapshotResolve()) {
captured = LoVisualRenderSystem.rhi().msaa().resolveDepthSnapshot(
source.getDepthTextureView(),
depthCopy.getDepthTextureView()
);
} else {
captured = LoVisualRenderSystem.rhi().msaa().resolve(source, depthCopy, false, true);
}
} else {
depthCopy.copyDepthFrom(source);
captured = true;
}
} catch (Throwable ignored) {
captured = false;
}
return captured;
}
private boolean hasDepth() {
return captured && depthCopy != null && depthCopy.getDepthTextureView() != null;
}
private GpuTextureView depthView() {
return hasDepth() ? depthCopy.getDepthTextureView() : null;
}
private void reset() {
captured = false;
}
}
}

View file

@ -0,0 +1,46 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.framegraph;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext;
import dev.loki.lovisual.render.engine.core.frame.RenderPhase;
import dev.loki.lovisual.render.engine.core.frame.RenderPhaseScope;
import java.util.function.Consumer;
public final class LoVisualFrameGraph {
private final PassScheduler scheduler = new PassScheduler();
public void add(RenderPhase phase, Consumer<RenderFrameContext> handler) {
add(phase, phase == null ? "unnamed" : phase.name().toLowerCase(), handler);
}
public void add(RenderPhase phase, String label, Consumer<RenderFrameContext> handler) {
if (handler == null) return;
scheduler.add(new RenderPassNode(phase, label, handler));
}
public void execute(RenderPhase phase, RenderFrameContext baseContext) {
String label = "framegraph:" + (phase == null ? "none" : phase.name().toLowerCase());
try (RenderPhaseScope ignored = LoVisualRenderSystem.phase(phase, label)) {
RenderFrameContext ctx = LoVisualRenderSystem.currentContext();
if (ctx == null && baseContext != null) ctx = baseContext.withPhase(phase);
if (ctx == null) return;
for (RenderPassNode node : scheduler.nodes(phase)) {
try (RenderPhaseScope nodeScope = LoVisualRenderSystem.phase(phase, "pass:" + node.label())) {
node.execute(LoVisualRenderSystem.currentContext());
}
}
}
}
public void clear() {
scheduler.clear();
}
}

View file

@ -0,0 +1,33 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.framegraph;
import dev.loki.lovisual.render.engine.core.frame.RenderPhase;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
public final class PassScheduler {
private final EnumMap<RenderPhase, List<RenderPassNode>> nodes = new EnumMap<>(RenderPhase.class);
public void add(RenderPassNode node) {
if (node == null) return;
nodes.computeIfAbsent(node.phase(), ignored -> new ArrayList<>()).add(node);
}
public List<RenderPassNode> nodes(RenderPhase phase) {
List<RenderPassNode> list = nodes.get(phase);
if (list == null || list.isEmpty()) return List.of();
return List.copyOf(list);
}
public void clear() {
nodes.clear();
}
}

View file

@ -0,0 +1,41 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.framegraph;
import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext;
import dev.loki.lovisual.render.engine.core.frame.RenderPhase;
import java.util.Objects;
import java.util.function.Consumer;
/**
* A named frame-graph node bound to a LoVisual render phase.
*/
public final class RenderPassNode {
private final RenderPhase phase;
private final String label;
private final Consumer<RenderFrameContext> renderer;
public RenderPassNode(RenderPhase phase, String label, Consumer<RenderFrameContext> renderer) {
this.phase = phase == null ? RenderPhase.NONE : phase;
this.label = label == null || label.isBlank() ? this.phase.name().toLowerCase() : label;
this.renderer = Objects.requireNonNull(renderer, "renderer");
}
public RenderPhase phase() {
return phase;
}
public String label() {
return label;
}
public void execute(RenderFrameContext context) {
renderer.accept(context);
}
}

View file

@ -0,0 +1,32 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.guard;
import dev.loki.lovisual.render.engine.rhi.backend.SodiumGlBackend;
/**
* Old rendering paths that are no longer allowed as production behavior after the RHI migration.
*/
public enum LegacyRenderPath {
IMMEDIATE_MESH_UPLOAD("MeshBuilder immediate GPU upload"),
LEGACY_FULLSCREEN_MESH("FullScreenRenderer.mesh compatibility path"),
LEGACY_MESH_RENDERER("MeshRenderer legacy facade"),
DIRECT_GL_OUTSIDE_BACKEND("direct GL call outside SodiumGlBackend boundary"),
DIRECT_SODIUM_OUTSIDE_BRIDGE("direct Sodium call outside SodiumRenderBridge/backend boundary"),
PIPELINE_IDENTITY_POLICY("pipeline policy inferred from RenderPipeline object identity"),
LOCAL_FRAMEBUFFER_POOL("subsystem-local framebuffer/texture pool");
private final String description;
LegacyRenderPath(String description) {
this.description = description;
}
public String description() {
return description;
}
}

View file

@ -0,0 +1,62 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.guard;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
import dev.loki.lovisual.render.engine.rhi.backend.SodiumGlBackend;
/**
* Final architecture lock for the LoVisual RHI migration.
* <p>
* This class is intentionally small and dependency-light: it is used from low-level legacy facades to make sure old
* render paths cannot silently become production paths again. A path can only be enabled through an explicit system
* property, and every use is counted in RHI stats.
*/
public enum RenderArchitectureGuard {
;
public static final boolean DEBUG = Boolean.getBoolean("lovisual.render.debug");
public static final boolean STRICT = !Boolean.getBoolean("lovisual.render.allowLegacyPaths");
public static final boolean ALLOW_LEGACY_MESH_RENDERER = Boolean.getBoolean("lovisual.render.allowLegacyMeshRenderer");
public static final boolean ALLOW_LEGACY_FULLSCREEN_MESH = Boolean.getBoolean("lovisual.render.allowLegacyFullscreenMesh");
public static final boolean ALLOW_IMMEDIATE_UPLOAD = Boolean.getBoolean("lovisual.rhi.allowImmediateFallback")
|| Boolean.getBoolean("lovisual.render.allowImmediateUpload");
public static void requireAllowed(LegacyRenderPath path, String owner) {
boolean allowed = switch (path) {
case IMMEDIATE_MESH_UPLOAD -> ALLOW_IMMEDIATE_UPLOAD;
case LEGACY_FULLSCREEN_MESH -> ALLOW_LEGACY_FULLSCREEN_MESH;
case LEGACY_MESH_RENDERER -> ALLOW_LEGACY_MESH_RENDERER;
default -> !STRICT;
};
record(path);
if (allowed) return;
throw new IllegalStateException("Legacy render path is locked: " + path.description()
+ " owner=" + owner
+ ". Use LoVisualRHI/SodiumGlBackend path instead. "
+ "Temporary override: -D" + overrideProperty(path) + "=true");
}
public static void record(LegacyRenderPath path) {
try {
LoVisualRenderSystem.rhi().stats().legacyPath(path);
} catch (Throwable ignored) {
// Guard must be safe during bootstrap/static initialization.
}
}
private static String overrideProperty(LegacyRenderPath path) {
return switch (path) {
case IMMEDIATE_MESH_UPLOAD -> "lovisual.rhi.allowImmediateFallback";
case LEGACY_FULLSCREEN_MESH -> "lovisual.render.allowLegacyFullscreenMesh";
case LEGACY_MESH_RENDERER -> "lovisual.render.allowLegacyMeshRenderer";
default -> "lovisual.render.allowLegacyPaths";
};
}
}

View file

@ -0,0 +1,36 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.guard.boundary;
/**
* Central list of architecture boundary package rules.
* <p>
* This is used by debug audits and as documentation in code. The rules are deliberately string-based so they do not
* introduce hard dependencies from high-level renderer code to GL/Sodium classes.
*/
public enum RenderBoundary {
;
public static final String RHI_BACKEND_PACKAGE = "dev.loki.lovisual.render.engine.rhi";
public static final String SODIUM_GL_BACKEND_PACKAGE = "dev.loki.lovisual.render.engine.rhi";
public static final String SODIUM_BRIDGE_PACKAGE = "dev.loki.lovisual.render.sodium";
public static final String PROFILER_PACKAGE = "dev.loki.lovisual.render.engine.profiler";
public static final String LEGACY_MSAA_PACKAGE = "dev.loki.lovisual.render.engine.msaa";
public static boolean mayUseGl(String className) {
return className.startsWith(SODIUM_GL_BACKEND_PACKAGE)
|| className.startsWith(PROFILER_PACKAGE)
|| className.startsWith(LEGACY_MSAA_PACKAGE);
}
public static boolean mayUseSodiumInternals(String className) {
return className.startsWith(SODIUM_BRIDGE_PACKAGE)
|| className.startsWith("dev.loki.lovisual.render.sodium.terrain")
|| className.startsWith("dev.loki.lovisual.render.engine.visuals.lighting")
|| className.startsWith("dev.loki.lovisual.render.helpers");
}
}

View file

@ -0,0 +1,43 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.guard.boundary;
import dev.loki.lovisual.render.engine.guard.RenderArchitectureGuard;
import dev.loki.lovisual.util.logging.DebugLog;
import java.util.List;
import dev.loki.lovisual.render.engine.rhi.pipeline.pipeline.RenderPipelineSpec;
/**
* Lightweight runtime boundary audit. It does not scan source files; it validates explicitly registered legacy
* exceptions and prints the architectural rules once during bootstrap in debug mode.
*/
public enum RenderBoundaryAudit {
;
private static final List<String> LOCKED_RULES = List.of(
"Renderer3D/Renderer2D/Text/PostProcess must submit through LoVisualRHI or command layers",
"Direct GL is backend/profiler/MSAA-legacy only",
"Direct Sodium runtime access is SodiumRenderBridge/terrain interop only",
"Immediate mesh upload is emergency fallback only",
"Individual uniform flipFrame calls are forbidden",
"Fullscreen MeshBuilder quad is forbidden as production path",
"Pipeline policy must come from RenderPipelineSpec metadata"
);
private static boolean ran;
public static void runOnce() {
if (ran) return;
ran = true;
if (!RenderArchitectureGuard.DEBUG) return;
DebugLog.renderThread("[LoVisualRHI] render architecture boundary locked:");
for (String rule : LOCKED_RULES) {
DebugLog.renderThread("[LoVisualRHI] - " + rule);
}
}
}

View file

@ -0,0 +1,29 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.guard.boundary;
import dev.loki.lovisual.render.engine.guard.LegacyRenderPath;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a deliberate exception to the post-RHI boundary rules.
* <p>
* Every usage should be temporary or isolated. New renderer features should not add this annotation unless the code is
* a backend/profiler/interop implementation.
*/
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface RenderBoundaryExempt {
public LegacyRenderPath value();
public String reason();
}

View file

@ -0,0 +1,36 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.light;
import net.minecraft.util.Mth;
public enum LightmapState {
;
private static volatile float baseAmbient = 0.0f;
private static volatile float ambient = 0.0f;
public static void setAmbient(float baseValue, float value) {
baseAmbient = Mth.clamp(baseValue, 0.0f, 1.0f);
ambient = Mth.clamp(value, 0.0f, 1.0f);
}
public static float getAmbient() {
return ambient;
}
public static float getBaseAmbient() {
return baseAmbient;
}
/**
* Returns override ambient if raised above base, otherwise -1.
*/
public static float getOverrideAmbient() {
return ambient > baseAmbient + 1.0e-4f ? ambient : -1.0f;
}
}

View file

@ -0,0 +1,59 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.math;
import net.minecraft.util.Mth;
public enum ColorMath {
;
public static int colorWithAlpha(int rgb, int alpha) {
int a = clamp255(alpha);
return (a << 24) | (rgb & 0x00FFFFFF);
}
public static int mixRgb(int rgbA, int rgbB, float t) {
float clamped = Mth.clamp(t, 0f, 1f);
int ar = (rgbA >>> 16) & 0xFF;
int ag = (rgbA >>> 8) & 0xFF;
int ab = rgbA & 0xFF;
int br = (rgbB >>> 16) & 0xFF;
int bg = (rgbB >>> 8) & 0xFF;
int bb = rgbB & 0xFF;
int r = Math.round(ar + (br - ar) * clamped);
int g = Math.round(ag + (bg - ag) * clamped);
int b = Math.round(ab + (bb - ab) * clamped);
return (clamp255(r) << 16) | (clamp255(g) << 8) | clamp255(b);
}
public static int scaleAlpha(int argb, float alpha) {
int a = (argb >>> 24) & 0xFF;
int na = Math.round(a * Mth.clamp(alpha, 0f, 1f));
return (argb & 0x00FFFFFF) | ((na & 0xFF) << 24);
}
public static int premultiplyAlpha(int rgb, int alpha) {
int a = clamp255(alpha);
int r = (rgb >>> 16) & 0xFF;
int g = (rgb >>> 8) & 0xFF;
int b = rgb & 0xFF;
r = (r * a + 127) / 255;
g = (g * a + 127) / 255;
b = (b * a + 127) / 255;
return (a << 24) | (r << 16) | (g << 8) | b;
}
public static int premultiplyArgb(int argb) {
return premultiplyAlpha(argb & 0x00FFFFFF, (argb >>> 24) & 0xFF);
}
private static int clamp255(int v) {
return Math.min(255, Math.max(0, v));
}
}

View file

@ -0,0 +1,64 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.math;
public enum HudScale {
;
public static final float BASE_WIDTH = 1920f;
public static final float BASE_HEIGHT = 1080f;
public static final float FIXED_HUD_FONT_SIZE = 20f;
public static float scale(int screenW, int screenH) {
if (screenW <= 0 || screenH <= 0) return 1f;
float sx = screenW / BASE_WIDTH;
float sy = screenH / BASE_HEIGHT;
return Math.min(sx, sy);
}
/**
* Logical (virtual) width in UI units for given framebuffer size.
*/
public static float virtualWidth(int screenW, int screenH) {
float scale = scale(screenW, screenH);
if (scale <= 0f) return screenW;
return screenW / scale;
}
/**
* Logical (virtual) height in UI units for given framebuffer size.
*/
public static float virtualHeight(int screenW, int screenH) {
float scale = scale(screenW, screenH);
if (scale <= 0f) return screenH;
return screenH / scale;
}
/**
* Convert framebuffer-space value to logical UI units.
*/
public static float toVirtual(float value, float scale) {
if (scale <= 0f) return value;
return value / scale;
}
/**
* Convert logical UI units to framebuffer-space value.
*/
public static float toFramebuffer(float value, float scale) {
if (scale <= 0f) return value;
return value * scale;
}
public static float s(float value, float scale) {
return value * scale;
}
public static float hudFontSize() {
return FIXED_HUD_FONT_SIZE;
}
}

View file

@ -0,0 +1,40 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.math;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import dev.loki.lovisual.render.engine.RenderState;
public enum RenderMath {
;
public static double lerp(double a, double b, double t) {
return a + (b - a) * t;
}
public static float lerp(float a, float b, float t) {
return a + (b - a) * t;
}
public static Vec3 getLerpedPos(Entity e, float tickDelta) {
return e.getPosition(tickDelta);
}
public static Vec3 getLerpedPosRelative(Entity e, float tickDelta) {
Vec3 p = getLerpedPos(e, tickDelta);
return p.subtract(RenderState.cameraPos);
}
public static float smoothLerp(float current, float target, float dt, float speed) {
if (current < 0.0f) return target;
float t = 1.0f - (float) Math.exp(-speed * Math.max(0.0f, dt));
return Mth.lerp(t, current, target);
}
}

View file

@ -0,0 +1,83 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.msaa;
import com.mojang.blaze3d.GpuFormat;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.GpuTexture;
import dev.loki.lovisual.mixininterface.device.IMsaaTexture;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
import lombok.Getter;
@Getter
public final class MsaaFramebuffer extends RenderTarget {
private int samples;
public MsaaFramebuffer(String name, int width, int height, boolean useDepthAttachment, int samples) {
super(name, useDepthAttachment, GpuFormat.RGBA8_UNORM);
this.samples = samples;
RenderSystem.assertOnRenderThread();
this.resize(width, height);
}
private static int textureSamples(GpuTexture texture) {
return texture instanceof IMsaaTexture msaa ? msaa.lovisual$getSamples() : 1;
}
@Override
public void createBuffers(int width, int height) {
RenderSystem.assertOnRenderThread();
if (!LoVisualRenderSystem.rhi().msaa().supported()) {
throw new IllegalStateException("MSAA device not available");
}
this.width = width;
this.height = height;
int usage = GpuTexture.USAGE_COPY_DST
| GpuTexture.USAGE_COPY_SRC
| GpuTexture.USAGE_TEXTURE_BINDING
| GpuTexture.USAGE_RENDER_ATTACHMENT;
try {
if (this.useDepth) {
this.depthTexture = LoVisualRenderSystem.rhi().msaa().createTexture(
this.label + " / Depth",
usage,
GpuFormat.D32_FLOAT,
width,
height,
samples
);
this.depthTextureView = RenderSystem.getDevice().createTextureView(this.depthTexture);
}
this.colorTexture = LoVisualRenderSystem.rhi().msaa().createTexture(
this.label + " / Color",
usage,
GpuFormat.RGBA8_UNORM,
width,
height,
samples
);
this.colorTextureView = RenderSystem.getDevice().createTextureView(this.colorTexture);
int colorSamples = textureSamples(this.colorTexture);
int depthSamples = this.depthTexture != null ? textureSamples(this.depthTexture) : colorSamples;
if (colorSamples <= 1 || depthSamples <= 1 || colorSamples != depthSamples) {
throw new IllegalStateException("Incomplete MSAA target sample layout: color="
+ colorSamples + ", depth=" + depthSamples + ", requested=" + samples);
}
this.samples = colorSamples;
} catch (RuntimeException | Error t) {
this.destroyBuffers();
throw t;
}
}
}

View file

@ -0,0 +1,22 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.msaa;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
@Deprecated
public enum MsaaResolveUtil {
;
public static boolean resolve(RenderTarget src, RenderTarget dst, boolean color, boolean depth) {
RenderSystem.assertOnRenderThread();
return LoVisualRenderSystem.rhi().msaa().resolve(src, dst, color, depth);
}
}

View file

@ -0,0 +1,34 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.msaa;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
@Deprecated
public enum MsaaTextureRegistry {
;
private static final Int2IntOpenHashMap SAMPLES = new Int2IntOpenHashMap();
public static void register(int glId, int samples) {
if (glId <= 0 || samples <= 1) return;
SAMPLES.put(glId, samples);
}
public static void unregister(int glId) {
if (glId <= 0) return;
SAMPLES.remove(glId);
}
public static boolean isMsaa(int glId) {
return glId > 0 && SAMPLES.containsKey(glId);
}
public static int getSamples(int glId) {
return glId > 0 ? SAMPLES.getOrDefault(glId, 1) : 1;
}
}

View file

@ -0,0 +1,267 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.msaa;
import com.mojang.blaze3d.pipeline.RenderTarget;
import net.minecraft.client.Minecraft;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
import dev.loki.lovisual.mixins.accessors.renderer.GameRendererAccessor;
import dev.loki.lovisual.util.logging.DebugLog;
public enum MsaaWorldTarget {
;
private static MsaaFramebuffer msaa;
private static int samples;
private static int allocationRequestSamples;
private static int width;
private static int height;
private static boolean active;
private static int requestedSamples;
private static boolean supported;
private static boolean lastResolveOk;
private static boolean resolveDepth;
private static String state = "never";
public static void begin(Minecraft mc, int sampleCount) {
begin(mc, sampleCount, true);
}
public static void begin(Minecraft mc, int sampleCount, boolean needDepthResolve) {
begin(mc, sampleCount, needDepthResolve, false);
}
public static void begin(Minecraft mc,
int sampleCount,
boolean needDepthResolve,
boolean preserveMainColor) {
requestedSamples = sampleCount;
resolveDepth = needDepthResolve;
if (mc == null) {
state = "no_client";
active = false;
return;
}
if (sampleCount <= 1) {
state = "off";
supported = false;
if (active || msaa != null) {
shutdown(mc);
}
return;
}
supported = LoVisualRenderSystem.rhi().msaa().supported();
if (!supported) {
state = "unsupported";
shutdown(mc);
return;
}
int w = mc.getWindow().getWidth();
int h = mc.getWindow().getHeight();
if (w <= 0 || h <= 0) {
state = "bad_size";
shutdown(mc);
return;
}
boolean reusableTarget = msaa != null
&& allocationRequestSamples == sampleCount
&& width == w
&& height == h;
ensureBuffer(w, h, sampleCount);
if (msaa == null) {
state = "alloc_failed";
shutdown(mc);
return;
}
RenderTarget main = rawMain(mc);
if (main == null) {
state = "no_main";
shutdown(mc);
return;
}
if (preserveMainColor) {
if (!reusableTarget) {
state = "seed_unavailable";
active = false;
return;
}
boolean seeded = LoVisualRenderSystem.rhi().msaa().resolve(main, msaa, true, false);
if (!seeded) {
lastResolveOk = false;
state = "seed_failed";
active = false;
DebugLog.warnOnChange(
"msaa.world.seed_failed",
sampleCount + "|" + w + "x" + h,
"[MSAA] could not preserve post-processed main color for the late world pass; rendering that pass without MSAA. requested=%d size=%dx%d",
sampleCount,
w,
h
);
return;
}
}
try {
LoVisualRenderSystem.rhi().msaa().prepareTarget(msaa, main, true, needDepthResolve);
} catch (Throwable t) {
LoVisualRenderSystem.rhi().msaa().abandonTarget(msaa);
lastResolveOk = false;
state = "prepare_failed";
DebugLog.warnOnChange("msaa.world.prepare_failed",
sampleCount + "|" + t.getClass().getSimpleName() + "|" + t.getMessage(),
"[MSAA] world target preparation failed; disabling target. requested=%d size=%dx%d error=%s: %s",
sampleCount, w, h, t.getClass().getSimpleName(), t.getMessage());
shutdown(mc);
return;
}
state = "active";
active = true;
}
public static void resolveToMain(Minecraft mc) {
if (mc == null) return;
if (!active || msaa == null) {
return;
}
RenderTarget main = rawMain(mc);
if (main == null) {
LoVisualRenderSystem.rhi().msaa().abandonTarget(msaa);
active = false;
lastResolveOk = false;
state = "resolve_no_main";
return;
}
lastResolveOk = LoVisualRenderSystem.rhi().msaa().resolve(msaa, main, true, resolveDepth);
active = false;
}
public static void shutdown(Minecraft mc) {
if (!active && msaa == null) {
return;
}
if (active) {
resolveToMain(mc);
}
if (msaa != null) {
MsaaFramebuffer retired = msaa;
LoVisualRenderSystem.rhi().msaa().abandonTarget(retired);
LoVisualRenderSystem.resources().retire(retired::destroyBuffers);
msaa = null;
}
active = false;
samples = 0;
allocationRequestSamples = 0;
width = 0;
height = 0;
}
private static void ensureBuffer(int w, int h, int sampleCount) {
if (msaa == null || allocationRequestSamples != sampleCount) {
MsaaFramebuffer previous = msaa;
try {
MsaaFramebuffer next = new MsaaFramebuffer("lovisual-msaa-world", w, h, true, sampleCount);
msaa = next;
samples = next.getSamples();
allocationRequestSamples = sampleCount;
width = w;
height = h;
if (previous != null) {
LoVisualRenderSystem.rhi().msaa().abandonTarget(previous);
LoVisualRenderSystem.resources().retire(previous::destroyBuffers);
}
} catch (Throwable t) {
if (previous != null) {
LoVisualRenderSystem.rhi().msaa().abandonTarget(previous);
LoVisualRenderSystem.resources().retire(previous::destroyBuffers);
}
msaa = null;
samples = 0;
allocationRequestSamples = 0;
width = 0;
height = 0;
state = "alloc_failed";
DebugLog.warnOnChange("msaa.world.alloc_failed", sampleCount + "|" + t.getClass().getSimpleName() + "|" + t.getMessage(),
"[MSAA] world MSAA allocation failed; disabling target. requested=%d size=%dx%d error=%s: %s",
sampleCount, w, h, t.getClass().getSimpleName(), t.getMessage());
}
return;
}
if (w != width || h != height) {
try {
msaa.resize(w, h);
samples = msaa.getSamples();
width = w;
height = h;
} catch (Throwable t) {
MsaaFramebuffer retired = msaa;
LoVisualRenderSystem.rhi().msaa().abandonTarget(retired);
LoVisualRenderSystem.resources().retire(retired::destroyBuffers);
msaa = null;
samples = 0;
allocationRequestSamples = 0;
width = 0;
height = 0;
state = "resize_failed";
DebugLog.warnOnChange("msaa.world.resize_failed", sampleCount + "|" + t.getClass().getSimpleName() + "|" + t.getMessage(),
"[MSAA] world MSAA resize failed; disabling target. requested=%d size=%dx%d error=%s: %s",
sampleCount, w, h, t.getClass().getSimpleName(), t.getMessage());
}
}
}
public static boolean isActive() {
return active && msaa != null;
}
private static RenderTarget rawMain(Minecraft mc) {
return mc != null && mc.gameRenderer != null
? ((GameRendererAccessor) mc.gameRenderer).lovisual$getMainRenderTargetRaw()
: null;
}
public static RenderTarget getFramebufferOverride() {
return active ? msaa : null;
}
public static MsaaFramebuffer getMsaaFramebuffer() {
return msaa;
}
public static int getSamples() {
return msaa != null ? msaa.getSamples() : 0;
}
public static DebugStatus debugStatus() {
return new DebugStatus(
requestedSamples,
samples,
width,
height,
supported,
active,
msaa != null,
lastResolveOk,
state
);
}
public record DebugStatus(int requestedSamples,
int targetSamples,
int width,
int height,
boolean supported,
boolean active,
boolean allocated,
boolean lastResolveOk,
String state) {
}
}

View file

@ -0,0 +1,26 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.pipeline;
import com.mojang.blaze3d.platform.CompareOp;
public enum DepthTestFunction {
NO_DEPTH_TEST(null),
LEQUAL_DEPTH_TEST(CompareOp.LESS_THAN_OR_EQUAL),
GEQUAL_DEPTH_TEST(CompareOp.GREATER_THAN_OR_EQUAL);
private final CompareOp compareOp;
DepthTestFunction(CompareOp compareOp) {
this.compareOp = compareOp;
}
public CompareOp compareOp() {
return compareOp;
}
}

View file

@ -0,0 +1,164 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.pipeline;
import com.mojang.blaze3d.GpuFormat;
import com.mojang.blaze3d.pipeline.*;
import com.mojang.blaze3d.shaders.UniformType;
import com.mojang.blaze3d.vertex.VertexFormat;
import dev.loki.lovisual.mixininterface.render.IRenderPipeline;
import dev.loki.lovisual.render.engine.rhi.clip.ShapeClipRenderPassContract;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Small delegating builder so we can tag RenderPipeline with additional features.
*/
public class ExtendedRenderPipelineBuilder {
private final RenderPipeline.Builder delegate;
private final List<String> samplers = new ArrayList<>();
private final List<UniformSpec> uniforms = new ArrayList<>();
private boolean lineSmooth;
private ShapeClipRenderPassContract shapeClipContract = ShapeClipRenderPassContract.NONE;
private DepthTestFunction depthTestFunction;
private net.minecraft.resources.Identifier location;
private boolean depthWrite = true;
private boolean customDepthState;
private boolean bindGroupLayoutApplied;
public ExtendedRenderPipelineBuilder(RenderPipeline.Snippet... snippets) {
this.delegate = RenderPipeline.builder(snippets);
}
public ExtendedRenderPipelineBuilder withLocation(net.minecraft.resources.Identifier id) {
this.location = id;
delegate.withLocation(id);
return this;
}
public ExtendedRenderPipelineBuilder withVertexFormat(VertexFormat format, com.mojang.blaze3d.PrimitiveTopology mode) {
delegate.withVertexBinding(0, format);
delegate.withPrimitiveTopology(mode);
return this;
}
public ExtendedRenderPipelineBuilder withVertexShader(net.minecraft.resources.Identifier id) {
delegate.withVertexShader(id);
return this;
}
public ExtendedRenderPipelineBuilder withFragmentShader(net.minecraft.resources.Identifier id) {
delegate.withFragmentShader(id);
return this;
}
public ExtendedRenderPipelineBuilder withDepthTestFunction(DepthTestFunction fn) {
this.depthTestFunction = fn;
this.customDepthState = true;
applyDepthStencilState();
return this;
}
public ExtendedRenderPipelineBuilder withDepthWrite(boolean write) {
this.depthWrite = write;
this.customDepthState = true;
applyDepthStencilState();
return this;
}
public ExtendedRenderPipelineBuilder withBlend(BlendFunction blend) {
delegate.withColorTargetState(new ColorTargetState(Optional.of(blend), GpuFormat.RGBA8_UNORM, ColorTargetState.WRITE_ALL));
return this;
}
public ExtendedRenderPipelineBuilder withCull(boolean cull) {
delegate.withCull(isUiCanvasPipeline() ? false : cull);
return this;
}
public ExtendedRenderPipelineBuilder withSampler(String name) {
samplers.add(name);
return this;
}
public ExtendedRenderPipelineBuilder withUniform(String name, UniformType type) {
uniforms.add(new UniformSpec(name, type));
return this;
}
public ExtendedRenderPipelineBuilder withLineSmooth() {
this.lineSmooth = true;
return this;
}
/**
* Declares that this pipeline participates in shape clipping. The backend decides how the
* attachment is represented. GL uses stencil; Vulkan must create/route the equivalent
* render-pass/mask attachment explicitly.
*/
public ExtendedRenderPipelineBuilder withShapeClipSupport() {
this.shapeClipContract = ShapeClipRenderPassContract.WHEN_ACTIVE;
return this;
}
public ExtendedRenderPipelineBuilder withShapeClipContract(ShapeClipRenderPassContract contract) {
this.shapeClipContract = contract == null ? ShapeClipRenderPassContract.NONE : contract;
return this;
}
public RenderPipeline build() {
applyBindGroupLayout();
RenderPipeline pipeline = delegate.build();
IRenderPipeline lovisualPipeline = (IRenderPipeline) pipeline;
lovisualPipeline.lovisual$setLineSmooth(lineSmooth);
lovisualPipeline.lovisual$setShapeClipContract(shapeClipContract);
return pipeline;
}
public RenderPipeline.Snippet buildSnippet() {
applyBindGroupLayout();
return delegate.buildSnippet();
}
private boolean isUiCanvasPipeline() {
if (location == null || !"lovisual".equals(location.getNamespace())) return false;
String path = location.getPath();
return path.startsWith("pipeline/ui_")
|| path.startsWith("pipeline/menu_")
|| path.equals("pipeline/gui_texture_lookup");
}
private void applyDepthStencilState() {
if (!customDepthState) return;
if (depthTestFunction == null || depthTestFunction.compareOp() == null) {
delegate.withDepthStencilState(Optional.empty());
return;
}
delegate.withDepthStencilState(new DepthStencilState(depthTestFunction.compareOp(), depthWrite));
}
private void applyBindGroupLayout() {
if (bindGroupLayoutApplied || samplers.isEmpty() && uniforms.isEmpty()) {
return;
}
BindGroupLayout.Builder layout = BindGroupLayout.builder();
for (String sampler : samplers) {
layout.withSampler(sampler);
}
for (UniformSpec uniform : uniforms) {
layout.withUniform(uniform.name(), uniform.type());
}
delegate.withBindGroupLayout(layout.build());
bindGroupLayoutApplied = true;
}
private record UniformSpec(String name, UniformType type) {
}
}

View file

@ -0,0 +1,439 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.pipeline;
import com.mojang.blaze3d.PrimitiveTopology;
import com.mojang.blaze3d.pipeline.BlendFunction;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.systems.GpuDevice;
import com.mojang.blaze3d.systems.RenderSystem;
import dev.loki.lovisual.mixininterface.render.IRenderPipeline;
import dev.loki.lovisual.render.engine.pipeline.pipelines.PipelineTemplates;
import dev.loki.lovisual.render.engine.pipeline.pipelines.ScreenPipelines;
import dev.loki.lovisual.render.engine.pipeline.pipelines.UiPipelines;
import dev.loki.lovisual.render.engine.pipeline.pipelines.WorldPipelines;
import dev.loki.lovisual.render.engine.rhi.clip.ShapeClipRenderPassContract;
import dev.loki.lovisual.render.engine.rhi.pipeline.layout.RenderPipelineRegistry;
import dev.loki.lovisual.render.engine.shader.LoVisualShaderSources;
import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats;
import dev.loki.lovisual.util.logging.DebugLog;
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.KillEffect;
import dev.loki.lovisual.features.module.modules.visuals.esp.ESP;
import dev.loki.lovisual.features.module.modules.visuals.fx.MotionBlur;
import dev.loki.lovisual.features.module.modules.visuals.fx.PostFX;
import dev.loki.lovisual.features.module.modules.visuals.scene.BlockHighlight;
import dev.loki.lovisual.features.module.modules.visuals.weather.sky.ShaderSky;
import net.minecraft.resources.Identifier;
import net.minecraft.server.packs.resources.ResourceManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Pipeline registry for LoVisual's custom rendering engine.
*
* <p>Public API facade: every shader identifier and pipeline constant keeps its original
* name and value. The repetitive builder chains live in the template classes of
* {@link dev.loki.lovisual.render.engine.pipeline.pipelines}.</p>
*
* <p>Keep this class free of module-specific logic: it is the foundation for all custom 3D/2D effects.</p>
*/
public enum LoVisualRenderPipelines {
;
// ======================
// Shaders (assets/lovisual/shaders/*)
// ======================
// Core colored / line / fog shaders
public static final Identifier SHADER_POS_COLOR_VERT = shader("pos_color.vert");
public static final Identifier SHADER_WIDE_LINE_VERT = shader("wide_line.vert");
public static final Identifier SHADER_POS_COLOR_FRAG = shader("pos_color.frag");
public static final Identifier SHADER_POS_COLOR_FOG_VERT = shader("pos_color_fog.vert");
public static final Identifier SHADER_POS_COLOR_FOG_FRAG = shader("pos_color_fog.frag");
// Rect-parameter vertex shaders (batched shapes)
public static final Identifier SHADER_POS_COLOR_RECT_PARAMS_VERT = shader("pos_color_rect_params.vert");
public static final Identifier SHADER_POS_COLOR_RECT_PARAMS2_VERT = shader("pos_color_rect_params2.vert");
public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT = shader("pos_local_color_rect_params.vert");
public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT = shader("pos_local_color_rect_params2.vert");
public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT = shader("pos_local_color_rect_params5.vert");
public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_GENERIC_VERT = shader("pos_local_color_rect_params5_generic.vert");
// Textured primitives & GUI lookup
public static final Identifier SHADER_POS_TEX_COLOR_VERT = shader("pos_tex_color.vert");
public static final Identifier SHADER_POS_TEX_COLOR_PARAMS2_VERT = shader("pos_tex_color_params2.vert");
public static final Identifier SHADER_POS_TEX_COLOR_FRAG = shader("pos_tex_color.frag");
public static final Identifier SHADER_GUI_TEXTURE_LOOKUP_FRAG = shader("gui_texture_lookup.frag");
public static final Identifier SHADER_POS_TEX_COLOR_TINT_FRAG = shader("pos_tex_color_tint.frag");
public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_VERT = shader("pos_tex_color_sky_fog.vert");
public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_FRAG = shader("pos_tex_color_sky_fog.frag");
public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_ADDITIVE_FRAG = shader("pos_tex_color_sky_fog_additive.frag");
// Skyboxes & shader skies
public static final Identifier SHADER_POS_COLOR_SKYBOX_SHADER_VERT = shader("pos_color_skybox_shader.vert");
public static final Identifier SHADER_REIMAGINED_SKYBOX_FRAG = shader("reimagined_skybox.frag");
public static final Identifier SHADER_SHADER_SKY_THUNDER_FRAG = shader("shader_sky_thunder.frag");
public static final Identifier SHADER_SHADER_SKY_PULSAR_FRAG = shader("shader_sky_pulsar.frag");
public static final Identifier SHADER_POS_TEX_COLOR_RECT_PARAMS_VERT = shader("pos_tex_color_rect_params.vert");
public static final Identifier SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT = shader("pos_tex_local_color_rect_params.vert");
public static final Identifier SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS2_VERT = shader("pos_tex_local_color_rect_params2.vert");
public static final Identifier SHADER_POS_TEX_COLOR_RECT_PARAMS2_VERT = shader("pos_tex_color_rect_params2.vert");
// Text & MSDF
public static final Identifier SHADER_TEXT_VERT = shader("text.vert");
public static final Identifier SHADER_TEXT_FRAG = shader("text.frag");
public static final Identifier SHADER_TEXT_MSDF_FRAG = shader("text_msdf.frag");
public static final Identifier SHADER_SVG_MSDF_FRAG = shader("svg_msdf.frag");
public static final Identifier SHADER_TEXT_LIQUID_GLASS_FRAG = shader("text_liquid_glass.frag");
public static final Identifier SHADER_TEXT_LIQUID_GLASS_MSDF_FRAG = shader("text_liquid_glass_msdf.frag");
// Batched UI shapes (SDF fragments)
public static final Identifier SHADER_CIRCLE_BATCH_FRAG = shader("circle_batch.frag");
public static final Identifier SHADER_ARC_BATCH_FRAG = shader("arc_batch.frag");
public static final Identifier SHADER_ORBIZ_RING_BATCH_FRAG = shader("orbiz_ring_batch.frag");
public static final Identifier SHADER_WORLD_DECAL_SDF_FRAG = shader("world_decal_sdf.frag");
public static final Identifier SHADER_ROUNDED_RECT_STROKE_CORNERS_BATCH_FRAG = shader("rounded_rect_stroke_corners_batch.frag");
public static final Identifier SHADER_ROUNDED_RECT_BATCH_FRAG = shader("rounded_rect_batch.frag");
public static final Identifier SHADER_UI_ROUNDED_FILL_SMOKE_BATCH_FRAG = shader("ui_rounded_fill_smoke_batch.frag");
public static final Identifier SHADER_ROUNDED_RECT_STROKE_BATCH_FRAG = shader("rounded_rect_stroke_batch.frag");
public static final Identifier SHADER_ROUNDED_RECT_STROKE_ANGULAR_BATCH_FRAG = shader("rounded_rect_stroke_angular_batch.frag");
public static final Identifier SHADER_CHAMFERED_RECT_BATCH_FRAG = shader("chamfered_rect_batch.frag");
public static final Identifier SHADER_UI_SHAPE_BATCH_FRAG = shader("ui_shape_batch.frag");
public static final Identifier SHADER_CHAMFERED_RECT_STROKE_BATCH_FRAG = shader("chamfered_rect_stroke_batch.frag");
public static final Identifier SHADER_ROUNDED_RECT_CORNERS_BATCH_FRAG = shader("rounded_rect_corners_batch.frag");
public static final Identifier SHADER_ROUNDED_RECT_GLOW_BATCH_FRAG = shader("rounded_rect_glow_batch.frag");
public static final Identifier SHADER_ROUNDED_RECT_SHADOW_BATCH_FRAG = shader("rounded_rect_shadow_batch.frag");
public static final Identifier SHADER_ROUNDED_RECT_SOFT_SHADOW_BATCH_FRAG = shader("rounded_rect_soft_shadow_batch.frag");
public static final Identifier SHADER_RADIAL_GLOW_BATCH_FRAG = shader("radial_glow_batch.frag");
public static final Identifier SHADER_ROUNDED_RECT_TEX_BATCH_FRAG = shader("rounded_rect_tex_batch.frag");
public static final Identifier SHADER_ROUNDED_RECT_TEX_MASK_BATCH_FRAG = shader("rounded_rect_tex_mask_batch.frag");
public static final Identifier SHADER_UI_BLUR_BATCH_FRAG = shader("ui_blur_batch.frag");
public static final Identifier SHADER_UI_BLUR_BATCH_CORNERS_FRAG = shader("ui_blur_batch_corners.frag");
public static final Identifier SHADER_UI_LIQUID_GLASS_BATCH_FRAG = shader("ui_liquid_glass_batch.frag");
// Fullscreen post-processing & overlays
public static final Identifier SHADER_DAMAGE_TINT_VERT = shader("damage_tint.vert");
public static final Identifier SHADER_DAMAGE_TINT_FRAG = shader("damage_tint.frag");
public static final Identifier SHADER_POST_FX_FRAG = shader("post_fx.frag");
public static final Identifier SHADER_MOTION_BLUR_FRAG = shader("motion_blur.frag");
public static final Identifier SHADER_DEPTH_OF_FIELD_FRAG = shader("depth_of_field.frag");
public static final Identifier SHADER_WET_SURFACE_FRAG = shader("wet_surface.frag");
public static final Identifier SHADER_HEAT_FX_FRAG = shader("heat_fx.frag");
public static final Identifier SHADER_ESP_GRADIENT_FRAG = shader("shader_esp_gradient.frag");
public static final Identifier SHADER_ESP_SHADOW_FRAG = shader("shader_esp_shadow.frag");
public static final Identifier SHADER_ESP_SMOKE_FRAG = shader("shader_esp_smoke.frag");
public static final Identifier SHADER_PORTAL_RIFT_FRAG = shader("portal_rift.frag");
public static final Identifier SHADER_SLEEP_OVERLAY_FRAG = shader("sleep_overlay.frag");
// Hand effects & chams
public static final Identifier SHADER_HAND_SMOKE_FRAG = shader("hand_smoke.frag");
public static final Identifier SHADER_HAND_METALLIC_FRAG = shader("hand_metallic.frag");
public static final Identifier SHADER_HAND_NOISE_FRAG = shader("hand_noise.frag");
public static final Identifier SHADER_UI_BLUR_FRAG = shader("ui_blur.frag");
public static final Identifier SHADER_HAND_GLASS_FRAG = shader("hand_glass.frag");
public static final Identifier SHADER_HAND_CHAMS_SOLID_FRAG = shader("hand_chams_solid.frag");
public static final Identifier SHADER_HAND_CHAMS_CHROMA_FRAG = shader("hand_chams_chroma.frag");
public static final Identifier SHADER_HAND_CHAMS_STRIPES_FRAG = shader("hand_chams_stripes.frag");
public static final Identifier SHADER_HAND_CHAMS_GLOW_FRAG = shader("hand_chams_glow.frag");
public static final Identifier SHADER_HAND_CHAMS_SNOW_FRAG = shader("hand_chams_snow.frag");
public static final Identifier SHADER_HAND_CHAMS_INVERT_FRAG = shader("hand_chams_invert.frag");
public static final Identifier SHADER_HAND_CHAMS_BALATRO_FRAG = shader("hand_chams_balatro.frag");
public static final Identifier SHADER_HAND_CHAMS_BLEND_FRAG = shader("hand_chams_blend.frag");
// Sky & menu backgrounds
public static final Identifier SHADER_SKY_SUN_FRAG = shader("sky_sun.frag");
public static final Identifier SHADER_MENU_BACKGROUND_WAVES_FRAG = shader("menu_background_waves.frag");
public static final Identifier SHADER_MENU_BACKGROUND_AURORA_FRAG = shader("menu_background_aurora.frag");
public static final Identifier SHADER_SKY_SHADER_SKY_FRAG = shader("sky_shader_sky.frag");
public static final Identifier SHADER_SKY_SHADER_WATER_FRAG = shader("sky_shader_water.frag");
// KillEffect procedural beam shaders
public static final Identifier SHADER_KILL_EFFECT_PORTAL_FRAG = shader("kill_effect_portal.frag");
public static final Identifier SHADER_KILL_EFFECT_TORNADO_FRAG = shader("kill_effect_tornado.frag");
// BlockHighlight procedural shader outlines
public static final Identifier SHADER_BLOCK_HIGHLIGHT_WATERCAUSTIC_FRAG = shader("block_highlight_watercaustic.frag");
public static final Identifier SHADER_BLOCK_HIGHLIGHT_SCANLINES_FRAG = shader("block_highlight_scanlines.frag");
public static final Identifier SHADER_BLOCK_HIGHLIGHT_HOLOGRAM_FRAG = shader("block_highlight_hologram.frag");
// SkyShader nebula
public static final Identifier SHADER_SKY_SHADER_NEBULA_FRAG = shader("sky_shader_nebula.frag");
public static final Identifier SHADER_SKY_SHADER_CAUSTIC_FRAG = shader("sky_shader_caustic.frag");
// SkyShader starry (procedural stars + milky way + round moon)
public static final Identifier SHADER_SKY_SHADER_STARRY_FRAG = shader("sky_shader_starry.frag");
private static final List<RenderPipeline> PIPELINES = new ArrayList<>();
public static final RenderPipeline GUI_TEXTURE_LOOKUP = add(ScreenPipelines.guiTextureLookup());
// ======================
// World (3D)
// ======================
/** No depth test; translucent; triangles. */
public static final RenderPipeline WORLD_COLORED = add(WorldPipelines.colored("world_colored", false, BlendFunction.TRANSLUCENT));
/** No depth test; translucent; lines. */
public static final RenderPipeline WORLD_COLORED_LINES = add(WorldPipelines.coloredLines("world_colored_lines", false));
/** Screen-space wide line expansion for WORLD_COLORED_LINES. Uses triangles; never depends on glLineWidth. */
public static final RenderPipeline WORLD_WIDE_COLORED_LINES = add(WorldPipelines.wideLines("world_wide_colored_lines", false));
/** Depth test (GEQUAL); translucent; triangles. */
public static final RenderPipeline WORLD_COLORED_DEPTH = add(WorldPipelines.colored("world_colored_depth", true, BlendFunction.TRANSLUCENT));
/** No depth test; translucent; textured triangles. */
public static final RenderPipeline WORLD_TEXTURED = add(WorldPipelines.textured("world_textured", false, BlendFunction.TRANSLUCENT));
/** Depth test (GEQUAL); translucent; textured triangles. */
public static final RenderPipeline WORLD_TEXTURED_DEPTH = add(WorldPipelines.textured("world_textured_depth", true, BlendFunction.TRANSLUCENT));
/** Depth test (GEQUAL); additive; colored triangles. */
public static final RenderPipeline WORLD_COLORED_ADDITIVE_DEPTH = add(WorldPipelines.colored("world_colored_additive_depth", true, PipelineTemplates.ADDITIVE));
/** No depth test; additive; colored triangles. */
public static final RenderPipeline WORLD_COLORED_ADDITIVE = add(WorldPipelines.colored("world_colored_additive", false, PipelineTemplates.ADDITIVE));
/** No depth test; translucent; world decal quads with SDF fill/stroke. */
public static final RenderPipeline WORLD_DECAL_SDF = add(WorldPipelines.decalSdf("world_decal_sdf", false));
/** Depth test (GEQUAL); translucent; world decal quads with SDF fill/stroke. */
public static final RenderPipeline WORLD_DECAL_SDF_DEPTH = add(WorldPipelines.decalSdf("world_decal_sdf_depth", true));
/** No depth test; translucent; world text quads. */
public static final RenderPipeline WORLD_TEXT = add(WorldPipelines.worldText("world_text", false, SHADER_TEXT_FRAG, false));
/** Depth test (GEQUAL); translucent; world text quads. */
public static final RenderPipeline WORLD_TEXT_DEPTH = add(WorldPipelines.worldText("world_text_depth", true, SHADER_TEXT_FRAG, false));
/** No depth test; translucent; world MSDF text quads. */
public static final RenderPipeline WORLD_TEXT_MSDF = add(WorldPipelines.worldText("world_text_msdf", false, SHADER_TEXT_MSDF_FRAG, true));
/** Depth test (GEQUAL); translucent; world MSDF text quads. */
public static final RenderPipeline WORLD_TEXT_MSDF_DEPTH = add(WorldPipelines.worldText("world_text_msdf_depth", true, SHADER_TEXT_MSDF_FRAG, true));
/** No depth test; additive; textured triangles. */
public static final RenderPipeline WORLD_TEXTURED_ADDITIVE = add(WorldPipelines.textured("world_textured_additive", false, PipelineTemplates.ADDITIVE));
/** No depth test; additive; textured triangles with shader-side tint. */
public static final RenderPipeline WORLD_TEXTURED_TINT_ADDITIVE = add(WorldPipelines.tintedTextured("world_textured_tint_additive", false));
/** Depth test (GEQUAL); additive; textured triangles. */
public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_DEPTH = add(WorldPipelines.textured("world_textured_additive_depth", true, PipelineTemplates.ADDITIVE));
/** Depth test (GEQUAL); translucent; lines. */
public static final RenderPipeline WORLD_COLORED_LINES_DEPTH = add(WorldPipelines.coloredLines("world_colored_lines_depth", true));
/** Screen-space wide line expansion for WORLD_COLORED_LINES_DEPTH. Uses triangles; never depends on glLineWidth. */
public static final RenderPipeline WORLD_WIDE_COLORED_LINES_DEPTH = add(WorldPipelines.wideLines("world_wide_colored_lines_depth", true));
/** Depth test (GEQUAL); translucent; triangles; lets liquids blend over (no depth write). */
public static final RenderPipeline WORLD_COLORED_LIQUID_BLEND = add(WorldPipelines.colored("world_colored_liquid_blend", true, BlendFunction.TRANSLUCENT));
/** Depth test (GEQUAL); translucent; lines; lets liquids blend over (no depth write). */
public static final RenderPipeline WORLD_COLORED_LINES_LIQUID_BLEND = add(WorldPipelines.coloredLines("world_colored_lines_liquid_blend", true));
/** Screen-space wide line expansion for WORLD_COLORED_LINES_LIQUID_BLEND. Uses triangles; never depends on glLineWidth. */
public static final RenderPipeline WORLD_WIDE_COLORED_LINES_LIQUID_BLEND = add(WorldPipelines.wideLines("world_wide_colored_lines_liquid_blend", true));
/** Depth test (GEQUAL); translucent; textured triangles; lets liquids blend over (no depth write). */
public static final RenderPipeline WORLD_TEXTURED_LIQUID_BLEND = add(WorldPipelines.textured("world_textured_liquid_blend", true, BlendFunction.TRANSLUCENT));
/** Depth test (GEQUAL); additive; textured triangles; lets liquids blend over (no depth write). */
public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_LIQUID_BLEND = add(WorldPipelines.textured("world_textured_additive_liquid_blend", true, PipelineTemplates.ADDITIVE));
/** Depth test (GEQUAL); translucent; triangles; intended for above-liquids pass (no depth write). */
public static final RenderPipeline WORLD_COLORED_LIQUID_IGNORE = add(WorldPipelines.colored("world_colored_liquid_ignore", true, BlendFunction.TRANSLUCENT));
/** Depth test (GEQUAL); translucent; lines; writes depth so liquids render below (above-liquids). */
public static final RenderPipeline WORLD_COLORED_LINES_LIQUID_IGNORE = add(WorldPipelines.coloredLines("world_colored_lines_liquid_ignore", true));
/** Screen-space wide line expansion for WORLD_COLORED_LINES_LIQUID_IGNORE. Uses triangles; never depends on glLineWidth. */
public static final RenderPipeline WORLD_WIDE_COLORED_LINES_LIQUID_IGNORE = add(WorldPipelines.wideLines("world_wide_colored_lines_liquid_ignore", true));
/** Depth test (GEQUAL); additive; textured triangles; intended for pre-water depth. */
public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE = add(WorldPipelines.textured("world_textured_additive_liquid_ignore", true, PipelineTemplates.ADDITIVE));
/** Depth test (GEQUAL); additive; textured triangles with shader-side tint; intended for pre-water depth. */
public static final RenderPipeline WORLD_TEXTURED_TINT_ADDITIVE_LIQUID_IGNORE = add(WorldPipelines.tintedTextured("world_textured_tint_additive_liquid_ignore", true));
/** Depth test (GEQUAL); translucent; textured triangles; intended for pre-water depth. */
public static final RenderPipeline WORLD_TEXTURED_LIQUID_IGNORE = add(WorldPipelines.textured("world_textured_liquid_ignore", true, BlendFunction.TRANSLUCENT));
// ======================
// UI (2D)
// ======================
/** UI triangles (pos2 + color). */
public static final RenderPipeline UI_COLORED = add(UiPipelines.colored("ui_colored", PrimitiveTopology.TRIANGLES));
/** UI lines (pos2 + color). */
public static final RenderPipeline UI_COLORED_LINES = add(UiPipelines.colored("ui_colored_lines", PrimitiveTopology.DEBUG_LINES));
/** UI textured triangles (pos2 + tex + color). */
public static final RenderPipeline UI_TEXTURED = add(UiPipelines.textured("ui_textured", SHADER_POS_TEX_COLOR_VERT, SHADER_POS_TEX_COLOR_FRAG, BlendFunction.TRANSLUCENT, true, null, "u_Texture"));
/** UI MSDF SVG quads (pos2 + tex + color). */
public static final RenderPipeline UI_SVG_MSDF = add(UiPipelines.textured("ui_svg_msdf", SHADER_POS_TEX_COLOR_VERT, SHADER_SVG_MSDF_FRAG, BlendFunction.TRANSLUCENT, true, "MsdfText", "u_Texture"));
/** UI textured triangles (pos2 + tex + color), additive blend. */
public static final RenderPipeline UI_TEXTURED_ADDITIVE = add(UiPipelines.textured("ui_textured_additive", SHADER_POS_TEX_COLOR_VERT, SHADER_POS_TEX_COLOR_FRAG, PipelineTemplates.ADDITIVE, true, null, "u_Texture"));
/** UI text (pos2 + tex + color). */
public static final RenderPipeline UI_TEXT = add(UiPipelines.textured("ui_text", SHADER_TEXT_VERT, SHADER_TEXT_FRAG, BlendFunction.TRANSLUCENT, true, null, "u_Texture"));
/** UI MSDF text (pos2 + tex + color). */
public static final RenderPipeline UI_TEXT_MSDF = add(UiPipelines.textured("ui_text_msdf", SHADER_TEXT_VERT, SHADER_TEXT_MSDF_FRAG, BlendFunction.TRANSLUCENT, true, "MsdfText", "u_Texture"));
public static final RenderPipeline UI_TEXT_LIQUID_GLASS = add(UiPipelines.textLiquidGlass("ui_text_liquid_glass", SHADER_TEXT_LIQUID_GLASS_FRAG, false));
public static final RenderPipeline UI_TEXT_LIQUID_GLASS_MSDF = add(UiPipelines.textLiquidGlass("ui_text_liquid_glass_msdf", SHADER_TEXT_LIQUID_GLASS_MSDF_FRAG, true));
public static final RenderPipeline UI_BLUR = add(UiPipelines.textured("ui_blur", SHADER_POS_TEX_COLOR_VERT, SHADER_UI_BLUR_FRAG, BlendFunction.TRANSLUCENT, false, "UIBlur", "u_Texture"));
/** Fullscreen menu background (pos2). */
public static final RenderPipeline MENU_BACKGROUND_WAVES = add(ScreenPipelines.screen("menu_background_waves", SHADER_MENU_BACKGROUND_WAVES_FRAG, "MenuBackground"));
/** Fullscreen SkyShader passes (pos2). Sky/water/caustic share the same uniform layout. */
public static final RenderPipeline SKY_SHADER_SKY = add(ScreenPipelines.screen("sky_shader_sky", SHADER_SKY_SHADER_SKY_FRAG, "SkyShader"));
public static final RenderPipeline SKY_SHADER_WATER = add(ScreenPipelines.screen("sky_shader_water", SHADER_SKY_SHADER_WATER_FRAG, "SkyShader"));
public static final RenderPipeline SKY_SHADER_CAUSTIC = add(ScreenPipelines.screen("sky_shader_caustic", SHADER_SKY_SHADER_CAUSTIC_FRAG, "SkyShader"));
public static final RenderPipeline SKY_SHADER_NEBULA = add(ScreenPipelines.screen("sky_shader_nebula", SHADER_SKY_SHADER_NEBULA_FRAG, "SkyShaderData"));
public static final RenderPipeline SKY_SHADER_STARRY = add(ScreenPipelines.screen("sky_shader_starry", SHADER_SKY_SHADER_STARRY_FRAG, "SkyShaderStarry"));
/** Fullscreen main menu background (pos2). */
public static final RenderPipeline MENU_BACKGROUND_AURORA = add(ScreenPipelines.screen("menu_background_aurora", SHADER_MENU_BACKGROUND_AURORA_FRAG, "MenuBackground"));
/** Fullscreen postprocess (pos2). */
public static final RenderPipeline DAMAGE_TINT = add(ScreenPipelines.screen("damage_tint", SHADER_DAMAGE_TINT_FRAG, "PostProcess", "u_Texture"));
/** Fullscreen visual grading + LUT (pos2). */
public static final RenderPipeline POST_FX = add(ScreenPipelines.screen("post_fx", SHADER_POST_FX_FRAG, "PostFX", "u_Texture", "u_Lut"));
/** Fullscreen camera-only perceptual motion blur (pos2). */
public static final RenderPipeline MOTION_BLUR = add(ScreenPipelines.screen("motion_blur", SHADER_MOTION_BLUR_FRAG, "MotionBlur", "u_Texture", "u_PreviousColor"));
/** Fullscreen scene-depth-aware far depth of field (pos2). */
public static final RenderPipeline DEPTH_OF_FIELD = add(ScreenPipelines.screen("depth_of_field", SHADER_DEPTH_OF_FIELD_FRAG, "DepthOfField",
"u_Texture", "u_MainDepth", "u_TranslucentDepth", "u_ItemEntityDepth", "u_ParticlesDepth", "u_WeatherDepth", "u_CloudsDepth"));
/** Fullscreen wet-ground screen-space reflection pass (pos2). */
public static final RenderPipeline WET_SURFACE = add(ScreenPipelines.screen("wet_surface", SHADER_WET_SURFACE_FRAG, "WetSurface", "u_Texture", "u_MainDepth"));
/** Fullscreen heat distortion + vignette (pos2). */
public static final RenderPipeline HEAT_FX = add(ScreenPipelines.screen("heat_fx", SHADER_HEAT_FX_FRAG, "Heat", "u_Texture"));
/** Fullscreen shader ESP gradient composite (pos2). */
public static final RenderPipeline SHADER_ESP_GRADIENT = add(ScreenPipelines.screen("shader_esp_gradient", SHADER_ESP_GRADIENT_FRAG, "ShaderEspGradient", "u_Texture"));
/** Fullscreen shader ESP separable shadow blur (pos2). */
public static final RenderPipeline SHADER_ESP_SHADOW = add(ScreenPipelines.screen("shader_esp_shadow", SHADER_ESP_SHADOW_FRAG, "ShaderEspBlur", "u_Texture", "u_Mask"));
/** Fullscreen shader ESP procedural smoke fill (pos2). */
public static final RenderPipeline SHADER_ESP_SMOKE = add(ScreenPipelines.screen("shader_esp_smoke", SHADER_ESP_SMOKE_FRAG, "ShaderEspSmoke", "u_Texture"));
/** No depth test; additive; textured-color triangles with procedural sun shading. */
public static final RenderPipeline WORLD_SKY_SUN = add(WorldPipelines.skySun("world_sky_sun"));
/** No depth test; additive; KillEffect "Portal" procedural beam shader. */
public static final RenderPipeline WORLD_KILL_EFFECT_PORTAL =
add(WorldPipelines.killEffectBeam("world_kill_effect_portal", SHADER_KILL_EFFECT_PORTAL_FRAG));
/** No depth test; additive; KillEffect "Tornado" procedural beam shader. */
public static final RenderPipeline WORLD_KILL_EFFECT_TORNADO =
add(WorldPipelines.killEffectBeam("world_kill_effect_tornado", SHADER_KILL_EFFECT_TORNADO_FRAG));
/** No depth test; additive; BlockHighlight "WaterCaustic" shader outline. */
public static final RenderPipeline WORLD_BLOCK_HIGHLIGHT_WATERCAUSTIC =
add(WorldPipelines.waterHighlight("world_block_highlight_watercaustic", SHADER_BLOCK_HIGHLIGHT_WATERCAUSTIC_FRAG));
/** No depth test; additive; BlockHighlight "ScanLines" shader outline. */
public static final RenderPipeline WORLD_BLOCK_HIGHLIGHT_SCANLINES =
add(WorldPipelines.waterHighlight("world_block_highlight_scanlines", SHADER_BLOCK_HIGHLIGHT_SCANLINES_FRAG));
/** No depth test; additive; BlockHighlight "Hologram" shader outline. */
public static final RenderPipeline WORLD_BLOCK_HIGHLIGHT_HOLOGRAM =
add(WorldPipelines.waterHighlight("world_block_highlight_hologram", SHADER_BLOCK_HIGHLIGHT_HOLOGRAM_FRAG));
/** Fullscreen nether portal rift (pos2). */
public static final RenderPipeline PORTAL_RIFT = add(ScreenPipelines.screen("portal_rift", SHADER_PORTAL_RIFT_FRAG, "Heat", "u_Texture"));
/** Fullscreen sleep overlay (pos2). */
public static final RenderPipeline SLEEP_OVERLAY = add(ScreenPipelines.screen("sleep_overlay", SHADER_SLEEP_OVERLAY_FRAG, "PostProcess", "u_Texture"));
/** Fullscreen hand glass (pos2). */
public static final RenderPipeline HAND_GLASS = add(ScreenPipelines.handEffect("hand_glass", SHADER_HAND_GLASS_FRAG, "HandGlass"));
public static final RenderPipeline HAND_SMOKE = add(ScreenPipelines.handEffect("hand_smoke", SHADER_HAND_SMOKE_FRAG, "HandSmoke"));
public static final RenderPipeline HAND_METALLIC = add(ScreenPipelines.handEffect("hand_metallic", SHADER_HAND_METALLIC_FRAG, "HandMetallic"));
public static final RenderPipeline HAND_NOISE = add(ScreenPipelines.handEffect("hand_noise", SHADER_HAND_NOISE_FRAG, "HandNoise"));
public static final RenderPipeline HAND_CHAMS_SOLID = add(ScreenPipelines.handEffect("hand_chams_solid", SHADER_HAND_CHAMS_SOLID_FRAG, "HandChams"));
public static final RenderPipeline HAND_CHAMS_CHROMA = add(ScreenPipelines.handEffect("hand_chams_chroma", SHADER_HAND_CHAMS_CHROMA_FRAG, "HandChams"));
public static final RenderPipeline HAND_CHAMS_STRIPES = add(ScreenPipelines.handEffect("hand_chams_stripes", SHADER_HAND_CHAMS_STRIPES_FRAG, "HandChams"));
public static final RenderPipeline HAND_CHAMS_GLOW = add(ScreenPipelines.handEffect("hand_chams_glow", SHADER_HAND_CHAMS_GLOW_FRAG, "HandChams"));
public static final RenderPipeline HAND_CHAMS_SNOW = add(ScreenPipelines.handEffect("hand_chams_snow", SHADER_HAND_CHAMS_SNOW_FRAG, "HandChams"));
public static final RenderPipeline HAND_CHAMS_INVERT = add(ScreenPipelines.handEffect("hand_chams_invert", SHADER_HAND_CHAMS_INVERT_FRAG, "HandChams"));
public static final RenderPipeline HAND_CHAMS_BALATRO = add(ScreenPipelines.handEffect("hand_chams_balatro", SHADER_HAND_CHAMS_BALATRO_FRAG, "HandChams"));
public static final RenderPipeline HAND_CHAMS_BLEND = add(ScreenPipelines.handBlendEffect("hand_chams_blend", SHADER_HAND_CHAMS_BLEND_FRAG));
/** No depth test; translucent; textured skybox with vanilla sky/fog blending. */
public static final RenderPipeline WORLD_SKYBOX_TEXTURED_FOG = add(WorldPipelines.texturedSkyboxFog("world_skybox_textured_fog", SHADER_POS_TEX_COLOR_SKY_FOG_FRAG, BlendFunction.TRANSLUCENT));
/** No depth test; additive; textured skybox with vanilla sky/fog blending. */
public static final RenderPipeline WORLD_SKYBOX_TEXTURED_FOG_ADDITIVE = add(WorldPipelines.texturedSkyboxFog("world_skybox_textured_fog_additive", SHADER_POS_TEX_COLOR_SKY_FOG_ADDITIVE_FRAG, PipelineTemplates.ADDITIVE));
/** Fullscreen procedural Reimagined skybox with sky/fog blending. */
public static final RenderPipeline WORLD_REIMAGINED_SKYBOX_SHADER = add(WorldPipelines.proceduralSkyboxFog("world_reimagined_skybox_shader", SHADER_REIMAGINED_SKYBOX_FRAG));
/** Fullscreen procedural ShaderSky (Thunder) with sky/fog blending. */
public static final RenderPipeline WORLD_SHADER_SKY_THUNDER = add(ScreenPipelines.screen("world_shader_sky_thunder", SHADER_SHADER_SKY_THUNDER_FRAG, "ShaderSky"));
/** Fullscreen procedural ShaderSky (Pulsar) with sky/fog blending. */
public static final RenderPipeline WORLD_SHADER_SKY_PULSAR = add(ScreenPipelines.screen("world_shader_sky_pulsar", SHADER_SHADER_SKY_PULSAR_FRAG, "ShaderSky"));
// ======================
// Batched UI shapes (2D)
// ======================
public static final RenderPipeline UI_CIRCLE_BATCH = add(UiPipelines.batch("ui_circle_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_CIRCLE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_ARC_BATCH = add(UiPipelines.batch("ui_arc_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_ARC_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_ORBIZ_RING_BATCH = add(UiPipelines.batch("ui_orbiz_ring_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_GENERIC_VERT, SHADER_ORBIZ_RING_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_ROUNDED_BATCH = add(UiPipelines.batch("ui_rounded_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_ROUNDED_FILL_SMOKE_BATCH = add(UiPipelines.batch("ui_rounded_fill_smoke_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT, SHADER_UI_ROUNDED_FILL_SMOKE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_ROUNDED_STROKE_BATCH = add(UiPipelines.batch("ui_rounded_stroke_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_STROKE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_ROUNDED_STROKE_ANGULAR_BATCH = add(UiPipelines.batch("ui_rounded_stroke_angular_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_ROUNDED_RECT_STROKE_ANGULAR_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_CHAMFERED_BATCH = add(UiPipelines.batch("ui_chamfered_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_CHAMFERED_RECT_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_SHAPE_BATCH = add(UiPipelines.batch("ui_shape_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT, SHADER_UI_SHAPE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_CHAMFERED_STROKE_BATCH = add(UiPipelines.batch("ui_chamfered_stroke_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_CHAMFERED_RECT_STROKE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_ROUNDED_STROKE_CORNERS_BATCH = add(UiPipelines.batch("ui_rounded_stroke_corners_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_ROUNDED_RECT_STROKE_CORNERS_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_ROUNDED_CORNERS_BATCH = add(UiPipelines.batch("ui_rounded_corners_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_ROUNDED_RECT_CORNERS_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_ROUNDED_GLOW_BATCH = add(UiPipelines.batch("ui_rounded_glow_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_GLOW_BATCH_FRAG, PipelineTemplates.ADDITIVE, true));
public static final RenderPipeline UI_ROUNDED_SHADOW_BATCH = add(UiPipelines.batch("ui_rounded_shadow_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_SHADOW_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_ROUNDED_SOFT_SHADOW_BATCH = add(UiPipelines.batch("ui_rounded_soft_shadow_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_SOFT_SHADOW_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
public static final RenderPipeline UI_RADIAL_GLOW_BATCH = add(UiPipelines.batch("ui_radial_glow_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_RADIAL_GLOW_BATCH_FRAG, PipelineTemplates.ADDITIVE, true));
public static final RenderPipeline UI_ROUNDED_TEXTURED_BATCH = add(UiPipelines.batch("ui_rounded_textured_batch",
LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_TEX_BATCH_FRAG, BlendFunction.TRANSLUCENT, true, "u_Texture"));
public static final RenderPipeline UI_ROUNDED_TEXTURED_MASK_BATCH = add(UiPipelines.batch("ui_rounded_textured_mask_batch",
LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_TEX_MASK_BATCH_FRAG, BlendFunction.TRANSLUCENT, true, "u_Texture"));
public static final RenderPipeline UI_BLUR_BATCH = add(UiPipelines.batch("ui_blur_batch",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_UI_BLUR_BATCH_FRAG, BlendFunction.TRANSLUCENT, false, "u_Texture"));
public static final RenderPipeline UI_BLUR_BATCH_CORNERS = add(UiPipelines.batch("ui_blur_batch_corners",
LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_UI_BLUR_BATCH_CORNERS_FRAG, BlendFunction.TRANSLUCENT, false, "u_Texture"));
public static final RenderPipeline UI_LIQUID_GLASS_BATCH = add(UiPipelines.batch("ui_liquid_glass_batch",
LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_UI_LIQUID_GLASS_BATCH_FRAG, BlendFunction.TRANSLUCENT, false, "u_Texture", "u_BlurTexture"));
/**
* Compile all registered pipelines (use after resource reload).
*/
public static void precompile(ResourceManager resources) {
final GpuDevice device;
try {
device = RenderSystem.getDevice();
} catch (Throwable t) {
DebugLog.warn("[LoVisual] RenderSystem not ready in precompile(ResourceManager)");
return;
}
DebugLog.renderThread("[LoVisual] Precompiling pipelines: " + PIPELINES.size());
for (RenderPipeline pipeline : PIPELINES) {
DebugLog.renderThread("[LoVisual] -> pipeline " + pipeline.getLocation());
DebugLog.renderThread(" VS = " + pipeline.getVertexShader());
DebugLog.renderThread(" FS = " + pipeline.getFragmentShader());
DebugLog.renderThread(" VF = " + pipeline.getVertexFormatBinding(0));
DebugLog.renderThread(" MODE = " + pipeline.getPrimitiveTopology());
device.precompilePipeline(pipeline, (identifier, shaderType) -> {
DebugLog.renderThread("[LoVisual] loading " + shaderType + " " + identifier);
return LoVisualShaderSources.load(resources, identifier, shaderType);
});
}
DebugLog.renderThread("[LoVisual] Precompile finished");
}
/**
* compile using the current MinecraftClient resource manager.
*/
public static List<RenderPipeline> all() {
return Collections.unmodifiableList(PIPELINES);
}
public static RenderPipeline.Snippet meshUniforms() {
return PipelineTemplates.MESH_UNIFORMS;
}
public static RenderPipeline registerAddonPipeline(RenderPipeline pipeline) {
return add(pipeline);
}
private static RenderPipeline add(RenderPipeline pipeline) {
declareDefaultShapeClipContract(pipeline);
PIPELINES.add(pipeline);
RenderPipelineRegistry.global().registerNative(pipeline);
return pipeline;
}
private static void declareDefaultShapeClipContract(RenderPipeline pipeline) {
if (!(pipeline instanceof IRenderPipeline lovisualPipeline) || pipeline.getLocation() == null) return;
ShapeClipRenderPassContract current = lovisualPipeline.lovisual$getShapeClipContract();
if (current != ShapeClipRenderPassContract.NONE) return;
String namespace = pipeline.getLocation().getNamespace();
String path = pipeline.getLocation().getPath();
if ("lovisual".equals(namespace) && (path.startsWith("pipeline/ui_") || path.equals("pipeline/gui_texture_lookup"))) {
lovisualPipeline.lovisual$setShapeClipContract(ShapeClipRenderPassContract.WHEN_ACTIVE);
}
}
/**
* LoVisual shader asset identifier for "shaders/&lt;name&gt;".
*/
private static Identifier shader(String name) {
return Identifier.fromNamespaceAndPath("lovisual", "shaders/" + name);
}
}

View file

@ -0,0 +1,60 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.pipeline.pipelines;
import com.mojang.blaze3d.pipeline.BlendFunction;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.platform.BlendFactor;
import com.mojang.blaze3d.shaders.UniformType;
import dev.loki.lovisual.render.engine.pipeline.DepthTestFunction;
import dev.loki.lovisual.render.engine.pipeline.ExtendedRenderPipelineBuilder;
import net.minecraft.resources.Identifier;
/**
* Shared building blocks for the LoVisual pipeline factories: uniform snippets, blend
* presets and the common depth/blend/cull tail that every custom pipeline applies.
*/
public final class PipelineTemplates {
/** Additive blending (SRC_ALPHA, ONE) shared by all additive LoVisual pipelines. */
public static final BlendFunction ADDITIVE = new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE);
public static final RenderPipeline.Snippet MESH_UNIFORMS = uniformSnippet("MeshData");
public static final RenderPipeline.Snippet FOG_UNIFORMS = uniformSnippet("Fog");
public static final RenderPipeline.Snippet UI_BATCH_UNIFORMS = uniformSnippet("UIBatch");
private PipelineTemplates() {
}
/**
* Snippet declaring a single {@link UniformType#UNIFORM_BUFFER} uniform.
*/
public static RenderPipeline.Snippet uniformSnippet(String name) {
return new ExtendedRenderPipelineBuilder()
.withUniform(name, UniformType.UNIFORM_BUFFER)
.buildSnippet();
}
/**
* Location identifier for a "pipeline/&lt;name&gt;" entry in the lovisual namespace.
*/
public static Identifier location(String name) {
return Identifier.fromNamespaceAndPath("lovisual", "pipeline/" + name);
}
/**
* Applies the shared pipeline tail (depth test, depth write off, blend, cull) and builds.
*/
public static RenderPipeline finish(ExtendedRenderPipelineBuilder builder, boolean depthTest, BlendFunction blend, boolean cull) {
return builder
.withDepthTestFunction(depthTest ? DepthTestFunction.GEQUAL_DEPTH_TEST : DepthTestFunction.NO_DEPTH_TEST)
.withDepthWrite(false)
.withBlend(blend)
.withCull(cull)
.build();
}
}

View file

@ -0,0 +1,72 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.pipeline.pipelines;
import com.mojang.blaze3d.PrimitiveTopology;
import com.mojang.blaze3d.pipeline.BlendFunction;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.shaders.UniformType;
import dev.loki.lovisual.render.engine.pipeline.ExtendedRenderPipelineBuilder;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.resources.Identifier;
/**
* Factory templates for LoVisual's fullscreen pos2 pipelines (post effects, hand effects,
* shader skies) plus the vanilla-textured GUI lookup pipeline. Samplers are declared
* before the single uniform buffer, matching the original declaration order.
*/
public final class ScreenPipelines {
private ScreenPipelines() {
}
/**
* Minimal vanilla-textured pipeline used for GUI texture lookups.
*/
public static RenderPipeline guiTextureLookup() {
return new ExtendedRenderPipelineBuilder(RenderPipelines.GUI_TEXTURED_SNIPPET)
.withLocation(PipelineTemplates.location("gui_texture_lookup"))
.withFragmentShader(LoVisualRenderPipelines.SHADER_GUI_TEXTURE_LOOKUP_FRAG)
.build();
}
/**
* Fullscreen pos2 pass: damage-tint vertex shader, no depth test, translucent, no culling.
*/
public static RenderPipeline screen(String name, Identifier fragmentShader, String uniform, String... samplers) {
ExtendedRenderPipelineBuilder builder = base(name)
.withVertexFormat(LoVisualVertexFormats.POS2, PrimitiveTopology.TRIANGLES)
.withVertexShader(LoVisualRenderPipelines.SHADER_DAMAGE_TINT_VERT)
.withFragmentShader(fragmentShader);
for (String sampler : samplers) {
builder.withSampler(sampler);
}
builder.withUniform(uniform, UniformType.UNIFORM_BUFFER);
return PipelineTemplates.finish(builder, false, BlendFunction.TRANSLUCENT, false);
}
/**
* Fullscreen hand effect with the standard "u_Src"/"u_Mask" sampler pair.
*/
public static RenderPipeline handEffect(String name, Identifier fragmentShader, String uniform) {
return screen(name, fragmentShader, uniform, "u_Src", "u_Mask");
}
/**
* Fullscreen chams blend effect sampling two shader textures plus the mask.
*/
public static RenderPipeline handBlendEffect(String name, Identifier fragmentShader) {
return screen(name, fragmentShader, "HandBlend", "u_Shader1", "u_Shader2", "u_Mask");
}
private static ExtendedRenderPipelineBuilder base(String name) {
return new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS)
.withLocation(PipelineTemplates.location(name));
}
}

View file

@ -0,0 +1,96 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.pipeline.pipelines;
import com.mojang.blaze3d.PrimitiveTopology;
import com.mojang.blaze3d.pipeline.BlendFunction;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.shaders.UniformType;
import com.mojang.blaze3d.vertex.VertexFormat;
import dev.loki.lovisual.render.engine.pipeline.ExtendedRenderPipelineBuilder;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats;
import net.minecraft.resources.Identifier;
/**
* Factory templates for LoVisual's UI (2D) pipelines: the base colored/textured set and
* the batched SDF shape pipelines built on the local-color rect-params vertex formats.
* Samplers are declared before extra uniform buffers, matching the original order.
*/
public final class UiPipelines {
private UiPipelines() {
}
/**
* UI colored primitives (pos2 + color); the canvas check in the builder still applies.
*/
public static RenderPipeline colored(String name, PrimitiveTopology topology) {
return PipelineTemplates.finish(base(name)
.withVertexFormat(LoVisualVertexFormats.POS2_COLOR, topology)
.withVertexShader(LoVisualRenderPipelines.SHADER_POS_COLOR_VERT)
.withFragmentShader(LoVisualRenderPipelines.SHADER_POS_COLOR_FRAG), false, BlendFunction.TRANSLUCENT, true);
}
/**
* UI textured triangles (pos2 + tex + color); {@code uniform} may be null.
*/
public static RenderPipeline textured(String name, Identifier vertexShader, Identifier fragmentShader,
BlendFunction blend, boolean cull, String uniform, String... samplers) {
ExtendedRenderPipelineBuilder builder = base(name)
.withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, PrimitiveTopology.TRIANGLES)
.withVertexShader(vertexShader)
.withFragmentShader(fragmentShader);
for (String sampler : samplers) {
builder.withSampler(sampler);
}
if (uniform != null) {
builder.withUniform(uniform, UniformType.UNIFORM_BUFFER);
}
return PipelineTemplates.finish(builder, false, blend, cull);
}
/**
* UI text with liquid glass sampling; MSDF variants add the "MsdfText" uniform.
*/
public static RenderPipeline textLiquidGlass(String name, Identifier fragmentShader, boolean msdf) {
ExtendedRenderPipelineBuilder builder = base(name)
.withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, PrimitiveTopology.TRIANGLES)
.withVertexShader(LoVisualRenderPipelines.SHADER_TEXT_VERT)
.withFragmentShader(fragmentShader)
.withSampler("u_Texture")
.withSampler("u_SceneTexture")
.withSampler("u_BlurTexture")
.withUniform("UIBatch", UniformType.UNIFORM_BUFFER);
if (msdf) {
builder.withUniform("MsdfText", UniformType.UNIFORM_BUFFER);
}
return PipelineTemplates.finish(builder, false, BlendFunction.TRANSLUCENT, true);
}
/**
* Batched SDF shape pipeline (local-color rect-params formats) with the shared UI batch
* uniforms; optional texture samplers keep their original declaration order.
*/
public static RenderPipeline batch(String name, VertexFormat format, Identifier vertexShader, Identifier fragmentShader,
BlendFunction blend, boolean cull, String... samplers) {
ExtendedRenderPipelineBuilder builder = new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS, PipelineTemplates.UI_BATCH_UNIFORMS)
.withLocation(PipelineTemplates.location(name))
.withVertexFormat(format, PrimitiveTopology.TRIANGLES)
.withVertexShader(vertexShader)
.withFragmentShader(fragmentShader);
for (String sampler : samplers) {
builder.withSampler(sampler);
}
return PipelineTemplates.finish(builder, false, blend, cull);
}
private static ExtendedRenderPipelineBuilder base(String name) {
return new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS)
.withLocation(PipelineTemplates.location(name));
}
}

View file

@ -0,0 +1,175 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.pipeline.pipelines;
import com.mojang.blaze3d.PrimitiveTopology;
import com.mojang.blaze3d.pipeline.BlendFunction;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.shaders.UniformType;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import dev.loki.lovisual.render.engine.pipeline.ExtendedRenderPipelineBuilder;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats;
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.KillEffect;
import dev.loki.lovisual.features.module.modules.visuals.scene.BlockHighlight;
import net.minecraft.resources.Identifier;
/**
* Factory templates for LoVisual's world-space (3D) pipelines. They all share the mesh
* uniform snippet, disable depth writing and culling; only depth test, blending, shaders
* and vertex format vary. Shader identifiers come from the public facade constants.
*/
public final class WorldPipelines {
private WorldPipelines() {
}
/**
* Solid colored triangles; {@code depthTest} selects GEQUAL vs no depth test.
*/
public static RenderPipeline colored(String name, boolean depthTest, BlendFunction blend) {
return PipelineTemplates.finish(base(name)
.withVertexFormat(DefaultVertexFormat.POSITION_COLOR, PrimitiveTopology.TRIANGLES)
.withVertexShader(LoVisualRenderPipelines.SHADER_POS_COLOR_VERT)
.withFragmentShader(LoVisualRenderPipelines.SHADER_POS_COLOR_FRAG), depthTest, blend, false);
}
/**
* Smooth debug lines rendered with the plain color shader.
*/
public static RenderPipeline coloredLines(String name, boolean depthTest) {
return PipelineTemplates.finish(base(name)
.withLineSmooth()
.withVertexFormat(DefaultVertexFormat.POSITION_COLOR, PrimitiveTopology.DEBUG_LINES)
.withVertexShader(LoVisualRenderPipelines.SHADER_POS_COLOR_VERT)
.withFragmentShader(LoVisualRenderPipelines.SHADER_POS_COLOR_FRAG), depthTest, BlendFunction.TRANSLUCENT, false);
}
/**
* Screen-space wide line expansion; triangles, never depends on glLineWidth.
*/
public static RenderPipeline wideLines(String name, boolean depthTest) {
return PipelineTemplates.finish(base(name)
.withVertexFormat(LoVisualVertexFormats.POS3_COLOR_LINE, PrimitiveTopology.TRIANGLES)
.withVertexShader(LoVisualRenderPipelines.SHADER_WIDE_LINE_VERT)
.withFragmentShader(LoVisualRenderPipelines.SHADER_POS_COLOR_FRAG), depthTest, BlendFunction.TRANSLUCENT, false);
}
/**
* Textured triangles sampling "u_Texture".
*/
public static RenderPipeline textured(String name, boolean depthTest, BlendFunction blend) {
return PipelineTemplates.finish(base(name)
.withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES)
.withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_VERT)
.withFragmentShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_FRAG)
.withSampler("u_Texture"), depthTest, blend, false);
}
/**
* Textured triangles with shader-side tint ("TextureTint" uniform), additive blending.
*/
public static RenderPipeline tintedTextured(String name, boolean depthTest) {
return PipelineTemplates.finish(base(name)
.withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES)
.withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_VERT)
.withFragmentShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_TINT_FRAG)
.withSampler("u_Texture")
.withUniform("TextureTint", UniformType.UNIFORM_BUFFER), depthTest, PipelineTemplates.ADDITIVE, false);
}
/**
* World decal quads with SDF fill/stroke.
*/
public static RenderPipeline decalSdf(String name, boolean depthTest) {
return PipelineTemplates.finish(base(name)
.withVertexFormat(LoVisualVertexFormats.POS3_TEXTURE_COLOR_PARAMS2, PrimitiveTopology.TRIANGLES)
.withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_PARAMS2_VERT)
.withFragmentShader(LoVisualRenderPipelines.SHADER_WORLD_DECAL_SDF_FRAG), depthTest, BlendFunction.TRANSLUCENT, false);
}
/**
* World text quads; MSDF variants add the "MsdfText" uniform after "u_Texture".
*/
public static RenderPipeline worldText(String name, boolean depthTest, Identifier fragmentShader, boolean msdf) {
ExtendedRenderPipelineBuilder builder = base(name)
.withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES)
.withVertexShader(LoVisualRenderPipelines.SHADER_TEXT_VERT)
.withFragmentShader(fragmentShader)
.withSampler("u_Texture");
if (msdf) {
builder.withUniform("MsdfText", UniformType.UNIFORM_BUFFER);
}
return PipelineTemplates.finish(builder, depthTest, BlendFunction.TRANSLUCENT, false);
}
/**
* Textured triangles with procedural sun shading ("SkySun" uniform), additive.
*/
public static RenderPipeline skySun(String name) {
return PipelineTemplates.finish(base(name)
.withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES)
.withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_VERT)
.withFragmentShader(LoVisualRenderPipelines.SHADER_SKY_SUN_FRAG)
.withUniform("SkySun", UniformType.UNIFORM_BUFFER), false, PipelineTemplates.ADDITIVE, false);
}
/**
* Textured triangles with a procedural KillEffect beam shader ("KillEffectBeam" uniform), additive.
*/
public static RenderPipeline killEffectBeam(String name, Identifier fragmentShader) {
return PipelineTemplates.finish(base(name)
.withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES)
.withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_VERT)
.withFragmentShader(fragmentShader)
.withUniform("KillEffectBeam", UniformType.UNIFORM_BUFFER), false, PipelineTemplates.ADDITIVE, false);
}
/**
* Textured triangles with a procedural BlockHighlight shader outline ("WaterData" uniform), additive.
*/
public static RenderPipeline waterHighlight(String name, Identifier fragmentShader) {
return PipelineTemplates.finish(base(name)
.withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES)
.withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_VERT)
.withFragmentShader(fragmentShader)
.withUniform("WaterData", UniformType.UNIFORM_BUFFER), false, PipelineTemplates.ADDITIVE, false);
}
/**
* Textured skybox with vanilla sky/fog blending ("SkyboxShader" uniform + fog snippet).
*/
public static RenderPipeline texturedSkyboxFog(String name, Identifier fragmentShader, BlendFunction blend) {
return PipelineTemplates.finish(fogBase(name)
.withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES)
.withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_SKY_FOG_VERT)
.withFragmentShader(fragmentShader)
.withSampler("u_Texture")
.withUniform("SkyboxShader", UniformType.UNIFORM_BUFFER), false, blend, false);
}
/**
* Fullscreen procedural skybox with sky/fog blending ("SkyboxShader" uniform + fog snippet).
*/
public static RenderPipeline proceduralSkyboxFog(String name, Identifier fragmentShader) {
return PipelineTemplates.finish(fogBase(name)
.withVertexFormat(LoVisualVertexFormats.POS2, PrimitiveTopology.TRIANGLES)
.withVertexShader(LoVisualRenderPipelines.SHADER_DAMAGE_TINT_VERT)
.withFragmentShader(fragmentShader)
.withUniform("SkyboxShader", UniformType.UNIFORM_BUFFER), false, BlendFunction.TRANSLUCENT, false);
}
private static ExtendedRenderPipelineBuilder base(String name) {
return new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS)
.withLocation(PipelineTemplates.location(name));
}
private static ExtendedRenderPipelineBuilder fogBase(String name) {
return new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS, PipelineTemplates.FOG_UNIFORMS)
.withLocation(PipelineTemplates.location(name));
}
}

View file

@ -0,0 +1,28 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.blur;
import dev.loki.lovisual.config.values.mode.EnumValue;
public enum DepthOfFieldDepthSource implements EnumValue.IdProvider {
WORLD_SCENE("world_scene"),
MAIN("main"),
PRE_TRANSLUCENT("pre_translucent"),
OFF("off");
private final String id;
DepthOfFieldDepthSource(String id) {
this.id = id;
}
@Override
public String id() {
return id;
}
}

View file

@ -0,0 +1,33 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.blur;
import dev.loki.lovisual.config.values.mode.EnumValue;
public enum DepthOfFieldQuality implements EnumValue.IdProvider {
LOW("low", 8),
MEDIUM("medium", 14),
HIGH("high", 22);
private final String id;
private final int taps;
DepthOfFieldQuality(String id, int taps) {
this.id = id;
this.taps = taps;
}
@Override
public String id() {
return id;
}
public int taps() {
return taps;
}
}

View file

@ -0,0 +1,26 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.blur;
import dev.loki.lovisual.config.values.mode.EnumValue;
public enum MotionBlurMode implements EnumValue.IdProvider {
WORLD_AND_HAND("world_and_hand"),
WORLD_ONLY("world_only");
private final String id;
MotionBlurMode(String id) {
this.id = id;
}
@Override
public String id() {
return id;
}
}

View file

@ -0,0 +1,60 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.blur;
import com.mojang.blaze3d.pipeline.TextureTarget;
import com.mojang.blaze3d.textures.GpuTextureView;
import net.minecraft.client.Minecraft;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager;
public final class MotionBlurResources {
private TextureTarget previousColor;
private boolean previousValid;
public boolean ensure(Minecraft mc) {
if (mc == null || mc.getWindow() == null) return false;
return ensure(
mc.getWindow().getWidth(),
mc.getWindow().getHeight()
);
}
public boolean ensure(int width, int height) {
int w = Math.max(1, width);
int h = Math.max(1, height);
if (w <= 0 || h <= 0) return false;
TextureTarget next = LoVisualRenderSystem.resources().persistentFramebuffer(
"lovisual-motion-blur-previous-color", w, h, false, "MotionBlurResources");
if (previousColor != next) {
previousColor = next;
previousValid = false;
}
return true;
}
public GpuTextureView previousColorView() {
return previousColor != null ? previousColor.getColorTextureView() : null;
}
public boolean hasPreviousColor() {
return previousValid && previousColorView() != null;
}
public void captureCurrent(GpuTextureView src) {
GpuTextureView previous = previousColorView();
if (src == null || previous == null) return;
PostProcessManager.copy(src, previous);
previousValid = true;
}
public void invalidate() {
previousValid = false;
}
}

View file

@ -0,0 +1,78 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.graph;
import com.mojang.blaze3d.textures.GpuTextureView;
import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessContext;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass;
import dev.loki.lovisual.render.engine.rhi.resource.LoVisualRhi;
import java.util.Set;
import dev.loki.lovisual.render.engine.postprocess.graph.PostProcessGraphPass;
import dev.loki.lovisual.render.engine.postprocess.graph.resource.PostProcessGraphResources;
import dev.loki.lovisual.render.engine.postprocess.graph.resource.PostProcessResolution;
import dev.loki.lovisual.render.engine.postprocess.graph.resource.PostProcessResource;
public final class LegacyPostProcessGraphPass implements PostProcessGraphPass {
private final PostProcessPass delegate;
private final String id;
public LegacyPostProcessGraphPass(PostProcessPass delegate) {
this.delegate = delegate;
this.id = delegate.getClass().getName();
}
public PostProcessPass delegate() {
return delegate;
}
@Override
public String id() {
return id;
}
@Override
public int priority() {
return delegate.getPriority();
}
@Override
public PostProcessPass.Phase phase() {
return delegate.getPhase();
}
@Override
public Set<PostProcessResource> reads() {
return Set.of(PostProcessResource.GRAPH_SOURCE_COLOR);
}
@Override
public Set<PostProcessResource> writes() {
return Set.of(PostProcessResource.GRAPH_DEST_COLOR);
}
@Override
public PostProcessResolution resolution() {
return PostProcessResolution.FULL;
}
@Override
public boolean enabled(RenderFrameContext context) {
return delegate.isActive();
}
@Override
public boolean execute(RenderFrameContext context, LoVisualRhi rhi, PostProcessGraphResources resources) {
GpuTextureView src = resources.currentSource();
GpuTextureView dst = resources.currentDestination();
PostProcessContext legacyContext = resources.legacyContext();
if (src == null || dst == null || legacyContext == null) return false;
return delegate.render(legacyContext, src, dst);
}
}

View file

@ -0,0 +1,116 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.graph;
import com.mojang.blaze3d.textures.GpuTextureView;
import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass;
import dev.loki.lovisual.render.engine.profiler.frame.RenderCostProfiler;
import dev.loki.lovisual.render.engine.rhi.resource.LoVisualRhi;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import dev.loki.lovisual.render.engine.postprocess.graph.resource.HistoryBufferManager;
import dev.loki.lovisual.render.engine.postprocess.graph.LegacyPostProcessGraphPass;
import dev.loki.lovisual.render.engine.postprocess.graph.PostProcessGraphPass;
import dev.loki.lovisual.render.engine.postprocess.graph.resource.PostProcessGraphResources;
public final class PostProcessGraph implements AutoCloseable {
private final List<PostProcessGraphPass> passes = new ArrayList<>();
private final PostProcessGraphResources resources = new PostProcessGraphResources();
private final HistoryBufferManager history = new HistoryBufferManager();
public void add(PostProcessGraphPass pass) {
if (pass == null) return;
for (PostProcessGraphPass existing : passes) {
if (existing.getId().equals(pass.getId())) return;
}
passes.add(pass);
passes.sort(Comparator
.comparingInt(PostProcessGraphPass::priority)
.thenComparing(PostProcessGraphPass::id));
}
public void addLegacy(PostProcessPass pass) {
if (pass != null) add(new LegacyPostProcessGraphPass(pass));
}
public boolean hasActivePass(PostProcessPass.Phase phase, RenderFrameContext context) {
for (PostProcessGraphPass pass : passes) {
if (pass.phase() == phase && pass.enabled(context)) return true;
}
return false;
}
/**
* Executes one postprocess phase as a ping-pong graph.
* <p>
* Flow:
* main color -> graph source -> active passes -> final graph source -> main color
*/
public boolean execute(PostProcessPass.Phase phase,
float tickDelta,
RenderFrameContext context,
LoVisualRhi rhi,
GraphCopy copy) {
if (!hasActivePass(phase, context)) return false;
try (RenderCostProfiler.Scope ignoredGraph = RenderCostProfiler.postPass("graph:", phase.toString())) {
if (!resources.prepare(phase, tickDelta)) return false;
GpuTextureView mainColor = resources.mainColor();
GpuTextureView source = resources.currentSource();
if (mainColor == null || source == null) return false;
copy.copy(mainColor, source);
resources.resetPingPong();
boolean anyApplied = false;
for (PostProcessGraphPass pass : passes) {
if (pass.phase() != phase || !pass.enabled(context)) continue;
boolean applied;
try (RenderCostProfiler.Scope ignoredPass = RenderCostProfiler.postPass(pass.getId())) {
applied = pass.execute(context, rhi, resources);
}
if (applied) {
anyApplied = true;
resources.advancePingPong();
}
}
if (!anyApplied) return false;
GpuTextureView finalColor = resources.finalColor();
if (finalColor != null && mainColor != null) {
copy.copy(finalColor, mainColor);
}
return true;
}
}
public HistoryBufferManager history() {
return history;
}
public List<PostProcessGraphPass> passes() {
return List.copyOf(passes);
}
public PostProcessGraphResources resources() {
return resources;
}
@Override
public void close() {
resources.close();
}
@FunctionalInterface
public interface GraphCopy {
void copy(GpuTextureView src, GpuTextureView dst);
}
}

View file

@ -0,0 +1,42 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.graph;
import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass;
import dev.loki.lovisual.render.engine.rhi.resource.LoVisualRhi;
import java.util.Set;
import dev.loki.lovisual.render.engine.postprocess.graph.resource.PostProcessGraphResources;
import dev.loki.lovisual.render.engine.postprocess.graph.resource.PostProcessResolution;
import dev.loki.lovisual.render.engine.postprocess.graph.resource.PostProcessResource;
public interface PostProcessGraphPass {
String id();
default String getId() {
return id();
}
int priority();
PostProcessPass.Phase phase();
Set<PostProcessResource> reads();
Set<PostProcessResource> writes();
PostProcessResolution resolution();
boolean enabled(RenderFrameContext context);
/**
* Returns true when the pass wrote its destination and the graph should advance ping-pong state.
*/
boolean execute(RenderFrameContext context, LoVisualRhi rhi, PostProcessGraphResources resources);
}

View file

@ -0,0 +1,37 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.graph.resource;
import com.mojang.blaze3d.textures.GpuTextureView;
import org.jetbrains.annotations.Nullable;
/**
* Owner for temporal/history resources used by motion blur and future temporal effects.
*/
public final class HistoryBufferManager {
private @Nullable GpuTextureView previousColor;
private long previousFrameId = -1L;
public void update(long frameId, @Nullable GpuTextureView finalColor) {
previousFrameId = frameId;
previousColor = finalColor;
}
public @Nullable GpuTextureView previousColor() {
return previousColor;
}
public long previousFrameId() {
return previousFrameId;
}
public void invalidate() {
previousColor = null;
previousFrameId = -1L;
}
}

View file

@ -0,0 +1,165 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.graph.resource;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.pipeline.TextureTarget;
import com.mojang.blaze3d.textures.GpuTextureView;
import net.minecraft.client.Minecraft;
import org.jetbrains.annotations.Nullable;
import dev.loki.lovisual.features.module.lifecycle.Modules;
import dev.loki.lovisual.features.module.modules.visuals.scene.visual.ReimaginedVisual;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
import dev.loki.lovisual.render.engine.depth.PreTranslucentDepth;
import dev.loki.lovisual.render.engine.depth.WorldSceneDepth;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessContext;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass;
import dev.loki.lovisual.render.iris.scene.IrisSceneDepth;
import java.util.EnumMap;
import dev.loki.lovisual.render.engine.rhi.resource.manager.RenderResourceManager;
import dev.loki.lovisual.render.engine.postprocess.graph.PostProcessGraph;
import dev.loki.lovisual.render.engine.postprocess.graph.resource.PostProcessResource;
public final class PostProcessGraphResources implements AutoCloseable {
private final EnumMap<PostProcessResource, GpuTextureView> views = new EnumMap<>(PostProcessResource.class);
private @Nullable RenderTarget mainFramebuffer;
private @Nullable TextureTarget ping;
private @Nullable TextureTarget pong;
private @Nullable GpuTextureView pingView;
private @Nullable GpuTextureView pongView;
private @Nullable PostProcessContext legacyContext;
private boolean usePingAsSource = true;
private boolean prepared;
public boolean prepare(PostProcessPass.Phase phase, float tickDelta) {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.getWindow() == null) return false;
RenderTarget main = mc.gameRenderer.mainRenderTarget();
if (main == null) return false;
int w = Math.max(1, mc.getWindow().getWidth());
int h = Math.max(1, mc.getWindow().getHeight());
ensurePingPong(w, h);
if (ping == null || pong == null) return false;
pingView = ping.getColorTextureView();
pongView = pong.getColorTextureView();
if (main.getColorTextureView() == null || pingView == null || pongView == null) return false;
clear();
this.mainFramebuffer = main;
GpuTextureView mainDepth = IrisSceneDepth.isValid()
? IrisSceneDepth.mainDepthView()
: WorldSceneDepth.hasMain() ? WorldSceneDepth.mainDepthView() : main.getDepthTextureView();
GpuTextureView preTranslucentDepth = IrisSceneDepth.isValid()
? IrisSceneDepth.preTranslucentDepthView()
: needsPreTranslucentDepth() ? PreTranslucentDepth.getDepthView() : null;
GpuTextureView staticWorldDepth = IrisSceneDepth.isValid()
? null
: WorldSceneDepth.hasItemEntity() ? WorldSceneDepth.itemEntityDepthView() : null;
put(PostProcessResource.MAIN_COLOR, main.getColorTextureView());
put(PostProcessResource.MAIN_DEPTH, mainDepth);
put(PostProcessResource.PRE_TRANSLUCENT_DEPTH, preTranslucentDepth);
put(PostProcessResource.GRAPH_SOURCE_COLOR, pingView);
put(PostProcessResource.GRAPH_DEST_COLOR, pongView);
this.legacyContext = new PostProcessContext(
phase,
tickDelta,
main,
main.getColorTextureView(),
mainDepth,
preTranslucentDepth,
staticWorldDepth,
main.width,
main.height
);
this.usePingAsSource = true;
this.prepared = true;
return true;
}
public void resetPingPong() {
usePingAsSource = true;
put(PostProcessResource.GRAPH_SOURCE_COLOR, pingView);
put(PostProcessResource.GRAPH_DEST_COLOR, pongView);
}
public void advancePingPong() {
usePingAsSource = !usePingAsSource;
put(PostProcessResource.GRAPH_SOURCE_COLOR, currentSource());
put(PostProcessResource.GRAPH_DEST_COLOR, currentDestination());
}
public @Nullable GpuTextureView currentSource() {
return usePingAsSource ? pingView : pongView;
}
public @Nullable GpuTextureView currentDestination() {
return usePingAsSource ? pongView : pingView;
}
public @Nullable GpuTextureView finalColor() {
return currentSource();
}
public boolean isPrepared() {
return prepared;
}
public @Nullable RenderTarget mainFramebuffer() {
return mainFramebuffer;
}
public @Nullable GpuTextureView mainColor() {
return get(PostProcessResource.MAIN_COLOR);
}
public @Nullable PostProcessContext legacyContext() {
return legacyContext;
}
public void put(PostProcessResource resource, @Nullable GpuTextureView view) {
if (view != null) views.put(resource, view);
}
public @Nullable GpuTextureView get(PostProcessResource resource) {
return views.get(resource);
}
public void clear() {
views.clear();
mainFramebuffer = null;
legacyContext = null;
prepared = false;
}
private void ensurePingPong(int w, int h) {
ping = LoVisualRenderSystem.resources().persistentFramebuffer(
"lovisual-postprocess-graph-ping", w, h, false, "PostProcessGraph");
pong = LoVisualRenderSystem.resources().persistentFramebuffer(
"lovisual-postprocess-graph-pong", w, h, false, "PostProcessGraph");
}
private static boolean needsPreTranslucentDepth() {
ReimaginedVisual module = Modules.get(ReimaginedVisual.class);
return module != null && module.needsPreTranslucentDepthCapture();
}
@Override
public void close() {
// Framebuffers are owned by RenderResourceManager. This object only drops references.
ping = null;
pong = null;
pingView = null;
pongView = null;
clear();
}
}

View file

@ -0,0 +1,15 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.graph.resource;
public enum PostProcessResolution {
FULL,
HALF,
QUARTER,
CUSTOM
}

View file

@ -0,0 +1,19 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.graph.resource;
public enum PostProcessResource {
MAIN_COLOR,
MAIN_DEPTH,
PRE_TRANSLUCENT_DEPTH,
HISTORY_COLOR,
GRAPH_SOURCE_COLOR,
GRAPH_DEST_COLOR,
TEMP_COLOR,
CUSTOM
}

View file

@ -0,0 +1,26 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.post;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.textures.GpuTextureView;
import org.jetbrains.annotations.Nullable;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass;
public record PostProcessContext(
PostProcessPass.Phase phase,
float tickDelta,
RenderTarget mainFramebuffer,
GpuTextureView mainColor,
@Nullable GpuTextureView mainDepth,
@Nullable GpuTextureView preTranslucentDepth,
@Nullable GpuTextureView staticWorldDepth,
int width,
int height
) {
}

View file

@ -0,0 +1,119 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.post;
import com.mojang.blaze3d.ProjectionType;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.AddressMode;
import com.mojang.blaze3d.textures.FilterMode;
import com.mojang.blaze3d.textures.GpuSampler;
import com.mojang.blaze3d.textures.GpuTextureView;
import net.minecraft.client.Minecraft;
import org.joml.Matrix4f;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.postprocess.graph.PostProcessGraph;
import dev.loki.lovisual.render.engine.profiler.frame.RenderCostProfiler;
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
import dev.loki.lovisual.render.engine.rhi.command.FullscreenDrawCommand;
import dev.loki.lovisual.render.engine.uniform.impl.post.PostProcessUniforms;
import dev.loki.lovisual.Lifecycle;
import java.util.OptionalDouble;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass;
public enum PostProcessManager {
;
private static final PostProcessGraph GRAPH = new PostProcessGraph();
private static GpuSampler sampler;
public static void register(PostProcessPass pass) {
GRAPH.addLegacy(pass);
}
public static PostProcessGraph graph() {
return GRAPH;
}
public static void renderAll(PostProcessPass.Phase phase, float tickDelta) {
if (!Lifecycle.canRunRender()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.gameRenderer.mainRenderTarget() == null) return;
FullScreenRenderer.ensureInit();
ensureSampler();
var mv = RenderSystem.getModelViewStack();
Matrix4f previousProjection = MeshRenderer.projection();
GpuBufferSlice previousProjectionBuffer = RenderSystem.getProjectionMatrixBuffer();
ProjectionType previousProjectionType = RenderSystem.getProjectionType();
boolean previousRendering3D = RenderState.rendering3D;
mv.pushMatrix();
mv.identity();
MeshRenderer.setProjection(new Matrix4f().identity());
RenderState.rendering3D = false;
try {
GRAPH.execute(
phase,
tickDelta,
LoVisualRenderSystem.ensureFrameContext(),
LoVisualRenderSystem.rhi(),
PostProcessManager::copy
);
} finally {
MeshRenderer.setProjection(previousProjection);
if (previousProjectionBuffer != null && previousProjectionType != null) {
RenderSystem.setProjectionMatrix(previousProjectionBuffer, previousProjectionType);
}
RenderState.rendering3D = previousRendering3D;
mv.popMatrix();
}
}
private static void ensureSampler() {
if (sampler != null) return;
sampler = RenderSystem.getDevice().createSampler(
AddressMode.CLAMP_TO_EDGE,
AddressMode.CLAMP_TO_EDGE,
FilterMode.LINEAR,
FilterMode.LINEAR,
1,
OptionalDouble.empty()
);
}
public static GpuSampler getSampler() {
ensureSampler();
return sampler;
}
public static void copy(GpuTextureView src, GpuTextureView dst) {
try (RenderCostProfiler.Scope ignored = RenderCostProfiler.postPass("copy")) {
if (src == null || dst == null || src == dst) return;
if (LoVisualRenderSystem.rhi().textureBlitter().copyFast(src, dst)) {
return;
}
ensureSampler();
PostProcessUniforms.update(0.0f, 0.0f, 0.0f, 0.0f);
LoVisualRenderSystem.rhi().drawFullscreen(
FullscreenDrawCommand.builder("LoVisual PostProcess Copy")
.colorAttachment(dst)
.pipeline(LoVisualRenderPipelines.DAMAGE_TINT)
.uniform("PostProcess", PostProcessUniforms.get())
.sampler("u_Texture", src, sampler)
.build()
);
LoVisualRenderSystem.rhi().stats().textureShaderCopy();
}
}
}

View file

@ -0,0 +1,31 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.post;
import com.mojang.blaze3d.textures.GpuTextureView;
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessContext;
public interface PostProcessPass {
boolean isActive();
boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta);
default boolean render(PostProcessContext context, GpuTextureView src, GpuTextureView dst) {
return render(src, dst, context != null ? context.tickDelta() : 0.0f);
}
default int getPriority() {
return 0;
}
default Phase getPhase() {
return Phase.PRE_HAND;
}
enum Phase {PRE_HAND, POST_HAND}
}

View file

@ -0,0 +1,131 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.render;
import dev.loki.lovisual.features.theme.Theme;
import com.mojang.blaze3d.ProjectionType;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
import net.minecraft.util.Util;
import org.joml.Matrix4f;
import dev.loki.lovisual.config.i18n.MainConfig;
import dev.loki.lovisual.features.theme.Themes;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
import dev.loki.lovisual.render.engine.uniform.impl.ui.MenuBackgroundUniforms;
import dev.loki.lovisual.features.module.lifecycle.Modules;
import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer;
public enum MenuBackgroundRenderer {
;
private static final Matrix4f IDENTITY = new Matrix4f();
private static final int DEFAULT_ACCENT_RGB = 0x5CC8E7;
private static final int DEFAULT_BG_RGB = 0x0E1720;
private static ProjectionMatrixBuffer projection;
private static boolean deferredPending;
private static boolean deferredAurora;
public static void render(Minecraft mc) {
render(mc, false);
}
public static void render(Minecraft mc, boolean aurora) {
if (mc == null) return;
FpsOptimizer fpsOptimizer = Modules.get(FpsOptimizer.class);
if (fpsOptimizer != null && fpsOptimizer.isMenuBackgroundDisabled()) return;
if (Renderer2D.isDeferredExtractRecording()) {
deferredPending = true;
deferredAurora = aurora;
return;
}
renderNow(mc, aurora);
}
public static void drainDeferred(Minecraft mc) {
if (!deferredPending) return;
boolean aurora = deferredAurora;
deferredPending = false;
renderNow(mc, aurora);
}
private static void renderNow(Minecraft mc, boolean aurora) {
if (mc == null) return;
RenderTarget fb = mc.gameRenderer.mainRenderTarget();
if (fb == null) return;
int w = mc.getWindow().getWidth();
int h = mc.getWindow().getHeight();
if (w <= 0 || h <= 0) return;
FullScreenRenderer.ensureInit();
float time = (float) (Util.getMillis() / 1000.0);
int accentRgb = DEFAULT_ACCENT_RGB;
int bgRgb = DEFAULT_BG_RGB;
MainConfig cfg = MainConfig.get();
if (cfg != null && cfg.isMenuBackgroundUseTheme()) {
Themes.Theme theme = Theme.theme();
if (theme != null) {
accentRgb = theme.accent() & 0x00FFFFFF;
bgRgb = theme.windowBg() & 0x00FFFFFF;
}
}
MenuBackgroundUniforms.update(w, h, time, accentRgb, bgRgb);
var mv = RenderSystem.getModelViewStack();
boolean pushedModelView = false;
GpuBufferSlice prevProj = RenderSystem.getProjectionMatrixBuffer();
ProjectionType prevProjType = RenderSystem.getProjectionType();
Matrix4f prevMeshProjection = MeshRenderer.projection();
boolean prevRendering3D = RenderState.rendering3D;
try {
mv.pushMatrix();
pushedModelView = true;
mv.identity();
if (projection == null) {
projection = new ProjectionMatrixBuffer("lovisual-menu-bg-projection");
}
Matrix4f proj = IDENTITY.identity();
RenderSystem.setProjectionMatrix(projection.getBuffer(proj), ProjectionType.PERSPECTIVE);
MeshRenderer.setProjection(proj);
RenderState.rendering3D = false;
var pipeline = aurora
? LoVisualRenderPipelines.MENU_BACKGROUND_AURORA
: LoVisualRenderPipelines.MENU_BACKGROUND_WAVES;
FullScreenRenderer.begin("LoVisual Fullscreen Pass")
.attachment(fb)
.pipeline(pipeline)
.uniform("MenuBackground", MenuBackgroundUniforms.get())
.end();
} finally {
if (pushedModelView) {
mv.popMatrix();
}
MeshRenderer.setProjection(prevMeshProjection);
RenderState.rendering3D = prevRendering3D;
if (prevProj != null && prevProjType != null) {
RenderSystem.setProjectionMatrix(prevProj, prevProjType);
}
}
}
}

View file

@ -0,0 +1,194 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.render;
import com.mojang.blaze3d.ProjectionType;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
import dev.loki.lovisual.render.engine.uniform.impl.sky.postprocess.SkyShaderNebulaUniforms;
import dev.loki.lovisual.render.engine.uniform.impl.sky.postprocess.SkyShaderUniforms;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
import net.minecraft.util.Util;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
/**
* Fullscreen SkyShader pass: sky / water / caustic procedural GLSL effect drawn behind the world,
* with a camera-direction-driven ray so it reads correctly regardless of look direction.
*/
public enum SkyShaderRenderer {
;
public enum Mode {
SKY, WATER, CAUSTIC, NEBULA
}
private static final Matrix4f IDENTITY = new Matrix4f();
private static ProjectionMatrixBuffer projection;
private static long startMillis = -1L;
public static void reset() {
startMillis = -1L;
}
public static void render(Minecraft mc, Mode mode, float speed, float scale, float intensity,
float alpha, int colorRgb) {
if (mc == null || mc.level == null || mc.player == null) return;
RenderTarget fb = mc.gameRenderer.mainRenderTarget();
if (fb == null) return;
int w = mc.getWindow().getWidth();
int h = mc.getWindow().getHeight();
if (w <= 0 || h <= 0) return;
FullScreenRenderer.ensureInit();
if (startMillis < 0) startMillis = Util.getMillis();
float time = (Util.getMillis() - startMillis) / 1000.0f;
float cr = ((colorRgb >>> 16) & 0xFF) / 255f;
float cg = ((colorRgb >>> 8) & 0xFF) / 255f;
float cb = (colorRgb & 0xFF) / 255f;
Camera cam = mc.gameRenderer.mainCamera();
float yawRad = (float) Math.toRadians(-cam.yRot());
float pitchRad = (float) Math.toRadians(cam.xRot());
float fov = (float) mc.options.fov().get().intValue();
SkyShaderUniforms.update(time, speed, scale, intensity, w, h, alpha, fov, cr, cg, cb, yawRad, pitchRad);
var mv = RenderSystem.getModelViewStack();
boolean pushedModelView = false;
GpuBufferSlice prevProj = RenderSystem.getProjectionMatrixBuffer();
ProjectionType prevProjType = RenderSystem.getProjectionType();
Matrix4f prevMeshProjection = MeshRenderer.projection();
boolean prevRendering3D = RenderState.rendering3D;
try {
mv.pushMatrix();
pushedModelView = true;
mv.identity();
if (projection == null) {
projection = new ProjectionMatrixBuffer("lovisual-sky-shader-projection");
}
Matrix4f proj = IDENTITY.identity();
RenderSystem.setProjectionMatrix(projection.getBuffer(proj), ProjectionType.PERSPECTIVE);
MeshRenderer.setProjection(proj);
RenderState.rendering3D = false;
RenderPipeline pipeline = switch (mode) {
case WATER -> LoVisualRenderPipelines.SKY_SHADER_WATER;
case CAUSTIC -> LoVisualRenderPipelines.SKY_SHADER_CAUSTIC;
default -> LoVisualRenderPipelines.SKY_SHADER_SKY;
};
FullScreenRenderer.begin("LoVisual Fullscreen Pass")
.attachment(fb)
.pipeline(pipeline)
.uniform("SkyShader", SkyShaderUniforms.get())
.end();
} finally {
if (pushedModelView) {
mv.popMatrix();
}
MeshRenderer.setProjection(prevMeshProjection);
RenderState.rendering3D = prevRendering3D;
if (prevProj != null && prevProjType != null) {
RenderSystem.setProjectionMatrix(prevProj, prevProjType);
}
}
}
/** Domain-warped nebula sky (quaternion-based ray dir instead of yaw/pitch). */
public static void renderNebula(Minecraft mc, float speed, float intensity, float alpha,
int nebColor1Rgb, int nebColor2Rgb, int starColorRgb) {
if (mc == null || mc.level == null || mc.player == null) return;
RenderTarget fb = mc.gameRenderer.mainRenderTarget();
if (fb == null) return;
int w = mc.getWindow().getWidth();
int h = mc.getWindow().getHeight();
if (w <= 0 || h <= 0) return;
FullScreenRenderer.ensureInit();
if (startMillis < 0) startMillis = Util.getMillis();
float time = (Util.getMillis() - startMillis) / 1000.0f;
Camera cam = mc.gameRenderer.mainCamera();
Quaternionf rot = cam.rotation();
float fov = (float) mc.options.fov().get().intValue();
SkyShaderNebulaUniforms.update(w, h, time, speed, intensity, alpha, fov,
rot.x(), rot.y(), rot.z(), rot.w(),
channelR(nebColor1Rgb), channelG(nebColor1Rgb), channelB(nebColor1Rgb),
channelR(nebColor2Rgb), channelG(nebColor2Rgb), channelB(nebColor2Rgb),
channelR(starColorRgb), channelG(starColorRgb), channelB(starColorRgb));
var mv = RenderSystem.getModelViewStack();
boolean pushedModelView = false;
GpuBufferSlice prevProj = RenderSystem.getProjectionMatrixBuffer();
ProjectionType prevProjType = RenderSystem.getProjectionType();
Matrix4f prevMeshProjection = MeshRenderer.projection();
boolean prevRendering3D = RenderState.rendering3D;
try {
mv.pushMatrix();
pushedModelView = true;
mv.identity();
if (projection == null) {
projection = new ProjectionMatrixBuffer("lovisual-sky-shader-projection");
}
Matrix4f proj = IDENTITY.identity();
RenderSystem.setProjectionMatrix(projection.getBuffer(proj), ProjectionType.PERSPECTIVE);
MeshRenderer.setProjection(proj);
RenderState.rendering3D = false;
FullScreenRenderer.begin("LoVisual Fullscreen Pass")
.attachment(fb)
.pipeline(LoVisualRenderPipelines.SKY_SHADER_NEBULA)
.uniform("SkyShaderData", SkyShaderNebulaUniforms.get())
.end();
} finally {
if (pushedModelView) {
mv.popMatrix();
}
MeshRenderer.setProjection(prevMeshProjection);
RenderState.rendering3D = prevRendering3D;
if (prevProj != null && prevProjType != null) {
RenderSystem.setProjectionMatrix(prevProj, prevProjType);
}
}
}
private static float channelR(int rgb) {
return ((rgb >>> 16) & 0xFF) / 255f;
}
private static float channelG(int rgb) {
return ((rgb >>> 8) & 0xFF) / 255f;
}
private static float channelB(int rgb) {
return (rgb & 0xFF) / 255f;
}
}

View file

@ -0,0 +1,138 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.postprocess.render;
import com.mojang.blaze3d.ProjectionType;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.sky.CustomSkyboxRenderer;
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
import dev.loki.lovisual.render.engine.uniform.impl.sky.postprocess.SkyShaderStarryUniforms;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
import net.minecraft.util.Util;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
/**
* Fullscreen "Starry" sky pass: procedural star field + milky way band + a round moon
* placed opposite the sun (driven by the world day cycle). Mirrors the scaffolding of
* {@link SkyShaderRenderer#renderNebula}; split out to keep both files within the size limit.
*/
public enum SkyShaderStarryRenderer {
;
private static final Matrix4f IDENTITY = new Matrix4f();
private static ProjectionMatrixBuffer projection;
private static long startMillis = -1L;
public static void reset() {
startMillis = -1L;
}
public static void render(Minecraft mc, float speed, float intensity, float alpha,
float starDensity, float milkyWayIntensity, float moonSize,
boolean showMoon, int moonColorArgb, int starColorArgb) {
if (mc == null || mc.level == null || mc.player == null) return;
RenderTarget fb = mc.gameRenderer.mainRenderTarget();
if (fb == null) return;
int w = mc.getWindow().getWidth();
int h = mc.getWindow().getHeight();
if (w <= 0 || h <= 0) return;
FullScreenRenderer.ensureInit();
if (startMillis < 0) startMillis = Util.getMillis();
float time = (Util.getMillis() - startMillis) / 1000.0f;
Camera cam = mc.gameRenderer.mainCamera();
Quaternionf rot = cam.rotation();
float fov = (float) mc.options.fov().get().intValue();
float[] moonDir = moonDirection(mc);
if (!showMoon) {
// The shader clamps the angular radius, so hiding the moon means pointing it
// below the horizon where horizonFade discards it.
moonDir = new float[]{0f, -1f, 0f};
}
float moonPhase = ((WorldTweaks.getServerTimeOfDay() / 24000f) % 8f) / 8f;
SkyShaderStarryUniforms.update(w, h, time, speed, intensity, alpha, fov,
rot.x(), rot.y(), rot.z(), rot.w(),
moonDir[0], moonDir[1], moonDir[2], moonSize,
moonPhase, milkyWayIntensity, starDensity,
channel(moonColorArgb, 16), channel(moonColorArgb, 8), channel(moonColorArgb, 0),
channel(starColorArgb, 16), channel(starColorArgb, 8), channel(starColorArgb, 0));
var mv = RenderSystem.getModelViewStack();
boolean pushedModelView = false;
GpuBufferSlice prevProj = RenderSystem.getProjectionMatrixBuffer();
ProjectionType prevProjType = RenderSystem.getProjectionType();
Matrix4f prevMeshProjection = MeshRenderer.projection();
boolean prevRendering3D = RenderState.rendering3D;
try {
mv.pushMatrix();
pushedModelView = true;
mv.identity();
if (projection == null) {
projection = new ProjectionMatrixBuffer("lovisual-sky-shader-starry-projection");
}
Matrix4f proj = IDENTITY.identity();
RenderSystem.setProjectionMatrix(projection.getBuffer(proj), ProjectionType.PERSPECTIVE);
MeshRenderer.setProjection(proj);
RenderState.rendering3D = false;
FullScreenRenderer.begin("LoVisual Fullscreen Pass")
.attachment(fb)
.pipeline(LoVisualRenderPipelines.SKY_SHADER_STARRY)
.uniform("SkyShaderStarry", SkyShaderStarryUniforms.get())
.end();
} finally {
if (pushedModelView) {
mv.popMatrix();
}
MeshRenderer.setProjection(prevMeshProjection);
RenderState.rendering3D = prevRendering3D;
if (prevProj != null && prevProjType != null) {
RenderSystem.setProjectionMatrix(prevProj, prevProjType);
}
}
}
/**
* World-space direction to the moon: opposite the sun along its day arc
* (CustomSkyboxRenderer sunDir = (-sin a, cos a, 0), so moon = (sin a, -cos a, 0)).
* Sun angle comes from the last rendered sky pass; fallback derives it from the day clock.
*
* @return normalized xyz
*/
private static float[] moonDirection(Minecraft mc) {
float sunAngleRad;
if (CustomSkyboxRenderer.hasLastSunAngle()) {
sunAngleRad = CustomSkyboxRenderer.getLastSunAngle();
} else {
float timeOfDay = (WorldTweaks.getServerTimeOfDay() % 24000L) / 24000f;
sunAngleRad = timeOfDay * (float) (Math.PI * 2.0);
}
float x = (float) Math.sin(sunAngleRad);
float y = (float) -Math.cos(sunAngleRad);
return new float[]{x, y, 0f};
}
private static float channel(int argb, int shift) {
return ((argb >>> shift) & 0xFF) / 255f;
}
}

View file

@ -0,0 +1,93 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.prewarm;
import dev.loki.lovisual.events.meta.EventHandler;
import dev.loki.lovisual.events.impl.render.RenderPrewarmCollectEvent;
import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.ModulesMenuCategory;
import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.MenuScreen;
import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement;
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry;
import dev.loki.lovisual.features.gui.hud.nondraggable.StaticHudElementRegistry;
import dev.loki.lovisual.features.module.core.Module;
import dev.loki.lovisual.features.module.core.ModuleManager;
import dev.loki.lovisual.render.engine.text.font.info.FontInfo;
public enum RenderPrewarmContributors {
INSTANCE;
@EventHandler(priority = 1000)
private void onCollect(RenderPrewarmCollectEvent event) {
collectCoreGui(event);
collectEnabledModules(event);
collectEnabledHud(event);
}
private static void collectCoreGui(RenderPrewarmCollectEvent event) {
event.font("Inter", FontInfo.Type.Regular)
.font("Inter", FontInfo.Type.Bold)
.font("Inter", FontInfo.Type.Italic)
.font("InterMedium", FontInfo.Type.Regular)
.font("OnestMedium", FontInfo.Type.Regular)
.font("Onest", FontInfo.Type.Regular)
.font("Onest", FontInfo.Type.Bold)
.font("OnestMedium", FontInfo.Type.Regular)
.font("OnestBold", FontInfo.Type.Regular)
.font("OnestLight", FontInfo.Type.Regular)
.font("Monsterrat", FontInfo.Type.Regular)
.font("Comfortaa", FontInfo.Type.Regular)
.font("Icons", FontInfo.Type.Regular)
.font("IconsNur", FontInfo.Type.Regular)
.font("GuiIcons", FontInfo.Type.Regular)
.font("RichIcons", FontInfo.Type.Regular);
for (ModulesMenuCategory category : ModulesMenuCategory.values()) {
event.svg(category.icon());
}
for (MenuScreen.Category category : MenuScreen.Category.values()) {
if (category.svgIcon()) {
event.svg(category.token());
}
}
event.svg("arrow")
.svg("check")
.svg("save")
.svg("x")
.svg("palette")
.svg("brush")
.svg("paintbrush")
.svg("copy")
.svg("trash")
.svg("folder-cog")
.svg("user-pen")
.svg("columns-3-cog");
}
private static void collectEnabledModules(RenderPrewarmCollectEvent event) {
for (Module module : ModuleManager.getModules()) {
if (module != null && module.isEnabled()) {
module.collectRenderPrewarm(event);
}
}
}
private static void collectEnabledHud(RenderPrewarmCollectEvent event) {
for (DraggableHudElement element : DraggableHudElementRegistry.getWidgets()) {
if (element != null && element.isEnabled()) {
element.collectRenderPrewarm(event);
}
}
for (AbstractHudElement element : StaticHudElementRegistry.getAll()) {
if (element != null && element.isEnabled()) {
element.collectRenderPrewarm(event);
}
}
}
}

View file

@ -0,0 +1,59 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.prewarm;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.render.RenderPrewarmCollectEvent;
import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts;
import dev.loki.lovisual.render.engine.svg.parser.SvgMsdfRegistry;
import dev.loki.lovisual.render.engine.text.font.Fonts;
import dev.loki.lovisual.Lifecycle;
import dev.loki.lovisual.util.logging.DebugLog;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.Identifier;
public enum RenderPrewarmManager {
;
public static void prewarm(String reason) {
if (!Lifecycle.isActive()) return;
RenderPrewarmCollectEvent event = new RenderPrewarmCollectEvent(reason);
Events.BUS.post(event);
UiScriptStats uiScripts = prewarmUiScripts();
if (event.isEmpty()) {
DebugLog.renderThread("[LoVisual][Prewarm] %s: ui-scripts=%d changed, %d cached, %d errors",
event.reason(), uiScripts.changed(), uiScripts.cached(), uiScripts.errors());
return;
}
int fonts = Fonts.prewarmRenderers(event.fonts());
int svg = 0;
for (Identifier id : event.svgMsdfIcons()) {
if (SvgMsdfRegistry.preload(id)) {
svg++;
}
}
DebugLog.renderThread("[LoVisual][Prewarm] %s: fonts=%d/%d svg-msdf=%d/%d ui-scripts=%d changed, %d cached, %d errors",
event.reason(), fonts, event.fonts().size(), svg, event.svgMsdfIcons().size(),
uiScripts.changed(), uiScripts.cached(), uiScripts.errors());
}
private static UiScriptStats prewarmUiScripts() {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.getResourceManager() == null) {
return new UiScriptStats(0, 0, 0);
}
var stats = HudScriptLayouts.prewarmRegistered(mc.getResourceManager());
return new UiScriptStats(stats.changed(), stats.unchanged(), stats.errors());
}
private record UiScriptStats(int changed, int cached, int errors) {
}
}

Some files were not shown because too many files have changed in this diff Show more