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:
parent
ec10395511
commit
9d08fa910a
2239 changed files with 35265 additions and 28110 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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('+', ' ');
|
||||
}
|
||||
}
|
||||
|
|
@ -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); }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue