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,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.iris.compat;
public enum IrisCompatibilityFeature {
FOG_POLICY,
LIGHTING_POLICY,
HAND_RENDERING_POLICY,
DEFERRED_FINALIZATION_POLICY,
MOTION_BLUR_POLICY,
INTERACTION_POLICY
}

View file

@ -0,0 +1,45 @@
/*
* 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.iris.compat;
import dev.loki.lovisual.features.module.lifecycle.Modules;
import dev.loki.lovisual.features.module.modules.visuals.fx.MotionBlur;
import dev.loki.lovisual.Lifecycle;
import dev.loki.lovisual.features.module.modules.visuals.scene.visual.Chams;
import dev.loki.lovisual.render.iris.compat.IrisCompatibilityFeature;
import dev.loki.lovisual.render.iris.runtime.IrisRuntime;
public enum IrisCompatibilityGuards {
;
public static boolean suppressShaderpackMotionBlur() {
if (!Lifecycle.canRunShaderBridge()) return false;
MotionBlur module = Modules.get(MotionBlur.class);
return IrisRuntime.isShaderpackRendererActive()
&& IrisRuntime.supports(IrisCompatibilityFeature.MOTION_BLUR_POLICY)
&& module != null
&& module.isActive();
}
public static boolean suppressLoVisualTerrainShaderOverrides() {
if (!Lifecycle.canRunShaderBridge()) return false;
return IrisRuntime.isModLoaded();
}
public static boolean suppressIrisHandRendering() {
// Do not cancel Iris' own hand renderer. Iris redirects vanilla hand submit to a no-op
// while a shaderpack is active, so suppressing HandRenderer removes the visible hand entirely.
// Chams builds its mask as an extra pass and must not own the visible hand path.
return false;
}
public static boolean deferIrisFinalizationForSecondHandScene() {
if (!Lifecycle.canRunShaderBridge()) return false;
return IrisRuntime.isShaderpackRendererActive();
}
}

View file

@ -0,0 +1,65 @@
/*
* 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.iris.compat;
import java.util.EnumSet;
import java.util.Set;
import dev.loki.lovisual.render.iris.compat.IrisCompatibilityFeature;
public record IrisCompatibilityProfile(
String id,
String displayName,
String targetShaderpack,
Set<IrisCompatibilityFeature> features
) {
public static final IrisCompatibilityProfile NONE = new IrisCompatibilityProfile(
"none",
"No Iris profile",
"",
Set.of()
);
public static final IrisCompatibilityProfile GENERIC_IRIS = new IrisCompatibilityProfile(
"generic_iris",
"Generic Iris shaderpack",
"*",
EnumSet.of(IrisCompatibilityFeature.INTERACTION_POLICY)
);
public static final IrisCompatibilityProfile COMPLEMENTARY_REIMAGINED = new IrisCompatibilityProfile(
"complementary_reimagined",
"Complementary Reimagined",
"Complementary Reimagined",
EnumSet.of(
IrisCompatibilityFeature.FOG_POLICY,
IrisCompatibilityFeature.LIGHTING_POLICY,
IrisCompatibilityFeature.HAND_RENDERING_POLICY,
IrisCompatibilityFeature.DEFERRED_FINALIZATION_POLICY,
IrisCompatibilityFeature.MOTION_BLUR_POLICY,
IrisCompatibilityFeature.INTERACTION_POLICY
)
);
public static final IrisCompatibilityProfile PHOTON = new IrisCompatibilityProfile(
"photon",
"Photon",
"Photon",
EnumSet.of(
IrisCompatibilityFeature.FOG_POLICY,
IrisCompatibilityFeature.LIGHTING_POLICY,
IrisCompatibilityFeature.HAND_RENDERING_POLICY,
IrisCompatibilityFeature.MOTION_BLUR_POLICY,
IrisCompatibilityFeature.INTERACTION_POLICY
)
);
public String getId() {
return id;
}
public boolean supports(IrisCompatibilityFeature feature) {
return feature != null && features.contains(feature);
}
}

View file

@ -0,0 +1,51 @@
/*
* 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.iris.compat;
import java.util.Locale;
import dev.loki.lovisual.render.iris.compat.IrisCompatibilityProfile;
public enum IrisCompatibilityProfiles {
;
public static IrisCompatibilityProfile resolve(String shaderpackName, boolean shaderpackInUse) {
if (!shaderpackInUse) {
return IrisCompatibilityProfile.NONE;
}
if (isComplementaryReimagined(shaderpackName)) {
return IrisCompatibilityProfile.COMPLEMENTARY_REIMAGINED;
}
if (isPhoton(shaderpackName)) {
return IrisCompatibilityProfile.PHOTON;
}
return IrisCompatibilityProfile.GENERIC_IRIS;
}
private static boolean isComplementaryReimagined(String shaderpackName) {
if (shaderpackName == null || shaderpackName.isBlank()) {
return false;
}
String normalized = normalize(shaderpackName);
return normalized.contains("complementary")
&& (normalized.contains("reimagined") || normalized.contains("reimagined latest"));
}
private static boolean isPhoton(String shaderpackName) {
if (shaderpackName == null || shaderpackName.isBlank()) {
return false;
}
return normalize(shaderpackName).contains("photon");
}
private static String normalize(String shaderpackName) {
return shaderpackName.toLowerCase(Locale.ROOT)
.replace('-', ' ')
.replace('_', ' ')
.replace('+', ' ');
}
}

View file

@ -0,0 +1,66 @@
/*
* 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.iris.patch;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.io.Reader;
import java.util.List;
import java.util.Locale;
import java.util.Set;
public enum ShaderPatchCompiler {
;
public static PatchProgram parse(Reader source) throws IOException, CompileException {
return PatchParser.parse(source);
}
public static CompiledPatch compile(PatchProgram program, ImmutableList<String> source) throws CompileException {
return compile(program, source, CompileMode.PREFLIGHT);
}
public static CompiledPatch compile(PatchProgram program, ImmutableList<String> source, CompileMode mode) throws CompileException {
return compile(program, source, mode, Set.of());
}
public static CompiledPatch compile(PatchProgram program, ImmutableList<String> source, CompileMode mode, Set<ShaderStage> stages) throws CompileException {
return PatchCompileHelper.compile(program, source, mode, stages);
}
public static ImmutableList<String> apply(CompiledPatch patch, ImmutableList<String> source) {
return PatchCompileHelper.apply(patch, source);
}
public static String signature(String sourceLine) {
return PatchCompileHelper.signature(sourceLine);
}
public enum CompileMode { PREFLIGHT, APPLICATION }
public enum ShaderStage {
ANY, FRAGMENT, VERTEX, GEOMETRY, COMPUTE;
public static ShaderStage parse(String value) throws CompileException {
if (value == null || value.isBlank()) return ANY;
try { return ShaderStage.valueOf(value.trim().toUpperCase(Locale.ROOT)); }
catch (IllegalArgumentException e) { throw new CompileException("Unsupported shader stage '" + value + "'"); }
}
}
public enum ProbeType {
REQUIRE_HASH(true, "hash"), FORBID_HASH(false, "hash"),
REQUIRE_IDENT(true, "ident"), FORBID_IDENT(false, "ident"),
REQUIRE_FUNCTION(true, "function"), FORBID_FUNCTION(false, "function"),
REQUIRE_CALL(true, "call"), FORBID_CALL(false, "call"),
REQUIRE_UNIFORM(true, "uniform"), FORBID_UNIFORM(false, "uniform"),
REQUIRE_DEFINE(true, "define"), FORBID_DEFINE(false, "define");
private final boolean required; private final String label;
ProbeType(boolean required, String label) { this.required = required; this.label = label; }
boolean required() { return required; }
String label() { return label; }
}
public enum SelectorKind { HASH, IDENT, FUNCTION, FUNCTION_OPEN, DEFINE, DRAWBUFFERS, STAGE_BEGIN, ZONE }
public enum OperationType { INSERT_BEFORE, INSERT_AFTER, REPLACE_LINE, REMOVE_LINE, REPLACE_RANGE, REPLACE_BETWEEN }
public record PatchProgram(int schemaVersion, List<Probe> probes, List<Operation> operations) { }
public record CompiledPatch(List<Edit> edits) { }
public record Edit(int start, int end, List<String> payload) { }
public record Probe(ProbeType type, String value) { }
public record Selector(SelectorKind kind, String value, int occurrence, boolean requireUnique) { }
public record Operation(OperationType type, Selector anchor, Selector end, boolean includeEnd, List<String> payload) { }
public static final class CompileException extends Exception {
public CompileException(String message) { super(message); }
}
}

View file

@ -0,0 +1,123 @@
/*
* 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.iris.patch;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import dev.loki.lovisual.render.iris.patch.ShaderPatchEngine;
public final class ShaderPatchCompilerCli {
private static final Pattern INCLUDE = Pattern.compile("^\\s*#include\\s+\"([^\"]+)\"");
private ShaderPatchCompilerCli() {
}
static void main(String[] args) throws Exception {
if (args.length != 1) {
throw new IllegalArgumentException("Usage: validateIrisPatches -Pshaderpack=<shaderpack.zip>");
}
Path shaderpack = Path.of(args[0]).toAbsolutePath().normalize();
String shaderPackName = shaderpack.getFileName().toString();
try (ZipFile zip = new ZipFile(shaderpack.toFile())) {
String root = findShaderRoot(zip);
Map<String, ImmutableList<String>> sources = new HashMap<>();
List<String> targetPaths = ShaderPatchEngine.targetPaths(shaderPackName);
if (targetPaths.isEmpty()) {
throw new IllegalStateException("No shader patch manifest selected for " + shaderPackName);
}
for (String target : targetPaths) {
sources.put(target, ImmutableList.copyOf(expand(zip, root, target, new HashSet<>())));
}
ShaderPatchEngine.Session session = ShaderPatchEngine.newSession(shaderPackName);
for (String target : targetPaths) {
session.patch(target, sources.get(target), sources::get);
}
}
List<String> diagnostics = ShaderPatchEngine.diagnostics();
diagnostics.forEach(System.out::println);
boolean applied = diagnostics.stream().anyMatch(line -> line.contains(": applied"));
boolean rejected = diagnostics.stream().anyMatch(line -> line.contains("rejected"));
if (!applied || rejected) {
throw new IllegalStateException("No complete shader patch manifest compiled successfully");
}
}
private static List<String> expand(ZipFile zip, String root, String path, Set<String> stack) throws IOException {
String normalized = normalizePath(path);
if (!stack.add(normalized)) {
throw new IOException("Shader include cycle at " + normalized);
}
ZipEntry entry = zip.getEntry(root + "shaders" + normalized);
if (entry == null) {
throw new IOException("Missing shader source " + normalized);
}
String text = new String(zip.getInputStream(entry).readAllBytes(), StandardCharsets.UTF_8);
ArrayList<String> output = new ArrayList<>();
for (String line : text.split("\\R")) {
Matcher matcher = INCLUDE.matcher(line);
if (matcher.find()) {
output.addAll(expand(zip, root, resolveInclude(normalized, matcher.group(1)), stack));
} else {
output.add(line);
}
}
stack.remove(normalized);
return output;
}
private static String findShaderRoot(ZipFile zip) throws IOException {
return zip.stream()
.map(ZipEntry::getName)
.filter(name -> name.endsWith("shaders/shaders.properties"))
.map(name -> name.substring(0, name.length() - "shaders/shaders.properties".length()))
.min(java.util.Comparator.comparingInt(String::length))
.orElseThrow(() -> new IOException("Archive does not contain shaders/shaders.properties"));
}
private static String normalizePath(String path) {
String normalized = path.replace('\\', '/');
String[] parts = normalized.split("/");
ArrayList<String> clean = new ArrayList<>();
for (String part : parts) {
if (part.isBlank() || part.equals(".")) {
continue;
}
if (part.equals("..")) {
if (!clean.isEmpty()) {
clean.remove(clean.size() - 1);
}
continue;
}
clean.add(part);
}
return "/" + String.join("/", clean);
}
private static String resolveInclude(String currentPath, String includePath) {
if (includePath.startsWith("/")) {
return normalizePath(includePath);
}
String current = normalizePath(currentPath);
int separator = current.lastIndexOf('/');
String parent = separator <= 0 ? "/" : current.substring(0, separator + 1);
return normalizePath(parent + includePath);
}
}

View file

@ -0,0 +1,506 @@
/*
* 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.iris.patch;
import com.google.common.collect.ImmutableList;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import dev.loki.lovisual.util.logging.DebugLog;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import dev.loki.lovisual.render.iris.patch.ShaderPatchCompiler;
public enum ShaderPatchEngine {
;
private static final String INDEX_RESOURCE = "assets/lovisual/shaders/iris-patches/index.json";
private static final CopyOnWriteArrayList<String> EXTRA_MANIFEST_RESOURCES = new CopyOnWriteArrayList<>();
private static final AtomicReference<Repository> REPOSITORY = new AtomicReference<>(Repository.load(List.of()));
private static final Map<String, String> DIAGNOSTICS = new ConcurrentHashMap<>();
private static final ThreadLocal<String> LOADING_SHADER_PACK_NAME = new ThreadLocal<>();
private static final AtomicReference<String> GLOBAL_LOADING_SHADER_PACK_NAME = new AtomicReference<>();
/**
* Legacy validation session: all repository manifests are visible. Runtime Iris hooks should use
* {@link #newSession(String)} so LoVisual does not probe unrelated shaderpacks.
*/
public static Session newSession() {
Repository repository = repository();
return new Session("<all>", repository.targetsByPath, Set.copyOf(repository.manifestsById.keySet()), false);
}
public static Session newSession(String shaderPackName) {
Repository.Selection selection = repository().select(shaderPackName);
return new Session(shaderPackName, selection.targetsByPath(), selection.manifestIds(), selection.nameGated());
}
public static boolean registerManifestResource(String manifestResourcePath) {
String normalized = normalizeResourcePath(manifestResourcePath);
if (normalized.isBlank()) return false;
if (EXTRA_MANIFEST_RESOURCES.contains(normalized)) return true;
EXTRA_MANIFEST_RESOURCES.add(normalized);
REPOSITORY.set(Repository.load(List.copyOf(EXTRA_MANIFEST_RESOURCES)));
DebugLog.info("[IrisPatch] addon manifest registered: %s", normalized);
return true;
}
public static void beginShaderPackLoad(String shaderPackName) {
if (shaderPackName == null || shaderPackName.isBlank()) {
LOADING_SHADER_PACK_NAME.remove();
return;
}
LOADING_SHADER_PACK_NAME.set(shaderPackName);
GLOBAL_LOADING_SHADER_PACK_NAME.set(shaderPackName);
DebugLog.info("[IrisPatch] shaderpack load begin: '%s'", shaderPackName);
}
public static void endShaderPackLoad(String shaderPackName) {
String active = LOADING_SHADER_PACK_NAME.get();
if (active != null && (shaderPackName == null || active.equals(shaderPackName))) {
LOADING_SHADER_PACK_NAME.remove();
}
String global = GLOBAL_LOADING_SHADER_PACK_NAME.get();
if (global != null && (shaderPackName == null || global.equals(shaderPackName))) {
GLOBAL_LOADING_SHADER_PACK_NAME.compareAndSet(global, null);
}
DebugLog.info("[IrisPatch] shaderpack load end: '%s'", shaderPackName == null ? "" : shaderPackName);
}
public static String loadingShaderPackName() {
String threadLocal = LOADING_SHADER_PACK_NAME.get();
if (threadLocal != null && !threadLocal.isBlank()) {
return threadLocal;
}
return GLOBAL_LOADING_SHADER_PACK_NAME.get();
}
public static List<String> diagnostics() {
return DIAGNOSTICS.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> entry.getKey() + ": " + entry.getValue())
.toList();
}
public static List<String> targetPaths() {
return repository().targetsByPath.keySet().stream().sorted().toList();
}
public static List<String> targetPaths(String shaderPackName) {
return repository().select(shaderPackName).targetsByPath.keySet().stream().sorted().toList();
}
private static Repository repository() {
return REPOSITORY.get();
}
private static void setDiagnostic(Target target, String value) {
DIAGNOSTICS.put(diagnosticKey(target), value);
}
private static boolean contains(List<String> lines, String marker) {
if (marker == null || marker.isBlank()) {
return false;
}
for (String line : lines) {
if (line.contains(marker)) {
return true;
}
}
return false;
}
private static String diagnosticKey(Target target) {
return target.manifestId + " " + target.path;
}
private static String normalizePath(String path) {
String normalized = path.replace('\\', '/');
return normalized.startsWith("/") ? normalized : "/" + normalized;
}
private static String normalizeResourcePath(String path) {
if (path == null || path.isBlank()) return "";
String normalized = path.trim().replace('\\', '/');
return normalized.startsWith("/") ? normalized.substring(1) : normalized;
}
@FunctionalInterface
public interface SourceProvider {
ImmutableList<String> load(String path);
}
public static final class Session {
private final String shaderPackName;
private final Map<String, List<Target>> targetsByPath;
private final Set<String> activeManifestIds;
private final Map<String, Boolean> manifestApplicability = new ConcurrentHashMap<>();
private final ThreadLocal<Boolean> preflighting = ThreadLocal.withInitial(() -> false);
private final boolean nameGated;
private boolean announced;
private Session(String shaderPackName,
Map<String, List<Target>> targetsByPath,
Set<String> activeManifestIds,
boolean nameGated) {
this.shaderPackName = shaderPackName == null ? "" : shaderPackName;
this.targetsByPath = targetsByPath;
this.activeManifestIds = activeManifestIds;
this.nameGated = nameGated;
}
public boolean isActive() {
return !targetsByPath.isEmpty();
}
public ImmutableList<String> patch(String path, ImmutableList<String> source, SourceProvider sourceProvider) {
if (path == null || source == null || preflighting.get()) {
return source;
}
announceOnce();
List<Target> candidates = targetsByPath.get(normalizePath(path));
if (candidates == null || candidates.isEmpty()) {
return source;
}
DebugLog.renderThread("[IrisPatch] path=%s candidates=%d sourceLines=%d", normalizePath(path), candidates.size(), source.size());
for (Target target : candidates) {
if (contains(source, target.marker)) {
setDiagnostic(target, "already applied");
DebugLog.renderThread("[IrisPatch] %s %s already applied marker=%s", target.manifestId, target.path, target.marker);
return source;
}
boolean applicable = manifestApplicability.computeIfAbsent(
target.manifestId,
ignored -> preflight(target.manifestId, sourceProvider)
);
if (!applicable) {
DebugLog.renderThread("[IrisPatch] %s %s skipped: manifest preflight rejected earlier", target.manifestId, target.path);
continue;
}
try {
ShaderPatchCompiler.CompiledPatch compiled = ShaderPatchCompiler.compile(
target.program,
source,
ShaderPatchCompiler.CompileMode.APPLICATION,
target.stages
);
ImmutableList<String> patched = ShaderPatchCompiler.apply(compiled, source);
setDiagnostic(target, "applied edits=" + compiled.edits().size());
DebugLog.info("[IrisPatch] %s %s applied: schema=%d stages=%s edits=%d marker=%s",
target.manifestId,
target.path,
target.program.schemaVersion(),
target.stages,
compiled.edits().size(),
target.marker);
return patched;
} catch (ShaderPatchCompiler.CompileException e) {
// Do not poison the whole manifest here. Actual source can already contain LoVisual-owned
// includes from earlier targets, while preflight is the compatibility decision point.
setDiagnostic(target, "rejected after preflight: " + e.getMessage());
DebugLog.warn("[IrisPatch] %s %s rejected after preflight: %s", target.manifestId, target.path, e.getMessage());
}
}
DebugLog.renderThread("[IrisPatch] path=%s no candidate applied", normalizePath(path));
return source;
}
private void announceOnce() {
if (announced) {
return;
}
announced = true;
if (targetsByPath.isEmpty()) {
DebugLog.info("[IrisPatch] session inactive: shaderPack='%s' nameGated=%s matchedManifests=0", shaderPackName, nameGated);
} else {
DebugLog.info("[IrisPatch] session active: shaderPack='%s' nameGated=%s manifests=%d targetPaths=%d targets=%d",
shaderPackName,
nameGated,
activeManifestIds.size(),
targetsByPath.size(),
targetsByPath.values().stream().mapToInt(List::size).sum());
}
}
private boolean preflight(String manifestId, SourceProvider sourceProvider) {
Manifest manifest = repository().manifestsById.get(manifestId);
if (manifest == null) {
DebugLog.warn("[IrisPatch] preflight rejected: missing manifest %s", manifestId);
return false;
}
if (!activeManifestIds.contains(manifestId)) {
DebugLog.renderThread("[IrisPatch] preflight skipped: manifest=%s not selected for shaderPack='%s'", manifestId, shaderPackName);
return false;
}
DebugLog.info("[IrisPatch] preflight start: manifest=%s shaderPack='%s' targets=%d", manifestId, shaderPackName, manifest.targets.size());
preflighting.set(true);
try {
for (Target target : manifest.targets) {
ImmutableList<String> source = sourceProvider.load(target.path);
if (source == null) {
setDiagnostic(target, "preflight rejected: source unavailable");
DebugLog.warn("[IrisPatch] %s %s preflight rejected: source unavailable", target.manifestId, target.path);
return false;
}
try {
ShaderPatchCompiler.CompiledPatch compiled = ShaderPatchCompiler.compile(
target.program,
source,
ShaderPatchCompiler.CompileMode.PREFLIGHT,
target.stages
);
setDiagnostic(target, "preflight passed edits=" + compiled.edits().size());
DebugLog.renderThread("[IrisPatch] %s %s preflight passed: schema=%d stages=%s sourceLines=%d edits=%d",
target.manifestId,
target.path,
target.program.schemaVersion(),
target.stages,
source.size(),
compiled.edits().size());
} catch (ShaderPatchCompiler.CompileException e) {
setDiagnostic(target, "preflight rejected: " + e.getMessage());
DebugLog.warn("[IrisPatch] %s %s preflight rejected: %s", target.manifestId, target.path, e.getMessage());
return false;
}
}
DebugLog.info("[IrisPatch] preflight accepted: manifest=%s shaderPack='%s'", manifestId, shaderPackName);
return true;
} finally {
preflighting.set(false);
}
}
}
private record Target(
String manifestId,
int priority,
String path,
String marker,
Set<ShaderPatchCompiler.ShaderStage> stages,
ShaderPatchCompiler.PatchProgram program
) {
}
private record Manifest(String id, int priority, Identity identity, List<Target> targets) {
}
private record Identity(String family, String profileId, String policy, Pattern packNamePattern) {
private boolean matches(String shaderPackName) {
if (shaderPackName == null || shaderPackName.isBlank()) {
return false;
}
if (packNamePattern != null) {
return packNamePattern.matcher(shaderPackName).matches();
}
String normalized = shaderPackName.toLowerCase(Locale.ROOT);
return normalized.contains(family.toLowerCase(Locale.ROOT)) || normalized.contains(profileId.toLowerCase(Locale.ROOT));
}
}
private record Repository(Map<String, List<Target>> targetsByPath, Map<String, Manifest> manifestsById) {
private static Repository load(List<String> extraManifestResources) {
Map<String, List<Target>> targets = new HashMap<>();
Map<String, Manifest> manifests = new HashMap<>();
try {
JsonObject index = readObject(INDEX_RESOURCE);
requireSchema(index, INDEX_RESOURCE);
for (JsonElement element : requireArray(index, "manifests")) {
addManifest(targets, manifests, element.getAsString());
}
if (extraManifestResources != null) {
for (String manifestResource : extraManifestResources) {
addManifest(targets, manifests, manifestResource);
}
}
} catch (Exception e) {
DebugLog.error("[IrisPatch] Failed to load shader patch repository", e);
}
targets.replaceAll((path, list) -> list.stream()
.sorted(Comparator.comparingInt(Target::priority).reversed())
.toList());
return new Repository(Map.copyOf(targets), Map.copyOf(manifests));
}
private static void addManifest(Map<String, List<Target>> targets,
Map<String, Manifest> manifests,
String manifestResource)
throws IOException, ShaderPatchCompiler.CompileException {
Manifest manifest = loadManifest(manifestResource);
if (manifests.putIfAbsent(manifest.id, manifest) != null) {
throw new IllegalArgumentException("Duplicate shader patch manifest " + manifest.id);
}
for (Target target : manifest.targets) {
targets.computeIfAbsent(target.path, ignored -> new ArrayList<>()).add(target);
}
}
private static Manifest loadManifest(String manifestPath)
throws IOException, ShaderPatchCompiler.CompileException {
JsonObject manifestJson = readObject(manifestPath);
requireSchema(manifestJson, manifestPath);
String manifestId = requireString(manifestJson, "id");
int priority = manifestJson.has("priority") ? manifestJson.get("priority").getAsInt() : 0;
Identity identity = readIdentity(requireObject(manifestJson, "identity"));
// Compatibility is descriptive. Runtime patchability is decided by preflight against the active pack.
requireObject(manifestJson, "compatibility");
String base = parent(manifestPath);
ArrayList<Target> targets = new ArrayList<>();
for (JsonElement element : requireArray(manifestJson, "targets")) {
JsonObject targetJson = element.getAsJsonObject();
String path = normalizePath(requireString(targetJson, "path"));
String patchPath = resolve(base, requireString(targetJson, "patch"));
ShaderPatchCompiler.PatchProgram program;
try (InputStreamReader reader = readResource(patchPath)) {
program = ShaderPatchCompiler.parse(reader);
}
targets.add(new Target(
manifestId,
priority,
path,
requireString(targetJson, "marker"),
readStages(targetJson),
program
));
}
if (targets.isEmpty()) {
throw new IllegalArgumentException("Shader patch manifest has no targets: " + manifestId);
}
return new Manifest(manifestId, priority, identity, List.copyOf(targets));
}
private static Set<ShaderPatchCompiler.ShaderStage> readStages(JsonObject targetJson)
throws ShaderPatchCompiler.CompileException {
if (targetJson.has("stage")) {
return Set.of(ShaderPatchCompiler.ShaderStage.parse(targetJson.get("stage").getAsString()));
}
if (!targetJson.has("stages")) {
return Set.of();
}
HashSet<ShaderPatchCompiler.ShaderStage> stages = new HashSet<>();
JsonArray array = requireArray(targetJson, "stages");
for (JsonElement element : array) {
stages.add(ShaderPatchCompiler.ShaderStage.parse(element.getAsString()));
}
return Set.copyOf(stages);
}
private static Identity readIdentity(JsonObject identity) {
String family = requireString(identity, "family");
String profile = requireString(identity, "profile");
String policy = requireString(identity, "policy");
Pattern namePattern = null;
if (identity.has("packNameRegex")) {
try {
namePattern = Pattern.compile(identity.get("packNameRegex").getAsString(), Pattern.CASE_INSENSITIVE);
} catch (PatternSyntaxException e) {
throw new IllegalArgumentException("Invalid shaderpack identity regex", e);
}
}
return new Identity(family, profile, policy, namePattern);
}
private static JsonObject readObject(String resource) throws IOException {
try (InputStreamReader reader = readResource(resource)) {
return JsonParser.parseReader(reader).getAsJsonObject();
}
}
private static InputStreamReader readResource(String resource) throws IOException {
InputStream stream = ShaderPatchEngine.class.getClassLoader().getResourceAsStream(resource);
if (stream == null) {
throw new IOException("Missing resource " + resource);
}
return new InputStreamReader(stream, StandardCharsets.UTF_8);
}
private static void requireSchema(JsonObject object, String resource) {
if (!object.has("schemaVersion") || object.get("schemaVersion").getAsInt() != 1) {
throw new IllegalArgumentException("Unsupported shader patch manifest schema in " + resource);
}
}
private static JsonObject requireObject(JsonObject object, String key) {
if (!object.has(key) || !object.get(key).isJsonObject()) {
throw new IllegalArgumentException("Missing object " + key);
}
return object.getAsJsonObject(key);
}
private static JsonArray requireArray(JsonObject object, String key) {
if (!object.has(key) || !object.get(key).isJsonArray()) {
throw new IllegalArgumentException("Missing array " + key);
}
return object.getAsJsonArray(key);
}
private static String requireString(JsonObject object, String key) {
if (!object.has(key) || !object.get(key).isJsonPrimitive()) {
throw new IllegalArgumentException("Missing string " + key);
}
return object.get(key).getAsString();
}
private static String parent(String path) {
int separator = path.lastIndexOf('/');
return separator < 0 ? "" : path.substring(0, separator + 1);
}
private static String resolve(String base, String path) {
return path.startsWith("/") ? path.substring(1) : base + path;
}
private Selection select(String shaderPackName) {
if (shaderPackName == null || shaderPackName.isBlank()) {
return new Selection(Map.of(), Set.of(), true);
}
HashSet<String> selectedManifests = new HashSet<>();
for (Manifest manifest : manifestsById.values()) {
if (manifest.identity.matches(shaderPackName)) {
selectedManifests.add(manifest.id);
}
}
if (selectedManifests.isEmpty()) {
return new Selection(Map.of(), Set.of(), true);
}
Map<String, List<Target>> selectedTargets = new HashMap<>();
for (Map.Entry<String, List<Target>> entry : targetsByPath.entrySet()) {
ArrayList<Target> list = new ArrayList<>();
for (Target target : entry.getValue()) {
if (selectedManifests.contains(target.manifestId)) {
list.add(target);
}
}
if (!list.isEmpty()) {
selectedTargets.put(entry.getKey(), List.copyOf(list));
}
}
return new Selection(Map.copyOf(selectedTargets), Set.copyOf(selectedManifests), true);
}
private record Selection(Map<String, List<Target>> targetsByPath, Set<String> manifestIds, boolean nameGated) {
}
}
}

View file

@ -0,0 +1,146 @@
/*
* 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.iris.patch;
import com.google.common.collect.ImmutableList;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
final class PatchCompileHelper {
private PatchCompileHelper() {}
static ShaderPatchCompiler.CompiledPatch compile(ShaderPatchCompiler.PatchProgram program, ImmutableList<String> source, ShaderPatchCompiler.CompileMode mode, Set<ShaderPatchCompiler.ShaderStage> stages) throws ShaderPatchCompiler.CompileException {
if (mode == ShaderPatchCompiler.CompileMode.PREFLIGHT && !containsLoVisualPayload(source)) validateNoSourceExcerpt(program, source);
PatchSourceHelper.SourceIndex index = PatchSourceHelper.SourceIndex.create(source);
PatchSourceHelper.validateProbes(program, index);
ArrayList<ShaderPatchCompiler.Edit> edits = new ArrayList<>(program.operations().size());
for (ShaderPatchCompiler.Operation operation : program.operations()) {
int anchor = PatchSourceHelper.find(index, operation.anchor());
switch (operation.type()) {
case INSERT_BEFORE -> edits.add(new ShaderPatchCompiler.Edit(anchor, anchor, operation.payload()));
case INSERT_AFTER -> edits.add(new ShaderPatchCompiler.Edit(anchor + 1, anchor + 1, operation.payload()));
case REPLACE_LINE -> edits.add(new ShaderPatchCompiler.Edit(anchor, anchor + 1, operation.payload()));
case REMOVE_LINE -> edits.add(new ShaderPatchCompiler.Edit(anchor, anchor + 1, List.of()));
case REPLACE_RANGE -> {
int end = PatchSourceHelper.find(index, operation.end());
if (end < anchor) throw new ShaderPatchCompiler.CompileException("Range end precedes start");
edits.add(new ShaderPatchCompiler.Edit(anchor, operation.includeEnd() ? end + 1 : end, operation.payload()));
}
case REPLACE_BETWEEN -> {
int end = PatchSourceHelper.find(index, operation.end());
if (end <= anchor) throw new ShaderPatchCompiler.CompileException("Range end does not follow start");
edits.add(new ShaderPatchCompiler.Edit(anchor + 1, end, operation.payload()));
}
}
}
validateNonOverlapping(edits);
validateStageBounds(edits, source, stages);
validateDelimiterDelta(edits, source);
edits.sort(Comparator.comparingInt(ShaderPatchCompiler.Edit::start).thenComparingInt(ShaderPatchCompiler.Edit::end).reversed());
return new ShaderPatchCompiler.CompiledPatch(List.copyOf(edits));
}
static ImmutableList<String> apply(ShaderPatchCompiler.CompiledPatch patch, ImmutableList<String> source) {
ArrayList<String> output = new ArrayList<>(source);
for (ShaderPatchCompiler.Edit edit : patch.edits()) {
output.subList(edit.start(), edit.end()).clear();
output.addAll(edit.start(), edit.payload());
}
return ImmutableList.copyOf(output);
}
static String signature(String sourceLine) {
String normalized = normalizeLine(sourceLine);
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return java.util.HexFormat.of().formatHex(digest.digest(normalized.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException e) { throw new IllegalStateException("SHA-256 unavailable", e); }
}
static boolean containsLoVisualPayload(List<String> source) {
for (String line : source) if (line.contains("lovisual") || line.contains("LOVISUAL_")) return true;
return false;
}
static void validateNoSourceExcerpt(ShaderPatchCompiler.PatchProgram program, List<String> source) throws ShaderPatchCompiler.CompileException {
List<String> normalizedSource = source.stream().map(PatchCompileHelper::normalizeLine).toList();
for (ShaderPatchCompiler.Operation operation : program.operations()) {
List<String> payload = operation.payload().stream().map(PatchCompileHelper::normalizeLine).toList();
for (int payloadStart = 0; payloadStart + 2 < payload.size(); payloadStart++) {
if (payload.get(payloadStart).isEmpty() || payload.get(payloadStart + 1).isEmpty() || payload.get(payloadStart + 2).isEmpty()) continue;
for (int sourceStart = 0; sourceStart + 2 < normalizedSource.size(); sourceStart++) {
if (payload.get(payloadStart).equals(normalizedSource.get(sourceStart)) && payload.get(payloadStart + 1).equals(normalizedSource.get(sourceStart + 1)) && payload.get(payloadStart + 2).equals(normalizedSource.get(sourceStart + 2))) {
throw new ShaderPatchCompiler.CompileException("Patch payload reproduces a target source excerpt");
}
}
}
}
}
static String normalizeLine(String sourceLine) {
String source = sourceLine == null ? "" : sourceLine;
StringBuilder normalized = new StringBuilder(source.length());
for (int i = 0; i < source.length(); i++) { char c = source.charAt(i); if (!Character.isWhitespace(c)) normalized.append(c); }
return normalized.toString();
}
static void validateNonOverlapping(List<ShaderPatchCompiler.Edit> edits) throws ShaderPatchCompiler.CompileException {
ArrayList<ShaderPatchCompiler.Edit> sorted = new ArrayList<>(edits);
sorted.sort(Comparator.comparingInt(ShaderPatchCompiler.Edit::start).thenComparingInt(ShaderPatchCompiler.Edit::end));
int previousEnd = -1;
for (ShaderPatchCompiler.Edit edit : sorted) {
if (edit.start() < previousEnd) throw new ShaderPatchCompiler.CompileException("Patch operations overlap");
previousEnd = Math.max(previousEnd, edit.end());
}
}
static void validateStageBounds(List<ShaderPatchCompiler.Edit> edits, List<String> source, Set<ShaderPatchCompiler.ShaderStage> stages) throws ShaderPatchCompiler.CompileException {
if (stages == null || stages.isEmpty() || stages.contains(ShaderPatchCompiler.ShaderStage.ANY) || !sourceHasStageBlocks(source)) return;
for (ShaderPatchCompiler.Edit edit : edits) {
ShaderPatchCompiler.ShaderStage stage = stageBeforeLine(source, edit.start());
if (stage == null || !stages.contains(stage)) throw new ShaderPatchCompiler.CompileException("Patch edit escapes allowed shader stage(s): editLine=" + (edit.start() + 1) + " stage=" + (stage == null ? "none" : stage.name().toLowerCase(Locale.ROOT)) + " allowed=" + stages);
}
}
static boolean sourceHasStageBlocks(List<String> source) {
for (String line : source) {
String stripped = PatchSourceHelper.stripLineComment(line).trim();
if (stripped.contains("FRAGMENT_SHADER") || stripped.contains("VERTEX_SHADER") || stripped.contains("GEOMETRY_SHADER") || stripped.contains("COMPUTE_SHADER")) return true;
}
return false;
}
static ShaderPatchCompiler.ShaderStage stageBeforeLine(List<String> source, int lineIndex) {
ArrayList<ShaderPatchCompiler.ShaderStage> stack = new ArrayList<>();
int end = Math.min(Math.max(lineIndex, 0), source.size());
for (int i = 0; i < end; i++) {
String stripped = PatchSourceHelper.stripLineComment(source.get(i)).trim();
ShaderPatchCompiler.ShaderStage begin = stageFromDirective(stripped);
if (begin != null) stack.add(begin);
else if (stripped.startsWith("#endif") && !stack.isEmpty()) stack.remove(stack.size() - 1);
}
for (int i = stack.size() - 1; i >= 0; i--) { ShaderPatchCompiler.ShaderStage stage = stack.get(i); if (stage != ShaderPatchCompiler.ShaderStage.ANY) return stage; }
return null;
}
static ShaderPatchCompiler.ShaderStage stageFromDirective(String stripped) {
if (!stripped.startsWith("#if")) return null;
if (stripped.contains("FRAGMENT_SHADER")) return ShaderPatchCompiler.ShaderStage.FRAGMENT;
if (stripped.contains("VERTEX_SHADER")) return ShaderPatchCompiler.ShaderStage.VERTEX;
if (stripped.contains("GEOMETRY_SHADER")) return ShaderPatchCompiler.ShaderStage.GEOMETRY;
if (stripped.contains("COMPUTE_SHADER")) return ShaderPatchCompiler.ShaderStage.COMPUTE;
return null;
}
static void validateDelimiterDelta(List<ShaderPatchCompiler.Edit> edits, List<String> source) throws ShaderPatchCompiler.CompileException {
for (char open : new char[]{'{', '(', '['}) {
char close = switch (open) { case '{' -> '}'; case '(' -> ')'; case '[' -> ']'; default -> throw new IllegalStateException(); };
int delta = 0;
for (ShaderPatchCompiler.Edit edit : edits) {
delta += delimiterBalance(edit.payload(), open, close);
delta -= delimiterBalance(source.subList(edit.start(), edit.end()), open, close);
}
if (delta != 0) throw new ShaderPatchCompiler.CompileException("Patch changes " + open + close + " delimiter balance");
}
}
static int delimiterBalance(List<String> lines, char open, char close) {
int balance = 0;
for (String line : lines) for (int i = 0; i < line.length(); i++) { char c = line.charAt(i); if (c == open) balance++; if (c == close) balance--; }
return balance;
}
}

View file

@ -0,0 +1,174 @@
/*
* 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.iris.patch;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
final class PatchSourceHelper {
static final Pattern GLOBAL_DECLARATION = Pattern.compile("^(?:flat\\s+|smooth\\s+|noperspective\\s+|centroid\\s+)*(?:in|out|uniform|varying)\\b.*;");
private PatchSourceHelper() {}
static void validateProbes(ShaderPatchCompiler.PatchProgram program, SourceIndex index) throws ShaderPatchCompiler.CompileException {
ArrayList<String> missing = new ArrayList<>();
ArrayList<String> forbidden = new ArrayList<>();
for (ShaderPatchCompiler.Probe probe : program.probes()) {
boolean present = switch (probe.type()) {
case REQUIRE_HASH, FORBID_HASH -> index.availableHashes().contains(probe.value());
case REQUIRE_IDENT, FORBID_IDENT -> hasIdentifier(index.source(), probe.value());
case REQUIRE_FUNCTION, FORBID_FUNCTION -> findFunctionStart(index.source(), probe.value(), 1, false) >= 0;
case REQUIRE_CALL, FORBID_CALL -> hasCall(index.source(), probe.value());
case REQUIRE_UNIFORM, FORBID_UNIFORM -> hasUniform(index.source(), probe.value());
case REQUIRE_DEFINE, FORBID_DEFINE -> hasDefine(index.source(), probe.value());
};
if (probe.type().required() && !present) missing.add(probe.type().label() + " " + probe.value());
else if (!probe.type().required() && present) forbidden.add(probe.type().label() + " " + probe.value());
}
if (!missing.isEmpty()) throw new ShaderPatchCompiler.CompileException("Required structural probes are absent: " + String.join(", ", missing));
if (!forbidden.isEmpty()) throw new ShaderPatchCompiler.CompileException("Forbidden structural probes are present: " + String.join(", ", forbidden));
}
static int find(SourceIndex index, ShaderPatchCompiler.Selector selector) throws ShaderPatchCompiler.CompileException {
return switch (selector.kind()) {
case HASH -> findHash(index.hashes(), selector);
case IDENT -> findLine(index.source(), selector, line -> hasIdentifier(line, selector.value()));
case FUNCTION -> findFunctionStartChecked(index.source(), selector);
case FUNCTION_OPEN -> findFunctionOpenChecked(index.source(), selector);
case DEFINE -> findLine(index.source(), selector, line -> isDefine(line, selector.value()));
case DRAWBUFFERS -> findLine(index.source(), selector, line -> line.contains("DRAWBUFFERS"));
case STAGE_BEGIN -> findLine(index.source(), selector, line -> isStageBegin(line, selector.value()));
case ZONE -> findZone(index.source(), selector.value());
};
}
static int findHash(List<String> hashes, ShaderPatchCompiler.Selector selector) throws ShaderPatchCompiler.CompileException {
int occurrence = 0; int result = -1;
for (int i = 0; i < hashes.size(); i++) {
if (!hashes.get(i).equals(selector.value())) continue;
occurrence++;
if (occurrence == selector.occurrence()) result = i;
}
if (result < 0) throw new ShaderPatchCompiler.CompileException("Anchor not found: hash " + selector.value() + "#" + selector.occurrence());
if (selector.requireUnique() && occurrence != 1) throw new ShaderPatchCompiler.CompileException("Anchor is ambiguous; specify occurrence: hash " + selector.value());
return result;
}
static int findLine(List<String> source, ShaderPatchCompiler.Selector selector, LinePredicate predicate) throws ShaderPatchCompiler.CompileException {
int occurrence = 0; int result = -1;
for (int i = 0; i < source.size(); i++) {
if (!predicate.matches(source.get(i))) continue;
occurrence++;
if (occurrence == selector.occurrence()) result = i;
}
if (result < 0) throw new ShaderPatchCompiler.CompileException("Anchor not found: " + selector.kind().name().toLowerCase(Locale.ROOT) + " " + selector.value() + "#" + selector.occurrence());
if (selector.requireUnique() && occurrence != 1) throw new ShaderPatchCompiler.CompileException("Anchor is ambiguous; specify occurrence: " + selector.kind().name().toLowerCase(Locale.ROOT) + " " + selector.value());
return result;
}
static int findFunctionStartChecked(List<String> source, ShaderPatchCompiler.Selector selector) throws ShaderPatchCompiler.CompileException {
int index = findFunctionStart(source, selector.value(), selector.occurrence(), selector.requireUnique());
if (index < 0) throw new ShaderPatchCompiler.CompileException("Anchor not found: function " + selector.value() + "#" + selector.occurrence());
return index;
}
static int findFunctionOpenChecked(List<String> source, ShaderPatchCompiler.Selector selector) throws ShaderPatchCompiler.CompileException {
int start = findFunctionStartChecked(source, new ShaderPatchCompiler.Selector(ShaderPatchCompiler.SelectorKind.FUNCTION, selector.value(), selector.occurrence(), selector.requireUnique()));
for (int i = start; i < source.size(); i++) {
String line = stripLineComment(source.get(i));
int brace = line.indexOf('{'); int semicolon = line.indexOf(';');
if (brace >= 0 && (semicolon < 0 || brace < semicolon)) return i;
if (semicolon >= 0) break;
}
throw new ShaderPatchCompiler.CompileException("Function opening brace not found: " + selector.value() + "#" + selector.occurrence());
}
static int findFunctionStart(List<String> source, String functionName, int requestedOccurrence, boolean requireUnique) {
int occurrence = 0; int result = -1;
for (int i = 0; i < source.size(); i++) {
if (!looksLikeFunctionStart(source.get(i), functionName)) continue;
occurrence++;
if (occurrence == requestedOccurrence) result = i;
}
if (result < 0) return -1;
if (requireUnique && occurrence != 1) return -2;
return result;
}
static int findZone(List<String> source, String zone) {
if (zone.equals("declarations")) {
for (int i = 0; i < source.size(); i++) {
String trimmed = source.get(i).trim();
if (GLOBAL_DECLARATION.matcher(trimmed).matches()) return i;
}
}
for (int i = 0; i < source.size(); i++) if (looksLikeAnyFunctionStart(source.get(i))) return i;
return 0;
}
static boolean isStageBegin(String line, String stageMacro) {
String stripped = stripLineComment(line).trim();
return stripped.equals("#ifdef " + stageMacro) || stripped.equals("#if defined " + stageMacro) || stripped.equals("#if defined(" + stageMacro + ")");
}
static boolean looksLikeAnyFunctionStart(String line) {
String stripped = stripLineComment(line).trim();
if (stripped.isEmpty() || stripped.startsWith("#")) return false;
return stripped.matches(".*\\b[A-Za-z_][A-Za-z0-9_]*\\s*\\(.*") && !stripped.endsWith(";");
}
static boolean looksLikeFunctionStart(String line, String functionName) {
String stripped = stripLineComment(line).trim();
if (stripped.isEmpty() || stripped.startsWith("#")) return false;
if (!hasIdentifier(stripped, functionName)) return false;
int nameIndex = stripped.indexOf(functionName);
int paren = stripped.indexOf('(', nameIndex + functionName.length());
if (paren < 0) return false;
int semicolon = stripped.indexOf(';');
return semicolon < 0 || semicolon > paren;
}
static boolean hasCall(List<String> source, String name) {
String needle = name + "(";
for (String line : source) { String stripped = stripLineComment(line); if (stripped.contains(needle)) return true; }
return false;
}
static boolean hasUniform(List<String> source, String name) {
for (String line : source) { String stripped = stripLineComment(line).trim(); if (stripped.startsWith("uniform ") && hasIdentifier(stripped, name)) return true; }
return false;
}
static boolean hasDefine(List<String> source, String name) {
for (String line : source) if (isDefine(line, name)) return true;
return false;
}
static boolean isDefine(String line, String name) {
String stripped = stripLineComment(line).trim();
if (!stripped.startsWith("#define")) return false;
String rest = stripped.substring("#define".length()).trim();
if (rest.startsWith(name)) return rest.length() == name.length() || !isIdentifierPart(rest.charAt(name.length()));
return false;
}
static boolean hasIdentifier(List<String> source, String identifier) {
for (String line : source) if (hasIdentifier(line, identifier)) return true;
return false;
}
static boolean hasIdentifier(String line, String identifier) {
String stripped = stripLineComment(line);
int from = 0;
while (from < stripped.length()) {
int index = stripped.indexOf(identifier, from);
if (index < 0) return false;
int before = index - 1; int after = index + identifier.length();
boolean validBefore = before < 0 || !isIdentifierPart(stripped.charAt(before));
boolean validAfter = after >= stripped.length() || !isIdentifierPart(stripped.charAt(after));
if (validBefore && validAfter) return true;
from = index + identifier.length();
}
return false;
}
static boolean isIdentifierPart(char character) { return Character.isLetterOrDigit(character) || character == '_'; }
static String stripLineComment(String line) { int comment = line.indexOf("//"); return comment >= 0 ? line.substring(0, comment) : line; }
interface LinePredicate { boolean matches(String line); }
record SourceIndex(ImmutableList<String> source, List<String> hashes, Set<String> availableHashes) {
static SourceIndex create(ImmutableList<String> source) {
ArrayList<String> hashes = new ArrayList<>(source.size());
Set<String> available = new HashSet<>(source.size());
for (String line : source) { String hash = ShaderPatchCompiler.signature(line); hashes.add(hash); available.add(hash); }
return new SourceIndex(source, List.copyOf(hashes), Set.copyOf(available));
}
}
}

View file

@ -0,0 +1,157 @@
/*
* 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.iris.patch;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
final class PatchParseSupport {
private PatchParseSupport() {
}
static ShaderPatchCompiler.Operation readHashRangeOperation(String[] parts, BufferedReader reader, int lineNumber, String directive) throws IOException, ShaderPatchCompiler.CompileException {
if (parts.length < 3 || parts.length > 6) {
throw new ShaderPatchCompiler.CompileException("Invalid " + directive + " at line " + lineNumber);
}
ShaderPatchCompiler.Selector start = new ShaderPatchCompiler.Selector(
ShaderPatchCompiler.SelectorKind.HASH,
validateHash(parts[1], lineNumber),
readPositive(parts, 3, 1, lineNumber),
parts.length <= 3
);
ShaderPatchCompiler.Selector end = new ShaderPatchCompiler.Selector(
ShaderPatchCompiler.SelectorKind.HASH,
validateHash(parts[2], lineNumber),
readPositive(parts, 4, 1, lineNumber),
parts.length <= 4
);
boolean includeEnd = parts.length >= 6 && Boolean.parseBoolean(parts[5]);
return new ShaderPatchCompiler.Operation(
ShaderPatchCompiler.OperationType.valueOf(directive.toUpperCase(Locale.ROOT)),
start,
end,
includeEnd,
readPayload(reader, lineNumber)
);
}
static ShaderPatchCompiler.Operation readSingleSelectorOperation(String[] parts, BufferedReader reader, int lineNumber, String directive, ShaderPatchCompiler.SelectorKind kind) throws IOException, ShaderPatchCompiler.CompileException {
ShaderPatchCompiler.OperationType operationType = operationType(directive);
ShaderPatchCompiler.Selector selector = readSelector(parts, kind, lineNumber);
return new ShaderPatchCompiler.Operation(
operationType,
selector,
null,
false,
operationType == ShaderPatchCompiler.OperationType.REMOVE_LINE ? List.of() : readPayload(reader, lineNumber)
);
}
static ShaderPatchCompiler.OperationType operationType(String directive) throws ShaderPatchCompiler.CompileException {
if (directive.startsWith("insert_before")) return ShaderPatchCompiler.OperationType.INSERT_BEFORE;
if (directive.startsWith("insert_after")) return ShaderPatchCompiler.OperationType.INSERT_AFTER;
if (directive.startsWith("replace_line")) return ShaderPatchCompiler.OperationType.REPLACE_LINE;
if (directive.startsWith("remove_line")) return ShaderPatchCompiler.OperationType.REMOVE_LINE;
throw new ShaderPatchCompiler.CompileException("Unsupported operation directive @" + directive);
}
static ShaderPatchCompiler.Selector readZoneSelector(String[] parts, int lineNumber) throws ShaderPatchCompiler.CompileException {
if (parts.length != 2) {
throw new ShaderPatchCompiler.CompileException("Invalid insert_zone directive at line " + lineNumber);
}
String zone = parts[1].toLowerCase(Locale.ROOT);
if (!zone.equals("declarations") && !zone.equals("functions")) {
throw new ShaderPatchCompiler.CompileException("Unsupported insert_zone " + parts[1] + " at line " + lineNumber);
}
return new ShaderPatchCompiler.Selector(ShaderPatchCompiler.SelectorKind.ZONE, zone, 1, false);
}
static ShaderPatchCompiler.Selector readSelector(String[] parts, ShaderPatchCompiler.SelectorKind kind, int lineNumber) throws ShaderPatchCompiler.CompileException {
if (kind == ShaderPatchCompiler.SelectorKind.DRAWBUFFERS) {
if (parts.length < 1 || parts.length > 2) {
throw new ShaderPatchCompiler.CompileException("Invalid drawbuffers selector at line " + lineNumber);
}
return new ShaderPatchCompiler.Selector(ShaderPatchCompiler.SelectorKind.DRAWBUFFERS, "DRAWBUFFERS", readPositive(parts, 1, 1, lineNumber), parts.length == 1);
}
if (parts.length < 2 || parts.length > 3) {
throw new ShaderPatchCompiler.CompileException("Invalid selector at line " + lineNumber);
}
String value = kind == ShaderPatchCompiler.SelectorKind.HASH ? validateHash(parts[1], lineNumber) : validateName(parts[1], lineNumber);
return new ShaderPatchCompiler.Selector(kind, value, readPositive(parts, 2, 1, lineNumber), parts.length == 2);
}
static ShaderPatchCompiler.Selector readHashSelector(String[] parts, int signatureIndex, int lineNumber) throws ShaderPatchCompiler.CompileException {
if (parts.length < signatureIndex + 1 || parts.length > signatureIndex + 2) {
throw new ShaderPatchCompiler.CompileException("Invalid hash anchor at line " + lineNumber);
}
return new ShaderPatchCompiler.Selector(
ShaderPatchCompiler.SelectorKind.HASH,
validateHash(parts[signatureIndex], lineNumber),
readPositive(parts, signatureIndex + 1, 1, lineNumber),
parts.length == signatureIndex + 1
);
}
static String readHash(String[] parts, int index, int lineNumber) throws ShaderPatchCompiler.CompileException {
if (parts.length != index + 1) {
throw new ShaderPatchCompiler.CompileException("Invalid hash directive at line " + lineNumber);
}
return validateHash(parts[index], lineNumber);
}
static String readName(String[] parts, int index, int lineNumber) throws ShaderPatchCompiler.CompileException {
if (parts.length != index + 1) {
throw new ShaderPatchCompiler.CompileException("Invalid identifier directive at line " + lineNumber);
}
return validateName(parts[index], lineNumber);
}
static String validateHash(String hash, int lineNumber) throws ShaderPatchCompiler.CompileException {
String normalized = hash.toLowerCase(Locale.ROOT);
if (!normalized.matches("[0-9a-f]{64}")) {
throw new ShaderPatchCompiler.CompileException("Invalid structural hash at line " + lineNumber);
}
return normalized;
}
static String validateName(String name, int lineNumber) throws ShaderPatchCompiler.CompileException {
if (!name.matches("[A-Za-z_][A-Za-z0-9_]*")) {
throw new ShaderPatchCompiler.CompileException("Invalid identifier '" + name + "' at line " + lineNumber);
}
return name;
}
static int readPositive(String[] parts, int index, int fallback, int lineNumber) throws ShaderPatchCompiler.CompileException {
if (parts.length <= index) {
return fallback;
}
try {
int value = Integer.parseInt(parts[index]);
if (value < 1) {
throw new NumberFormatException();
}
return value;
} catch (NumberFormatException e) {
throw new ShaderPatchCompiler.CompileException("Invalid occurrence at line " + lineNumber);
}
}
static List<String> readPayload(BufferedReader reader, int directiveLine) throws IOException, ShaderPatchCompiler.CompileException {
java.util.ArrayList<String> payload = new java.util.ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
if (line.trim().equals("@end")) {
return List.copyOf(payload);
}
payload.add(line);
}
throw new ShaderPatchCompiler.CompileException("Unterminated payload after line " + directiveLine);
}
}

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.iris.patch;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
final class PatchParser {
private PatchParser() {
}
static ShaderPatchCompiler.PatchProgram parse(Reader source) throws IOException, ShaderPatchCompiler.CompileException {
BufferedReader reader = source instanceof BufferedReader buffered ? buffered : new BufferedReader(source);
ArrayList<ShaderPatchCompiler.Probe> probes = new ArrayList<>();
ArrayList<ShaderPatchCompiler.Operation> operations = new ArrayList<>();
int schemaVersion = 1;
String line;
int lineNumber = 0;
while ((line = reader.readLine()) != null) {
lineNumber++;
String trimmed = line.trim();
if (trimmed.isEmpty() || trimmed.startsWith(";")) {
continue;
}
if (!trimmed.startsWith("@")) {
throw new ShaderPatchCompiler.CompileException("Expected directive at line " + lineNumber);
}
String[] parts = trimmed.substring(1).split("\\s+");
String directive = parts[0];
switch (directive) {
case "patch" -> {
if (parts.length != 2) {
throw new ShaderPatchCompiler.CompileException("Invalid patch schema directive at line " + lineNumber);
}
try {
schemaVersion = Integer.parseInt(parts[1]);
} catch (NumberFormatException e) {
throw new ShaderPatchCompiler.CompileException("Invalid patch schema at line " + lineNumber);
}
if (schemaVersion != 1 && schemaVersion != 2) {
throw new ShaderPatchCompiler.CompileException("Unsupported patch schema " + schemaVersion + " at line " + lineNumber);
}
}
case "require" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_HASH, PatchParseSupport.readHash(parts, 1, lineNumber)));
case "forbid" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_HASH, PatchParseSupport.readHash(parts, 1, lineNumber)));
case "require_hash" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_HASH, PatchParseSupport.readHash(parts, 1, lineNumber)));
case "forbid_hash" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_HASH, PatchParseSupport.readHash(parts, 1, lineNumber)));
case "require_ident" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_IDENT, PatchParseSupport.readName(parts, 1, lineNumber)));
case "forbid_ident" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_IDENT, PatchParseSupport.readName(parts, 1, lineNumber)));
case "require_function" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_FUNCTION, PatchParseSupport.readName(parts, 1, lineNumber)));
case "forbid_function" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_FUNCTION, PatchParseSupport.readName(parts, 1, lineNumber)));
case "require_call" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_CALL, PatchParseSupport.readName(parts, 1, lineNumber)));
case "forbid_call" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_CALL, PatchParseSupport.readName(parts, 1, lineNumber)));
case "require_uniform" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_UNIFORM, PatchParseSupport.readName(parts, 1, lineNumber)));
case "forbid_uniform" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_UNIFORM, PatchParseSupport.readName(parts, 1, lineNumber)));
case "require_define" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_DEFINE, PatchParseSupport.readName(parts, 1, lineNumber)));
case "forbid_define" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_DEFINE, PatchParseSupport.readName(parts, 1, lineNumber)));
case "insert_before", "insert_after", "replace_line", "remove_line" -> {
ShaderPatchCompiler.Selector selector = PatchParseSupport.readHashSelector(parts, 1, lineNumber);
operations.add(new ShaderPatchCompiler.Operation(
ShaderPatchCompiler.OperationType.valueOf(directive.toUpperCase(Locale.ROOT)),
selector,
null,
false,
directive.equals("remove_line") ? List.of() : PatchParseSupport.readPayload(reader, lineNumber)
));
}
case "replace_range", "replace_between" -> operations.add(PatchParseSupport.readHashRangeOperation(parts, reader, lineNumber, directive));
case "insert_zone" -> operations.add(new ShaderPatchCompiler.Operation(
ShaderPatchCompiler.OperationType.INSERT_BEFORE,
PatchParseSupport.readZoneSelector(parts, lineNumber),
null,
false,
PatchParseSupport.readPayload(reader, lineNumber)
));
case "insert_before_hash", "insert_after_hash", "replace_line_hash", "remove_line_hash" ->
operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.HASH));
case "insert_before_ident", "insert_after_ident", "replace_line_ident", "remove_line_ident" ->
operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.IDENT));
case "insert_before_function", "replace_line_function", "remove_line_function" ->
operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.FUNCTION));
case "insert_after_function_open" ->
operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.FUNCTION_OPEN));
case "insert_before_define", "insert_after_define", "replace_line_define", "remove_line_define" ->
operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.DEFINE));
case "insert_before_drawbuffers", "insert_after_drawbuffers", "replace_line_drawbuffers", "remove_line_drawbuffers" ->
operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.DRAWBUFFERS));
case "insert_after_stage_begin" ->
operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.STAGE_BEGIN));
default -> throw new ShaderPatchCompiler.CompileException("Unknown directive @" + directive + " at line " + lineNumber);
}
}
if (operations.isEmpty()) {
throw new ShaderPatchCompiler.CompileException("Patch contains no operations");
}
return new ShaderPatchCompiler.PatchProgram(schemaVersion, List.copyOf(probes), List.copyOf(operations));
}
}

View file

@ -0,0 +1,82 @@
/*
* 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.iris.runtime;
import net.fabricmc.loader.api.FabricLoader;
import dev.loki.lovisual.util.logging.DebugLog;
import dev.loki.lovisual.render.iris.compat.IrisCompatibilityFeature;
import dev.loki.lovisual.render.iris.compat.IrisCompatibilityProfile;
import dev.loki.lovisual.render.iris.runtime.IrisRuntimeBridge;
import dev.loki.lovisual.render.iris.runtime.IrisRuntimeSnapshot;
public enum IrisRuntime {
;
private static volatile boolean loggedApiFailure;
public static IrisRuntimeSnapshot snapshot() {
if (!FabricLoader.getInstance().isModLoaded("iris")) {
return IrisRuntimeSnapshot.UNLOADED;
}
try {
return IrisRuntimeBridge.snapshot();
} catch (LinkageError | RuntimeException t) {
if (!loggedApiFailure) {
loggedApiFailure = true;
DebugLog.warn("[IrisCompat] Iris runtime probe failed: " + t);
}
return loadedApiUnavailable(t.getClass().getSimpleName());
}
}
public static boolean isModLoaded() {
return FabricLoader.getInstance().isModLoaded("iris");
}
public static boolean isShaderpackRendererActive() {
IrisRuntimeSnapshot snapshot = snapshot();
return snapshot.modLoaded() && snapshot.apiAvailable() && snapshot.shadersEnabled() && snapshot.shaderpackInUse();
}
public static boolean isRenderingShadowPass() {
IrisRuntimeSnapshot snapshot = snapshot();
return snapshot.modLoaded() && snapshot.apiAvailable() && snapshot.renderingShadowPass();
}
public static void registerLoVisualPipelines() {
if (!FabricLoader.getInstance().isModLoaded("iris")) {
return;
}
try {
IrisRuntimeBridge.registerLoVisualPipelines();
} catch (LinkageError | RuntimeException t) {
if (!loggedApiFailure) {
loggedApiFailure = true;
DebugLog.warn("[IrisCompat] LoVisual pipeline registration failed: " + t);
}
}
}
public static boolean supports(IrisCompatibilityFeature feature) {
return snapshot().profile().supports(feature);
}
private static IrisRuntimeSnapshot loadedApiUnavailable(String status) {
return new IrisRuntimeSnapshot(
true,
false,
false,
false,
false,
"",
IrisCompatibilityProfile.NONE,
status == null || status.isBlank() ? "api unavailable" : status
);
}
}

View file

@ -0,0 +1,91 @@
/*
* 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.iris.runtime;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import net.irisshaders.iris.Iris;
import net.irisshaders.iris.api.v0.IrisApi;
import net.irisshaders.iris.api.v0.IrisProgram;
import dev.loki.lovisual.render.LoVisualEntityRenderTypes;
import dev.loki.lovisual.render.iris.compat.IrisCompatibilityProfile;
import dev.loki.lovisual.render.iris.compat.IrisCompatibilityProfiles;
import dev.loki.lovisual.render.iris.runtime.IrisRuntimeSnapshot;
public enum IrisRuntimeBridge {
;
static IrisRuntimeSnapshot snapshot() {
IrisApi api = IrisApi.getInstance();
String packName = currentPackName();
boolean shaderpackInUse = probe(api::isShaderPackInUse, packName != null && !packName.isBlank());
boolean shadersEnabled = shadersEnabled(api, shaderpackInUse);
boolean renderingShadowPass = probe(api::isRenderingShadowPass, false);
IrisCompatibilityProfile profile = IrisCompatibilityProfiles.resolve(packName, shaderpackInUse);
return new IrisRuntimeSnapshot(
true,
true,
shadersEnabled,
shaderpackInUse,
renderingShadowPass,
packName,
profile,
"ok"
);
}
static void registerLoVisualPipelines() {
IrisApi api = IrisApi.getInstance();
java.util.List<RenderPipeline> pipelines = LoVisualEntityRenderTypes.irisMappedPipelines();
for (RenderPipeline pipeline : pipelines) {
assign(api, pipeline, IrisProgram.ENTITIES_TRANSLUCENT);
}
}
private static void assign(IrisApi api, RenderPipeline pipeline, IrisProgram program) {
if (api == null || pipeline == null || program == null) return;
try {
api.assignPipeline(pipeline, program);
} catch (IllegalStateException ignored) {
// Iris keeps pipeline assignments globally; resource reloads can call this more than once.
}
}
private static boolean shadersEnabled(IrisApi api, boolean shaderpackInUse) {
try {
if (api.getConfig() != null) {
return api.getConfig().areShadersEnabled();
}
} catch (LinkageError | RuntimeException ignored) {
// Iris can expose a not-yet-initialized config during early LoVisual module/config loading.
}
return shaderpackInUse;
}
private static String currentPackName() {
try {
String value = Iris.getCurrentPackName();
return value == null ? "" : value;
} catch (LinkageError | RuntimeException ignored) {
return "";
}
}
private static boolean probe(BooleanProbe probe, boolean fallback) {
try {
return probe.getAsBoolean();
} catch (LinkageError | RuntimeException ignored) {
return fallback;
}
}
@FunctionalInterface
private interface BooleanProbe {
boolean getAsBoolean();
}
}

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.iris.runtime;
import dev.loki.lovisual.render.iris.compat.IrisCompatibilityProfile;
public record IrisRuntimeSnapshot(
boolean modLoaded,
boolean apiAvailable,
boolean shadersEnabled,
boolean shaderpackInUse,
boolean renderingShadowPass,
String shaderpackName,
IrisCompatibilityProfile profile,
String status
) {
public static final IrisRuntimeSnapshot UNLOADED = new IrisRuntimeSnapshot(
false,
false,
false,
false,
false,
"",
IrisCompatibilityProfile.NONE,
"iris mod not loaded"
);
public String shortLine() {
if (!modLoaded) {
return "iris: not loaded";
}
if (!apiAvailable) {
return "iris: loaded, api unavailable";
}
String pack = shaderpackName == null || shaderpackName.isBlank() ? "<unknown>" : shaderpackName;
return "iris: " + (shadersEnabled ? "on" : "off")
+ ", pack " + (shaderpackInUse ? pack : "<none>")
+ ", profile " + profile.getId();
}
}

View file

@ -0,0 +1,154 @@
/*
* 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.iris.runtime;
import net.irisshaders.iris.gl.uniform.UniformHolder;
import net.irisshaders.iris.gl.uniform.UniformUpdateFrequency;
import net.minecraft.client.Minecraft;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.material.FogType;
import org.joml.Vector3f;
import dev.loki.lovisual.features.module.lifecycle.Modules;
import dev.loki.lovisual.features.module.modules.visuals.player.FullBright;
import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.light.LightmapState;
import dev.loki.lovisual.util.logging.DebugLog;
import dev.loki.lovisual.render.iris.compat.IrisCompatibilityGuards;
public enum LoVisualIrisUniforms {
;
private static boolean loggedRegistration;
public static void add(UniformHolder uniforms) {
if (!loggedRegistration) {
loggedRegistration = true;
DebugLog.renderThread("[IrisPatch] registering LoVisual Iris uniforms");
}
uniforms
.uniform1b(UniformUpdateFrequency.PER_FRAME, "lovisualFullbrightEnabled", LoVisualIrisUniforms::fullbrightEnabled)
.uniform1f(UniformUpdateFrequency.PER_FRAME, "lovisualFullbrightMinLight", LoVisualIrisUniforms::fullbrightMinLight)
.uniform1f(UniformUpdateFrequency.PER_FRAME, "lovisualAmbientLight", LightmapState::getAmbient)
.uniform1f(UniformUpdateFrequency.PER_FRAME, "lovisualBaseAmbientLight", LightmapState::getBaseAmbient)
.uniform1f(UniformUpdateFrequency.PER_FRAME, "lovisualOverrideAmbientLight", LoVisualIrisUniforms::overrideAmbient)
.uniform1b(UniformUpdateFrequency.PER_FRAME, "lovisualWorldTweaksFogControl", LoVisualIrisUniforms::fogControlEnabled)
.uniform1b(UniformUpdateFrequency.PER_FRAME, "lovisualWorldTweaksFogModify", LoVisualIrisUniforms::fogModifyEnabled)
.uniform1f(UniformUpdateFrequency.PER_FRAME, "lovisualWorldTweaksFogStart", LoVisualIrisUniforms::fogStart)
.uniform1f(UniformUpdateFrequency.PER_FRAME, "lovisualWorldTweaksFogEnd", LoVisualIrisUniforms::fogEnd)
.uniform3f(UniformUpdateFrequency.PER_FRAME, "lovisualWorldTweaksFogColor", LoVisualIrisUniforms::fogColor)
.uniform1i(UniformUpdateFrequency.PER_FRAME, "lovisualWorldTweaksFogDisableMask", LoVisualIrisUniforms::fogDisableMask)
.uniform1i(UniformUpdateFrequency.PER_FRAME, "lovisualEyeInWater", LoVisualIrisUniforms::eyeInWater)
.uniform1b(UniformUpdateFrequency.PER_FRAME, "lovisualWorldTweaksTimeOverride", LoVisualIrisUniforms::timeOverride)
.uniform1i(UniformUpdateFrequency.PER_FRAME, "lovisualWorldTweaksTimeTicks", LoVisualIrisUniforms::timeTicks)
.uniform1i(UniformUpdateFrequency.PER_FRAME, "lovisualWorldTweaksWeatherMode", LoVisualIrisUniforms::weatherMode)
.uniform1b(UniformUpdateFrequency.PER_FRAME, "lovisualSuppressShaderpackMotionBlur", IrisCompatibilityGuards::suppressShaderpackMotionBlur);
}
private static boolean fullbrightEnabled() {
FullBright fullBright = Modules.get(FullBright.class);
return fullBright != null && fullBright.isEnabled();
}
private static float fullbrightMinLight() {
FullBright fullBright = Modules.get(FullBright.class);
return fullBright != null ? fullBright.getMinLight() / 15.0f : 0.0f;
}
private static float overrideAmbient() {
return Math.max(0.0f, LightmapState.getOverrideAmbient());
}
private static WorldTweaks worldTweaks() {
return Modules.get(WorldTweaks.class);
}
private static boolean fogControlEnabled() {
WorldTweaks module = worldTweaks();
return module != null && module.isFogControlEnabled();
}
private static boolean fogModifyEnabled() {
WorldTweaks module = worldTweaks();
return module != null && module.isFogModifyEnabled();
}
private static float fogStart() {
WorldTweaks module = worldTweaks();
return module != null ? module.getFogStartBlocks() : 0.0f;
}
private static float fogEnd() {
WorldTweaks module = worldTweaks();
return module != null ? module.getFogEndBlocks() : 0.0f;
}
private static Vector3f fogColor() {
WorldTweaks module = worldTweaks();
int argb = module != null ? module.getFogColorArgb() : 0xFFFFFFFF;
return new Vector3f(
((argb >> 16) & 0xFF) / 255.0f,
((argb >> 8) & 0xFF) / 255.0f,
(argb & 0xFF) / 255.0f
);
}
private static int fogDisableMask() {
WorldTweaks module = worldTweaks();
if (module == null || !module.isFogControlEnabled()) {
return 0;
}
int mask = 0;
if (module.disableWaterFog()) mask |= 1;
if (module.disableLavaFog()) mask |= 1 << 1;
if (module.disablePowderSnowFog()) mask |= 1 << 2;
if (module.disableOverworldFog()) mask |= 1 << 3;
if (module.disableNetherFog()) mask |= 1 << 4;
if (module.disableEndFog()) mask |= 1 << 5;
if (module.disableDistanceFog()) mask |= 1 << 6;
if (module.disableWeatherFog()) mask |= 1 << 7;
return mask;
}
private static int eyeInWater() {
FogType submersion = RenderState.cameraSubmersion;
if (submersion == FogType.WATER) return 1;
if (submersion == FogType.LAVA) return 2;
if (submersion == FogType.POWDER_SNOW) return 3;
return 0;
}
private static boolean timeOverride() {
WorldTweaks module = worldTweaks();
return module != null && module.shouldOverrideTime();
}
private static int timeTicks() {
WorldTweaks module = worldTweaks();
if (module != null && module.shouldOverrideTime()) {
return module.getTimeOfDayTicks();
}
Minecraft client = Minecraft.getInstance();
return client != null && client.level != null ? (int) (client.level.getLevelData().getGameTime() % 24000L) : 0;
}
private static int weatherMode() {
WorldTweaks module = worldTweaks();
if (module != null && module.isWeatherControlEnabled()) {
return switch (module.getWeatherMode()) {
case RAIN -> 1;
case THUNDER -> 2;
default -> 0;
};
}
Minecraft client = Minecraft.getInstance();
Level world = client != null ? client.level : null;
if (world == null) return 0;
if (WorldTweaks.isServerThundering(world)) return 2;
return WorldTweaks.isServerRaining(world) ? 1 : 0;
}
}

View file

@ -0,0 +1,14 @@
/*
* 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.iris.scene;
import net.minecraft.client.DeltaTracker;
public interface IrisFinalizedSceneRenderer {
void lovisual$renderAfterIrisFinalization(DeltaTracker tickCounter);
}

View file

@ -0,0 +1,94 @@
/*
* 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.iris.scene;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.GpuTexture;
import com.mojang.blaze3d.textures.GpuTextureView;
import net.irisshaders.iris.targets.RenderTargets;
/**
* Read-only views of the resolved Iris scene depth used by LoVisual postprocess passes.
*/
public enum IrisSceneDepth {
;
private static GpuTexture mainTexture;
private static GpuTexture preTranslucentTexture;
private static GpuTexture preHandTexture;
private static GpuTextureView mainView;
private static GpuTextureView preTranslucentView;
private static GpuTextureView preHandView;
private static boolean valid;
public static void capture(RenderTargets targets) {
RenderSystem.assertOnRenderThread();
if (targets == null) {
valid = false;
return;
}
mainView = updateView(mainTexture, mainView, targets.getDepthTexture());
mainTexture = targets.getDepthTexture();
preTranslucentView = updateView(preTranslucentTexture, preTranslucentView, targets.getDepthTextureNoTranslucents());
preTranslucentTexture = targets.getDepthTextureNoTranslucents();
preHandView = updateView(preHandTexture, preHandView, targets.getDepthTextureNoHand());
preHandTexture = targets.getDepthTextureNoHand();
valid = mainView != null;
}
public static void resetFrame() {
valid = false;
}
public static void shutdown() {
valid = false;
close(mainView);
close(preTranslucentView);
close(preHandView);
mainTexture = null;
preTranslucentTexture = null;
preHandTexture = null;
mainView = null;
preTranslucentView = null;
preHandView = null;
}
public static boolean isValid() {
return valid && mainView != null;
}
public static GpuTextureView mainDepthView() {
return isValid() ? mainView : null;
}
public static GpuTextureView preTranslucentDepthView() {
return isValid() ? preTranslucentView : null;
}
public static GpuTextureView preHandDepthView() {
return isValid() ? preHandView : null;
}
private static GpuTextureView updateView(GpuTexture previousTexture,
GpuTextureView previousView,
GpuTexture nextTexture) {
if (nextTexture == previousTexture && previousView != null && !previousView.isClosed()) {
return previousView;
}
close(previousView);
if (nextTexture == null || nextTexture.isClosed()) {
return null;
}
return RenderSystem.getDevice().createTextureView(nextTexture);
}
private static void close(GpuTextureView view) {
if (view != null && !view.isClosed()) {
view.close();
}
}
}

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.iris.scene;
import net.irisshaders.iris.pipeline.WorldRenderingPipeline;
import net.minecraft.client.Minecraft;
import dev.loki.lovisual.render.engine.depth.WorldSceneDepth;
import dev.loki.lovisual.render.iris.compat.IrisCompatibilityGuards;
public enum IrisSecondHandScene {
;
private static WorldRenderingPipeline pendingPipeline;
private static boolean rendering;
public static boolean deferFinalization(WorldRenderingPipeline pipeline) {
if (rendering) {
return false;
}
if (!IrisCompatibilityGuards.deferIrisFinalizationForSecondHandScene()) {
return false;
}
Minecraft client = Minecraft.getInstance();
if (client != null) {
WorldSceneDepth.captureResolvedMain(client.gameRenderer.mainRenderTarget());
}
if (pendingPipeline != null && pendingPipeline != pipeline) {
pendingPipeline.finalizeLevelRendering();
}
pendingPipeline = pipeline;
return true;
}
public static boolean isRendering() {
return rendering;
}
public static void finalizeWorld() {
WorldRenderingPipeline pipeline = pendingPipeline;
pendingPipeline = null;
if (pipeline == null) {
return;
}
rendering = true;
try {
pipeline.finalizeLevelRendering();
} finally {
rendering = false;
}
}
}

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.iris.state;
import com.mojang.blaze3d.vertex.PoseStack;
public enum IrisHandMaskState {
;
public static void renderBothPhases(Runnable renderer) {
renderBothPhases(null, renderer);
}
public static void renderBothPhases(PoseStack matrices, Runnable renderer) {
if (matrices != null) {
matrices.pushPose();
}
try {
renderer.run();
} finally {
if (matrices != null) {
matrices.popPose();
}
}
}
}

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.iris.state;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import dev.loki.lovisual.util.logging.DebugLog;
import dev.loki.lovisual.render.iris.scene.IrisFinalizedSceneRenderer;
public enum IrisLoVisualFrameHooks {
;
private static DeltaTracker currentTickCounter;
private static boolean renderedAfterFinalization;
private static boolean rendering;
private static boolean loggedFrameHook;
private static boolean loggedMissingFrame;
private static boolean loggedFinalRender;
public static void beginRenderLevel(DeltaTracker tickCounter) {
currentTickCounter = tickCounter;
renderedAfterFinalization = false;
if (!loggedFrameHook) {
loggedFrameHook = true;
DebugLog.info("[IrisCompat] GameRenderer.renderLevel frame hook active");
}
}
public static void endRenderLevel(DeltaTracker tickCounter) {
if (currentTickCounter == tickCounter) {
currentTickCounter = null;
}
renderedAfterFinalization = false;
rendering = false;
}
public static boolean isRenderingAfterIrisFinalization() {
return rendering;
}
public static void renderAfterIrisFinalization() {
if (rendering || renderedAfterFinalization || currentTickCounter == null) {
if (currentTickCounter == null && !loggedMissingFrame) {
loggedMissingFrame = true;
DebugLog.info("[IrisCompat] Iris final pass hook fired without active GameRenderer frame");
}
return;
}
Minecraft mc = Minecraft.getInstance();
if (mc == null || !(mc.gameRenderer instanceof IrisFinalizedSceneRenderer renderer)) {
return;
}
renderedAfterFinalization = true;
rendering = true;
if (!loggedFinalRender) {
loggedFinalRender = true;
DebugLog.info("[IrisCompat] rendering LoVisual after Iris final pass");
}
try {
renderer.lovisual$renderAfterIrisFinalization(currentTickCounter);
} finally {
rendering = false;
}
}
}