diff --git a/common/src/main/java/foundry/veil/api/client/render/VeilRenderer.java b/common/src/main/java/foundry/veil/api/client/render/VeilRenderer.java index c2547144a..afb375245 100644 --- a/common/src/main/java/foundry/veil/api/client/render/VeilRenderer.java +++ b/common/src/main/java/foundry/veil/api/client/render/VeilRenderer.java @@ -3,9 +3,7 @@ import com.mojang.blaze3d.platform.Window; import com.mojang.blaze3d.systems.RenderSystem; import foundry.veil.Veil; -import foundry.veil.VeilClient; import foundry.veil.api.client.editor.EditorManager; -import foundry.veil.api.client.editor.Inspector; import foundry.veil.api.client.render.dynamicbuffer.DynamicBufferType; import foundry.veil.api.client.render.framebuffer.FramebufferManager; import foundry.veil.api.client.render.light.renderer.LightRenderer; @@ -14,7 +12,6 @@ import foundry.veil.api.client.render.shader.ShaderManager; import foundry.veil.api.client.render.shader.ShaderModificationManager; import foundry.veil.api.client.render.shader.ShaderPreDefinitions; -import foundry.veil.api.event.VeilRegisterInspectorsEvent; import foundry.veil.api.flare.FlareEffectManager; import foundry.veil.api.quasar.particle.ParticleSystemManager; import foundry.veil.impl.client.render.dynamicbuffer.DynamicBufferManager; @@ -22,6 +19,7 @@ import foundry.veil.impl.client.render.pipeline.VeilBloomRenderer; import foundry.veil.impl.client.render.pipeline.VeilFirstPersonRenderer; import foundry.veil.impl.client.render.rendertype.DynamicRenderTypeManager; +import foundry.veil.impl.client.render.shader.injection.ShaderInjectionManager; import foundry.veil.mixin.pipeline.accessor.PipelineReloadableResourceManagerAccessor; import net.minecraft.ChatFormatting; import net.minecraft.resources.ResourceLocation; @@ -55,6 +53,7 @@ public class VeilRenderer implements ResourceManagerReloadListener { private final VanillaShaderCompiler vanillaShaderCompiler; private final DynamicBufferManager dynamicBufferManager; private final ShaderModificationManager shaderModificationManager; + private final ShaderInjectionManager shaderInjectionManager; private final ShaderPreDefinitions shaderPreDefinitions; private final ShaderManager shaderManager; private final FramebufferManager framebufferManager; @@ -73,6 +72,7 @@ public VeilRenderer(ReloadableResourceManager resourceManager, Window window) { this.dynamicBufferManager = new DynamicBufferManager(window.getWidth(), window.getHeight()); this.shaderPreDefinitions = new ShaderPreDefinitions(); this.shaderModificationManager = new ShaderModificationManager(); + this.shaderInjectionManager = new ShaderInjectionManager(); this.shaderManager = new ShaderManager(ShaderManager.PROGRAM_SET, this.shaderPreDefinitions, this.dynamicBufferManager); this.framebufferManager = new FramebufferManager(); this.postProcessingManager = new PostProcessingManager(); @@ -87,7 +87,7 @@ public VeilRenderer(ReloadableResourceManager resourceManager, Window window) { List listeners = ((PipelineReloadableResourceManagerAccessor) resourceManager).getListeners(); // This must finish loading before the game renderer so modifications can apply on load - listeners.add(0, this.shaderModificationManager); + listeners.add(0, this.shaderInjectionManager); // This must be before vanilla shaders so vanilla shaders can be replaced listeners.add(1, this.shaderManager); resourceManager.registerReloadListener(this.framebufferManager); @@ -183,10 +183,20 @@ public DynamicBufferManager getDynamicBufferManger() { /** * @return The manager for all custom shader modifications */ + @ApiStatus.ScheduledForRemoval(inVersion = "5.0.0") + @Deprecated(forRemoval = true) public ShaderModificationManager getShaderModificationManager() { return this.shaderModificationManager; } + /** + * @return The manager for all shader injections + */ + @ApiStatus.Internal + public ShaderInjectionManager getShaderInjectionManager() { + return this.shaderInjectionManager; + } + /** * @return The set of shader pre-definitions. Changes are automatically synced the next frame */ diff --git a/common/src/main/java/foundry/veil/api/client/render/shader/ShaderManager.java b/common/src/main/java/foundry/veil/api/client/render/shader/ShaderManager.java index aa2410c7d..77ca24fb5 100644 --- a/common/src/main/java/foundry/veil/api/client/render/shader/ShaderManager.java +++ b/common/src/main/java/foundry/veil/api/client/render/shader/ShaderManager.java @@ -146,7 +146,7 @@ private void addProcessors(ShaderProcessorList processorList, ResourceProvider p processorList.addPreprocessor(new ShaderBufferProcessor()); processorList.addPreprocessor(new ShaderBindingProcessor()); processorList.addPreprocessor(new ShaderVersionProcessor(), false); - processorList.addPreprocessor(new ShaderModifyProcessor(), false); + processorList.addPreprocessor(new ShaderInjectProcessor(), false); processorList.addPreprocessor(new DynamicBufferProcessor(), false); processorList.addPreprocessor(new ShaderFeatureProcessor(), false); VeilClient.clientPlatform().onRegisterShaderPreProcessors(provider, processorList); diff --git a/common/src/main/java/foundry/veil/api/client/render/shader/ShaderModificationManager.java b/common/src/main/java/foundry/veil/api/client/render/shader/ShaderModificationManager.java index 78b58e706..a445a1a6f 100644 --- a/common/src/main/java/foundry/veil/api/client/render/shader/ShaderModificationManager.java +++ b/common/src/main/java/foundry/veil/api/client/render/shader/ShaderModificationManager.java @@ -1,182 +1,39 @@ package foundry.veil.api.client.render.shader; -import foundry.veil.Veil; -import foundry.veil.impl.client.render.shader.modifier.*; +import foundry.veil.impl.client.render.shader.injection.ShaderInjectionManager; import io.github.ocelot.glslprocessor.api.node.GlslTree; -import net.minecraft.resources.FileToIdConverter; import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; -import net.minecraft.server.packs.resources.ResourceProvider; import net.minecraft.server.packs.resources.SimplePreparableReloadListener; -import net.minecraft.util.StringUtil; import net.minecraft.util.profiling.ProfilerFiller; -import org.apache.commons.io.IOUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.Reader; -import java.util.*; -import java.util.regex.Pattern; /** - * Manages modifications for both vanilla and Veil shader files. - * - * @author Ocelot + * @deprecated Use {@link ShaderInjectionManager} instead. */ +@ApiStatus.ScheduledForRemoval(inVersion = "5.0.0") +@Deprecated(forRemoval = true) public class ShaderModificationManager extends SimplePreparableReloadListener { - public static final FileToIdConverter MODIFIER_LISTER = new FileToIdConverter("pinwheel/shader_modifiers", ".txt"); - private static final Map NEXT_STAGES = Map.of( - "vsh", "gsh", - "gsh", "fsh" - ); - private static final Pattern OUT_PATTERN = Pattern.compile("out "); - - private Map> shaders; - private Map names; - - public ShaderModificationManager() { - this.shaders = Collections.emptyMap(); - this.names = Collections.emptyMap(); - } - - /** - * Applies all shader modifiers to the specified shader source. - * - * @param shaderId The id of the shader to get modifiers for - * @param tree The shader source tree - * @param flags Additional flags for applying modifiers - * @see ShaderModification - */ public void applyModifiers(ResourceLocation shaderId, GlslTree tree, int flags) { - Collection modifiers = this.getModifiers(shaderId); - if (modifiers.isEmpty()) { - return; - } - - try { - VeilJobParameters parameters = new VeilJobParameters(this, shaderId, flags); - for (ShaderModification modifier : modifiers) { - modifier.inject(tree, parameters); - } - } catch (Exception e) { - Veil.LOGGER.error("Failed to transform shader: {}", shaderId, e); - } - } - - /** - * Retrieves all modifiers for the specified shader. - * - * @param shaderId The shader to get all modifiers for - * @return The modifiers applied to the specified shader - */ - public List getModifiers(ResourceLocation shaderId) { - return this.shaders.getOrDefault(shaderId, Collections.emptyList()); - } - - /** - * Retrieves the id of the specified modifier. - * - * @param modification The modification to get the id of - * @return The id of that modification or null if unregistered - */ - public @Nullable ResourceLocation getModifierId(ShaderModification modification) { - return this.names.get(modification); - } - - private @Nullable ResourceLocation getNextStage(ResourceLocation shader, ResourceProvider resourceProvider) { - String[] parts = shader.getPath().split("\\."); - String extension = parts[parts.length - 1].toLowerCase(Locale.ROOT); - - while (extension != null) { - extension = NEXT_STAGES.get(extension); - - ResourceLocation nextShader = ResourceLocation.fromNamespaceAndPath(shader.getNamespace(), shader.getPath().substring(0, shader.getPath().length() - 3) + extension); - if (resourceProvider.getResource(nextShader).isPresent()) { - return nextShader; - } - } - return null; } @Override protected @NotNull Preparations prepare(@NotNull ResourceManager resourceManager, @NotNull ProfilerFiller profilerFiller) { - Map> modifiers = new HashMap<>(); - Map names = new HashMap<>(); - - for (Map.Entry entry : MODIFIER_LISTER.listMatchingResources(resourceManager).entrySet()) { - ResourceLocation file = entry.getKey(); - ResourceLocation id = MODIFIER_LISTER.fileToId(file); - - try { - String[] parts = id.getPath().split("/", 2); - if (parts.length < 2) { - Veil.LOGGER.warn("Ignoring shader modifier {}. Expected format to be located in shader_modifiers/domain/shader_path.vsh.txt", file); - continue; - } - - ResourceLocation shaderId = ResourceLocation.fromNamespaceAndPath(parts[0], parts[1]); - try (Reader reader = entry.getValue().openAsReader()) { - ShaderModification modification = ShaderModification.parse(IOUtils.toString(reader), shaderId.getPath().endsWith(".vsh")); - List modifications = modifiers.computeIfAbsent(shaderId, name -> new LinkedList<>()); - - if (modification instanceof ReplaceShaderModification) { - // TODO This doesn't respect priority - modifications.clear(); - } - if (modifications.size() != 1 || !(modifications.getFirst() instanceof ReplaceShaderModification)) { - modifications.add(modification); - } - names.put(modification, id); - } - } catch (Exception e) { - Veil.LOGGER.error("Couldn't parse data file {} from {}", id, file, e); - } - } - - // Inject inputs to next shader stage - for (Map.Entry> entry : new HashMap<>(modifiers).entrySet()) { - ResourceLocation nextStage = null; - - for (ShaderModification modification : entry.getValue()) { - if (!(modification instanceof SimpleShaderModification simpleMod)) { - continue; - } - - String output = simpleMod.getOutput(); - if (StringUtil.isNullOrEmpty(output)) { - continue; - } - - if (nextStage == null) { - nextStage = this.getNextStage(entry.getKey(), resourceManager); - } - if (nextStage == null) { - // No need to inject in into next shader - break; - } - - InputShaderModification input = new InputShaderModification(simpleMod.priority(), OUT_PATTERN.matcher(simpleMod.fillPlaceholders(simpleMod.getOutput())).replaceAll("in ")); - modifiers.computeIfAbsent(nextStage, unused -> new LinkedList<>()).add(input); - names.put(input, names.get(simpleMod)); - } - } - modifiers.values().forEach(modifications -> modifications.sort(Comparator.comparingInt(ShaderModification::priority).thenComparing(names::get))); - - return new Preparations(modifiers, names); + return new Preparations(); } @Override protected void apply(@NotNull Preparations preparations, @NotNull ResourceManager resourceManager, @NotNull ProfilerFiller profilerFiller) { - this.shaders = Collections.unmodifiableMap(preparations.shaders); - this.names = Collections.unmodifiableMap(preparations.names); - Veil.LOGGER.info("Loaded {} shader modifications", this.names.size()); } + /** + * @deprecated Use {@link ShaderInjectionManager} instead. + */ + @ApiStatus.ScheduledForRemoval(inVersion = "5.0.0") + @Deprecated(forRemoval = true) @ApiStatus.Internal - public record Preparations(Map> shaders, - Map names) { + public record Preparations() { } } diff --git a/common/src/main/java/foundry/veil/api/client/render/shader/processor/ShaderInjectProcessor.java b/common/src/main/java/foundry/veil/api/client/render/shader/processor/ShaderInjectProcessor.java new file mode 100644 index 000000000..e36bdfdb3 --- /dev/null +++ b/common/src/main/java/foundry/veil/api/client/render/shader/processor/ShaderInjectProcessor.java @@ -0,0 +1,65 @@ +package foundry.veil.api.client.render.shader.processor; + +import foundry.veil.Veil; +import foundry.veil.api.client.render.VeilRenderSystem; +import foundry.veil.impl.client.render.shader.injection.ShaderInjectionManager; +import foundry.veil.impl.client.render.shader.injection.util.ShaderInjection; +import io.github.ocelot.glslprocessor.api.GlslSyntaxException; +import io.github.ocelot.glslprocessor.api.node.GlslTree; +import io.github.ocelot.glslprocessor.lib.anarres.cpp.LexerException; +import net.minecraft.resources.ResourceLocation; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Injects shader modifications into shader source files. + * + * @author Ocelot + * @author Vowxky + */ +public class ShaderInjectProcessor implements ShaderPreProcessor { + + private final ShaderInjectionManager shaderInjectionManager; + private final Set appliedModifications; + + public ShaderInjectProcessor() { + this.shaderInjectionManager = VeilRenderSystem.renderer().getShaderInjectionManager(); + this.appliedModifications = new HashSet<>(); + } + + @Override + public void prepare() { + this.appliedModifications.clear(); + } + + @Override + public void modify(Context ctx, GlslTree tree) throws IOException, GlslSyntaxException, LexerException { + ResourceLocation name = ctx.name(); + if (name == null || !this.appliedModifications.add(name)) { + return; + } + + boolean applyVersion = ctx.isSourceFile(); + for (ResourceLocation include : ctx.shaderImporter().addedImports()) { + this.applyModifiers(include, tree, applyVersion); + } + this.applyModifiers(name, tree, applyVersion); + } + + private void applyModifiers(ResourceLocation shaderId, GlslTree tree, boolean applyVersion) { + List modifiers = this.shaderInjectionManager.getModifiers(shaderId); + if (modifiers.isEmpty()) { + return; + } + try { + for (ShaderInjection modifier : modifiers) { + modifier.inject(tree, applyVersion); + } + } catch (Exception e) { + Veil.LOGGER.error("Failed to transform shader: {}", shaderId, e); + } + } +} diff --git a/common/src/main/java/foundry/veil/api/client/render/shader/processor/ShaderModifyProcessor.java b/common/src/main/java/foundry/veil/api/client/render/shader/processor/ShaderModifyProcessor.java index c54bad923..5a712842e 100644 --- a/common/src/main/java/foundry/veil/api/client/render/shader/processor/ShaderModifyProcessor.java +++ b/common/src/main/java/foundry/veil/api/client/render/shader/processor/ShaderModifyProcessor.java @@ -1,12 +1,12 @@ package foundry.veil.api.client.render.shader.processor; import foundry.veil.api.client.render.VeilRenderSystem; -import foundry.veil.api.client.render.shader.ShaderModificationManager; -import foundry.veil.impl.client.render.shader.modifier.VeilJobParameters; +import foundry.veil.impl.client.render.shader.injection.ShaderInjectionManager; import io.github.ocelot.glslprocessor.api.GlslSyntaxException; import io.github.ocelot.glslprocessor.api.node.GlslTree; import io.github.ocelot.glslprocessor.lib.anarres.cpp.LexerException; import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; import java.io.IOException; import java.util.HashSet; @@ -16,14 +16,17 @@ * Modifies shader sources with the shader modification system. * * @author Ocelot + * @deprecated Use {@link ShaderInjectProcessor} instead. */ +@ApiStatus.ScheduledForRemoval(inVersion = "5.0.0") +@Deprecated(forRemoval = true) public class ShaderModifyProcessor implements ShaderPreProcessor { - private final ShaderModificationManager shaderModificationManager; + private final ShaderInjectionManager shaderInjectionManager; private final Set appliedModifications; public ShaderModifyProcessor() { - this.shaderModificationManager = VeilRenderSystem.renderer().getShaderModificationManager(); + this.shaderInjectionManager = VeilRenderSystem.renderer().getShaderInjectionManager(); this.appliedModifications = new HashSet<>(); } @@ -38,10 +41,10 @@ public void modify(Context ctx, GlslTree tree) throws IOException, GlslSyntaxExc if (name == null || !this.appliedModifications.add(name)) { return; } - int flags = ctx.isSourceFile() ? VeilJobParameters.APPLY_VERSION | VeilJobParameters.ALLOW_OUT : 0; - for (ResourceLocation include : ctx.shaderImporter().addedImports()) { // Run include modifiers first - this.shaderModificationManager.applyModifiers(include, tree, flags); + boolean applyVersion = ctx.isSourceFile(); + for (ResourceLocation include : ctx.shaderImporter().addedImports()) { + this.shaderInjectionManager.applyModifiers(include, tree, applyVersion); } - this.shaderModificationManager.applyModifiers(name, tree, flags); + this.shaderInjectionManager.applyModifiers(name, tree, applyVersion); } } diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/injection/InjectionBodyParser.java b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/InjectionBodyParser.java new file mode 100644 index 000000000..804b3f29d --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/InjectionBodyParser.java @@ -0,0 +1,127 @@ +package foundry.veil.impl.client.render.shader.injection; + +import org.jetbrains.annotations.ApiStatus; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@ApiStatus.Internal +public final class InjectionBodyParser { + + private static final Pattern VERSION_PATTERN = Pattern.compile("^\\s*#version\\s+(\\d+)\\s*.*$", Pattern.MULTILINE); + + public static Result parse(String code) { + int version = parseVersion(code); + String trimmed = code.trim(); + int funcStart = -1; + boolean isHead = true; + int bracePos = -1; + + if (trimmed.startsWith("void tail(")) { + funcStart = 0; + isHead = false; + bracePos = trimmed.indexOf('{'); + } else if (trimmed.startsWith("void head(")) { + funcStart = 0; + bracePos = trimmed.indexOf('{'); + } else { + int idx = trimmed.indexOf("\nvoid tail("); + if (idx >= 0) { + funcStart = idx + 1; + isHead = false; + bracePos = trimmed.indexOf('{', funcStart); + } else { + idx = trimmed.indexOf("\nvoid head("); + if (idx >= 0) { + funcStart = idx + 1; + bracePos = trimmed.indexOf('{', funcStart); + } + } + } + + if (bracePos < 0) { + return new Result("", "", true, version); + } + + int depth = 0; + int bodyStart = -1; + for (int i = bracePos; i < trimmed.length(); i++) { + char c = trimmed.charAt(i); + int skip = skipIfCommentOrString(trimmed, i); + if (skip >= 0) { + i = skip; + continue; + } + if (c == '{') { + if (depth == 0) bodyStart = i + 1; + depth++; + } else if (c == '}') { + depth--; + if (depth == 0) { + String bodyStr = trimmed.substring(bodyStart, i).trim(); + String rawGlobals = trimmed.substring(0, funcStart) + trimmed.substring(i + 1); + String globalsStr = stripNonCode(rawGlobals); + return new Result(bodyStr, globalsStr, isHead, version); + } + } + } + + return new Result("", "", true, version); + } + + private static int skipString(CharSequence s, int pos) { + for (int i = pos + 1; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\') i++; + else if (c == '"') return i; + } + return s.length() - 1; + } + + private static int skipLineComment(CharSequence s, int pos) { + for (int i = pos + 2; i < s.length(); i++) { + if (s.charAt(i) == '\n') return i; + } + return s.length() - 1; + } + + private static int skipBlockComment(CharSequence s, int pos) { + for (int i = pos + 2; i < s.length() - 1; i++) { + if (s.charAt(i) == '*' && s.charAt(i + 1) == '/') return i + 1; + } + return s.length() - 1; + } + + private static int parseVersion(String code) { + Matcher m = VERSION_PATTERN.matcher(code); + return m.find() ? Integer.parseInt(m.group(1)) : -1; + } + + private static String stripNonCode(String s) { + StringBuilder out = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + int skip = skipIfCommentOrString(s, i); + if (skip >= 0) { + if (s.charAt(i) == '"') out.append(s, i, skip + 1); + i = skip; + continue; + } + out.append(s.charAt(i)); + } + return out.toString().trim(); + } + + private static int skipIfCommentOrString(CharSequence s, int i) { + char c = s.charAt(i); + if (c == '"') return skipString(s, i); + if (c == '/' && i + 1 < s.length()) { + char n = s.charAt(i + 1); + if (n == '/') return skipLineComment(s, i); + if (n == '*') return skipBlockComment(s, i); + } + return -1; + } + + public record Result(String body, String globals, boolean isHead, int version) { + } +} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/injection/ShaderInjectionAdapter.java b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/ShaderInjectionAdapter.java new file mode 100644 index 000000000..6ced00d31 --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/ShaderInjectionAdapter.java @@ -0,0 +1,130 @@ +package foundry.veil.impl.client.render.shader.injection; + +import foundry.veil.Veil; +import foundry.veil.impl.client.render.shader.injection.util.ShaderInjection; +import foundry.veil.impl.client.render.shader.injection.util.ShaderInjectionDefinition; +import foundry.veil.impl.client.render.shader.injection.util.ShaderInjectionFunction; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.resources.FileToIdConverter; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceProvider; +import org.jetbrains.annotations.ApiStatus; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Adapts JSON-based shader injection definitions into + * {@link ShaderInjection} objects for GLSL tree transformation. + * + * @author Vowxky + */ +@ApiStatus.Internal +public final class ShaderInjectionAdapter { + + private static final String DEFAULT_FUNCTION = "main"; + private static final int DEFAULT_PARAM_COUNT = -1; + private static final Pattern INCLUDE_PATTERN = Pattern.compile("^\\s*#include\\s+[\"<]?([^>\"]+)[>\"]?\\s*$"); + private static final Pattern DEFINE_PATTERN = Pattern.compile("^\\s*#define\\s+(\\w+)\\s*(.*?)\\s*$"); + private static final FileToIdConverter INCLUDE_LISTER = new FileToIdConverter("pinwheel/shaders/include", ".glsl"); + private static final Set DEBUG_LOGGED = new HashSet<>(); + + public static List toModifications(ShaderInjectionDefinition definition, ResourceProvider provider) throws IOException { + List redirects = definition.redirects(); + List mods = new ObjectArrayList<>(redirects.size()); + for (ResourceLocation path : redirects) { + String code = loadGlsl(path, provider); + mods.add(buildModification(definition, code)); + } + return mods; + } + + private static ShaderInjection buildModification(ShaderInjectionDefinition def, String code) { + code = expandDefines(code); + InjectionBodyParser.Result parsed = InjectionBodyParser.parse(code); + + if (parsed.body().isEmpty() && parsed.globals().isEmpty()) { + Veil.LOGGER.warn("Shader injection '{}' target '{}' has no head() or tail() marker — skipping", def.id(), def.target()); + return new SimpleShaderInjection(parsed.version(), def.priority(), new ShaderInjectionFunction[0], null); + } + + if (def.debug() && DEBUG_LOGGED.add(def.id() != null ? def.id().toString() : String.valueOf(def.target()))) { + StringBuilder sb = new StringBuilder("\n═══ Injection: ").append(def.target()).append(" ═══\n"); + sb.append(" Version: ").append(parsed.version() >= 0 ? parsed.version() : "auto").append('\n'); + sb.append(" Body: ").append(parsed.body().replace("\n", "\n ")).append('\n'); + if (!parsed.globals().isEmpty()) { + sb.append(" Globals: ").append(parsed.globals().replace("\n", "\n ")); + } + Veil.LOGGER.info(sb.toString()); + } + + ShaderInjectionFunction func = new ShaderInjectionFunction(DEFAULT_FUNCTION, DEFAULT_PARAM_COUNT, parsed.isHead(), parsed.body()); + return new SimpleShaderInjection(parsed.version(), def.priority(), new ShaderInjectionFunction[]{func}, parsed.globals().isEmpty() ? null : parsed.globals()); + } + + private static String loadGlsl(ResourceLocation path, ResourceProvider provider) throws IOException { + ResourceLocation fullPath = ResourceLocation.fromNamespaceAndPath(path.getNamespace(), "pinwheel/shader_injection/" + path.getPath()); + return resolveIncludes(fullPath, provider, new HashSet<>()); + } + + private static String resolveIncludes(ResourceLocation loc, ResourceProvider provider, Set visited) throws IOException { + if (!visited.add(loc)) { + return ""; + } + String source; + try (BufferedReader reader = provider.openAsReader(loc)) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append('\n'); + } + source = sb.toString(); + } + + StringBuilder out = new StringBuilder(); + for (String line : source.split("\n", -1)) { + Matcher m = INCLUDE_PATTERN.matcher(line); + if (m.matches()) { + ResourceLocation includeId = ResourceLocation.parse(m.group(1)); + ResourceLocation includePath = INCLUDE_LISTER.idToFile(includeId); + out.append(resolveIncludes(includePath, provider, visited)); + } else { + out.append(line).append('\n'); + } + } + return out.toString(); + } + + private static String expandDefines(String source) { + List defines = new ArrayList<>(); + StringBuilder result = new StringBuilder(); + + for (String line : source.split("\n", -1)) { + Matcher m = DEFINE_PATTERN.matcher(line); + if (m.matches()) { + defines.add(new String[]{m.group(1), m.group(2)}); + } else { + if (!result.isEmpty()) { + result.append('\n'); + } + result.append(line); + } + } + + String expanded = result.toString(); + for (String[] define : defines) { + String name = define[0]; + String value = define[1]; + if (!value.isEmpty()) { + expanded = expanded.replaceAll("\\b" + name + "\\b", Matcher.quoteReplacement(value)); + } + } + return expanded; + } +} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/injection/ShaderInjectionLoader.java b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/ShaderInjectionLoader.java new file mode 100644 index 000000000..4db8c2506 --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/ShaderInjectionLoader.java @@ -0,0 +1,69 @@ +package foundry.veil.impl.client.render.shader.injection; + +import com.google.gson.Gson; +import foundry.veil.Veil; +import foundry.veil.impl.client.render.shader.injection.util.ShaderInjectionDefinition; +import foundry.veil.impl.client.render.shader.injection.util.ValidationResult; +import net.minecraft.resources.FileToIdConverter; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Unmodifiable; + +import java.io.Reader; +import java.util.*; + +/** + * Loads and indexes shader injection definitions + * from {@code pinwheel/shader_injection/*.json} resources. + * + * @author Vowxky + */ +@ApiStatus.Internal +public final class ShaderInjectionLoader { + + public static final FileToIdConverter INJECTION_LISTER = new FileToIdConverter("pinwheel/shader_injection", ".json"); + private static final Gson GSON = ShaderInjectionDefinition.createGson(); + + public static LoadedPatches load(ResourceManager resourceManager) { + List patches = new LinkedList<>(); + Map> byTarget = new LinkedHashMap<>(); + + for (Map.Entry entry : INJECTION_LISTER.listMatchingResources(resourceManager).entrySet()) { + ResourceLocation file = entry.getKey(); + ResourceLocation defaultId = INJECTION_LISTER.fileToId(file); + + try (Reader reader = entry.getValue().openAsReader()) { + ShaderInjectionDefinition definition = GSON.fromJson(reader, ShaderInjectionDefinition.class).withDefaultId(defaultId); + ValidationResult validation = ShaderInjectionValidator.validate(definition, file.toString()); + if (!validation.isValid()) { + Veil.LOGGER.warn("Skipping invalid shader injection {} from {}: {}", defaultId, file, validation.diagnostics()); + continue; + } + patches.add(new LoadedPatch(file, definition)); + } catch (Exception e) { + Veil.LOGGER.error("Couldn't load shader injection {} from {}", defaultId, file, e); + } + } + + for (LoadedPatch patch : patches) { + for (ResourceLocation target : patch.definition().targets()) { + byTarget.computeIfAbsent(target, k -> new ArrayList<>()).add(patch); + } + } + byTarget.values().forEach(g -> g.sort(Comparator.comparingInt(p -> p.definition().priority()))); + byTarget.replaceAll((t, g) -> List.copyOf(g)); + return new LoadedPatches(byTarget); + } + + public record LoadedPatches(@Unmodifiable Map> byTarget) { + + public LoadedPatches { + byTarget = Map.copyOf(byTarget); + } + } + + public record LoadedPatch(ResourceLocation resourceLocation, ShaderInjectionDefinition definition) { + } +} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/injection/ShaderInjectionManager.java b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/ShaderInjectionManager.java new file mode 100644 index 000000000..3bf78502c --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/ShaderInjectionManager.java @@ -0,0 +1,148 @@ +package foundry.veil.impl.client.render.shader.injection; + +import foundry.veil.Veil; +import foundry.veil.impl.client.render.shader.injection.util.ShaderInjection; +import foundry.veil.impl.client.render.shader.injection.util.ShaderInjectionDefinition; +import io.github.ocelot.glslprocessor.api.node.GlslTree; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.regex.Pattern; + +/** + * Manages modifications for both vanilla and Veil shader files. + * + * @author Ocelot, Vowxky + */ +@ApiStatus.Internal +public class ShaderInjectionManager extends SimplePreparableReloadListener { + + private static final Pattern SHADER_EXTENSION = Pattern.compile("\\.(fsh|vsh|tcsh|tesh|gsh|comp)$"); + + private Map> shaders = Collections.emptyMap(); + private Map names = Collections.emptyMap(); + private Map replacements = Collections.emptyMap(); + + /** + * Applies all registered shader modifications to the specified GLSL tree. + */ + public void applyModifiers(ResourceLocation shaderId, GlslTree tree, boolean applyVersion) { + Collection modifiers = this.getModifiers(shaderId); + if (modifiers.isEmpty()) return; + try { + for (ShaderInjection modifier : modifiers) { + modifier.inject(tree, applyVersion); + } + } catch (Exception e) { + Veil.LOGGER.error("Failed to transform shader: {}", shaderId, e); + } + } + + /** + * Fetches all registered modifications for the specified shader. + */ + public List getModifiers(ResourceLocation shaderId) { + return this.shaders.getOrDefault(shaderId, Collections.emptyList()); + } + + /** + * Returns the resource ID under which the given modification was registered, or null. + */ + @Nullable + public ResourceLocation getModifierId(ShaderInjection modification) { + return this.names.get(modification); + } + + /** + * Returns the replacement shader ID for the target, or null if none. + */ + @Nullable + public ResourceLocation getReplacement(ResourceLocation target) { + ResourceLocation stripped = stripShaderExtension(target); + return this.replacements.get(stripped != null ? stripped : target); + } + + /** + * Strips the shader extension (.fsh, .vsh, etc.) from a path, or null if none. + */ + @Nullable + private static ResourceLocation stripShaderExtension(ResourceLocation location) { + String path = location.getPath(); + String stripped = SHADER_EXTENSION.matcher(path).replaceFirst(""); + return !stripped.equals(path) ? ResourceLocation.fromNamespaceAndPath(location.getNamespace(), stripped) : null; + } + + /** + * Loads and adapts all shader injection JSON definitions from resource packs. + */ + @Override + protected @NotNull Preparations prepare(@NotNull ResourceManager resourceManager, @NotNull ProfilerFiller profilerFiller) { + Map> modifiers = new HashMap<>(); + Map names = new HashMap<>(); + Map replacements = new HashMap<>(); + + ShaderInjectionLoader.LoadedPatches loaded = ShaderInjectionLoader.load(resourceManager); + for (Map.Entry> entry : loaded.byTarget().entrySet()) { + ResourceLocation target = entry.getKey(); + List patches = entry.getValue(); + + List mods = modifiers.computeIfAbsent(target, k -> new LinkedList<>()); + for (ShaderInjectionLoader.LoadedPatch patch : patches) { + ShaderInjectionDefinition definition = patch.definition(); + if (definition.replace() != null) { + ResourceLocation replaceTarget = stripShaderExtension(target); + if (replaceTarget == null) { + replaceTarget = target; + } + replacements.put(replaceTarget, definition.replace()); + if (definition.debug()) { + Veil.LOGGER.info("\n═══ Replace: {} ═══\n Target: {}\n Replace: {}\n Source: {}", + replaceTarget, + target, + definition.replace(), + patch.resourceLocation()); + } + continue; + } + + try { + for (ShaderInjection m : ShaderInjectionAdapter.toModifications(definition, resourceManager)) { + mods.add(m); + names.put(m, definition.id() != null ? definition.id() : patch.resourceLocation()); + } + } catch (Throwable t) { + Veil.LOGGER.error("Couldn't adapt shader injection {}", patch.resourceLocation(), t); + } + } + } + + return new Preparations(modifiers, names, replacements); + } + + /** + * Applies the loaded preparations to the live injection and replacement maps. + */ + @Override + protected void apply(@NotNull Preparations preparations, @NotNull ResourceManager resourceManager, @NotNull ProfilerFiller profilerFiller) { + this.shaders = Collections.unmodifiableMap(preparations.shaders); + this.names = Collections.unmodifiableMap(preparations.names); + this.replacements = Collections.unmodifiableMap(preparations.replacements); + Veil.LOGGER.info("Loaded {} shader redirects, {} replacements", this.names.size(), this.replacements.size()); + if (!this.replacements.isEmpty()) { + Veil.LOGGER.debug("Active shader replacements:"); + this.replacements.forEach((target, replacement) -> Veil.LOGGER.debug(" {} -> {}", target, replacement)); + } + } + + @ApiStatus.Internal + public record Preparations(Map> shaders, + Map names, + Map replacements) { + } +} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/injection/ShaderInjectionValidator.java b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/ShaderInjectionValidator.java new file mode 100644 index 000000000..711335dac --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/ShaderInjectionValidator.java @@ -0,0 +1,66 @@ +package foundry.veil.impl.client.render.shader.injection; + +import foundry.veil.impl.client.render.shader.injection.util.Diagnostic; +import foundry.veil.impl.client.render.shader.injection.util.Severity; +import foundry.veil.impl.client.render.shader.injection.util.ShaderInjectionDefinition; +import foundry.veil.impl.client.render.shader.injection.util.ValidationResult; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Validates {@link ShaderInjectionDefinition} instances. + * + * @author Vowxky + */ +@ApiStatus.Internal +public final class ShaderInjectionValidator { + + public static ValidationResult validate(ShaderInjectionDefinition injection) { + return validate(injection, null); + } + + public static ValidationResult validate(ShaderInjectionDefinition injection, @Nullable String resourcePath) { + List diagnostics = new ArrayList<>(); + validateInjection(injection, resourcePath, diagnostics); + return new ValidationResult(diagnostics); + } + + public static ValidationResult validate(Collection injections) { + return validate(injections, null); + } + + public static ValidationResult validate(Collection injections, @Nullable String resourcePath) { + List diagnostics = new ArrayList<>(); + for (ShaderInjectionDefinition injection : injections) { + validateInjection(injection, resourcePath, diagnostics); + } + return new ValidationResult(diagnostics); + } + + private static void validateInjection(ShaderInjectionDefinition injection, @Nullable String resourcePath, List diagnostics) { + if (injection == null) { + diagnostics.add(new Diagnostic(resourcePath, null, null, "injection", "Injection definition is null", Severity.ERROR)); + return; + } + + if (injection.targets().isEmpty()) { + diagnostics.add(new Diagnostic(resourcePath, injection.id(), null, "targets", "Missing targets", Severity.ERROR)); + } + + boolean hasReplace = injection.replace() != null; + boolean hasRedirects = !injection.redirects().isEmpty(); + + if (hasReplace && hasRedirects) { + diagnostics.add(new Diagnostic(resourcePath, injection.id(), injection.target(), "replace", "Replace cannot be combined with redirect", Severity.ERROR)); + } + + if (!hasReplace && !hasRedirects) { + diagnostics.add(new Diagnostic(resourcePath, injection.id(), injection.target(), "redirect", "No redirect or replace specified", Severity.ERROR)); + } + } + +} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/injection/SimpleShaderInjection.java b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/SimpleShaderInjection.java new file mode 100644 index 000000000..0558eaa82 --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/SimpleShaderInjection.java @@ -0,0 +1,89 @@ +package foundry.veil.impl.client.render.shader.injection; + +import foundry.veil.impl.client.render.shader.injection.util.ShaderInjection; +import foundry.veil.impl.client.render.shader.injection.util.ShaderInjectionFunction; +import io.github.ocelot.glslprocessor.api.GlslParser; +import io.github.ocelot.glslprocessor.api.GlslSyntaxException; +import io.github.ocelot.glslprocessor.api.grammar.GlslVersionStatement; +import io.github.ocelot.glslprocessor.api.node.GlslNode; +import io.github.ocelot.glslprocessor.api.node.GlslTree; +import io.github.ocelot.glslprocessor.api.node.function.GlslFunctionNode; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.List; + +@ApiStatus.Internal +public class SimpleShaderInjection implements ShaderInjection { + + private final int version; + private final int priority; + private final ShaderInjectionFunction[] functions; + private final String globals; + + public SimpleShaderInjection(int version, int priority, ShaderInjectionFunction[] functions) { + this(version, priority, functions, null); + } + + public SimpleShaderInjection(int version, int priority, ShaderInjectionFunction[] functions, @Nullable String globals) { + this.version = version; + this.priority = priority; + this.functions = functions; + this.globals = globals; + } + + @Override + public void inject(GlslTree tree, boolean applyVersion) throws GlslSyntaxException, IOException { + if (applyVersion && this.version >= 0) { + GlslVersionStatement version = tree.getVersionStatement(); + if (version.getVersion() < this.version) { + version.setVersion(this.version); + } + } + + if (this.globals != null && !this.globals.isEmpty()) { + tree.getBody().addAll(0, GlslParser.parse(this.globals).getBody()); + } + + for (ShaderInjectionFunction function : this.functions) { + String name = function.name(); + List body = tree.functions().filter(definition -> { + if (definition == null || definition.getBody() == null) { + return false; + } + if (!name.equals(definition.getName())) { + return false; + } + int paramCount = function.parameters(); + return paramCount == -1 || definition.getHeader().getParameters().size() == paramCount; + }) + .findFirst() + .map(GlslFunctionNode::getBody) + .orElseThrow(() -> { + int paramCount = function.parameters(); + if (paramCount == -1) { + return new IOException("Unknown function: " + name); + } + return new IOException("Unknown function with " + paramCount + " parameters: " + name); + }); + + for (GlslNode node : GlslParser.parseExpressionList(function.code())) { + if (function.head()) { + body.addFirst(node); + } else { + body.add(node); + } + } + } + } + + @Override + public int priority() { + return this.priority; + } + + public ShaderInjectionFunction[] getShaderInjectionFunctions() { + return this.functions; + } +} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/Diagnostic.java b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/Diagnostic.java new file mode 100644 index 000000000..9420d80ee --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/Diagnostic.java @@ -0,0 +1,25 @@ +package foundry.veil.impl.client.render.shader.injection.util; + +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public record Diagnostic( + @Nullable String resourcePath, + @Nullable ResourceLocation injectionId, + @Nullable ResourceLocation target, + @Nullable String field, + String message, + Severity severity +) { + + @Override + public String toString() { + return this.severity + ": " + this.message + + (this.resourcePath != null ? " (" + this.resourcePath + ")" : "") + + (this.field != null ? " field=" + this.field : "") + + (this.injectionId != null ? " id=" + this.injectionId : "") + + (this.target != null ? " target=" + this.target : ""); + } +} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/Severity.java b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/Severity.java new file mode 100644 index 000000000..8771cb965 --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/Severity.java @@ -0,0 +1,10 @@ +package foundry.veil.impl.client.render.shader.injection.util; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public enum Severity { + ERROR, + WARNING, + INFO +} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/ShaderInjection.java b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/ShaderInjection.java new file mode 100644 index 000000000..7f68fa99d --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/ShaderInjection.java @@ -0,0 +1,15 @@ +package foundry.veil.impl.client.render.shader.injection.util; + +import io.github.ocelot.glslprocessor.api.GlslSyntaxException; +import io.github.ocelot.glslprocessor.api.node.GlslTree; +import org.jetbrains.annotations.ApiStatus; + +import java.io.IOException; + +@ApiStatus.Internal +public interface ShaderInjection { + + void inject(GlslTree tree, boolean applyVersion) throws GlslSyntaxException, IOException; + + int priority(); +} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/ShaderInjectionDefinition.java b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/ShaderInjectionDefinition.java new file mode 100644 index 000000000..644c9f29f --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/ShaderInjectionDefinition.java @@ -0,0 +1,89 @@ +package foundry.veil.impl.client.render.shader.injection.util; + +import com.google.gson.*; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +/** + * JSON-deserialized model for shader injection definitions. + * + * @author Vowxky + */ +@ApiStatus.Internal +public record ShaderInjectionDefinition( + @Nullable ResourceLocation id, + List targets, + List redirects, + int priority, + @Nullable ResourceLocation replace, + boolean debug) { + + public static final int DEFAULT_PRIORITY = 1000; + + public ShaderInjectionDefinition { + if (priority == 0) { + priority = DEFAULT_PRIORITY; + } + targets = List.copyOf(targets); + redirects = List.copyOf(redirects); + } + + public static Gson createGson() { + return new GsonBuilder() + .registerTypeAdapter(ResourceLocation.class, (JsonDeserializer) (json, type, context) -> ResourceLocation.parse(json.getAsString())) + .registerTypeAdapter(ResourceLocation.class, (JsonSerializer) (location, type, context) -> context.serialize(location.toString())) + .registerTypeAdapter(ShaderInjectionDefinition.class, new Deserializer()) + .create(); + } + + public ShaderInjectionDefinition withDefaultId(ResourceLocation defaultId) { + return new ShaderInjectionDefinition( + this.id != null ? this.id : defaultId, + this.targets, + this.redirects, + this.priority, + this.replace, + this.debug + ); + } + + public @Nullable ResourceLocation target() { + return this.targets.isEmpty() ? null : this.targets.getFirst(); + } + + private static class Deserializer implements JsonDeserializer { + + @Override + public ShaderInjectionDefinition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject obj = json.getAsJsonObject(); + + List targets = parseResourceLocations(obj, "target"); + List redirects = parseResourceLocations(obj, "redirect"); + int priority = obj.has("priority") ? obj.get("priority").getAsInt() : DEFAULT_PRIORITY; + ResourceLocation replace = obj.has("replace") ? context.deserialize(obj.get("replace"), ResourceLocation.class) : null; + boolean debug = obj.has("debug") && obj.get("debug").getAsBoolean(); + + return new ShaderInjectionDefinition(null, targets, redirects, priority, replace, debug); + } + } + + private static List parseResourceLocations(JsonObject obj, String field) { + if (!obj.has(field) || obj.get(field) instanceof JsonNull) { + return List.of(); + } + JsonElement element = obj.get(field); + if (element instanceof JsonArray array) { + List result = new ArrayList<>(); + for (JsonElement el : array) { + result.add(ResourceLocation.parse(el.getAsString())); + } + return result; + } + return List.of(ResourceLocation.parse(element.getAsString())); + } +} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/ShaderInjectionFunction.java b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/ShaderInjectionFunction.java new file mode 100644 index 000000000..6382f4260 --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/ShaderInjectionFunction.java @@ -0,0 +1,7 @@ +package foundry.veil.impl.client.render.shader.injection.util; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public record ShaderInjectionFunction(String name, int parameters, boolean head, String code) { +} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/ValidationResult.java b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/ValidationResult.java new file mode 100644 index 000000000..09464ad94 --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/injection/util/ValidationResult.java @@ -0,0 +1,27 @@ +package foundry.veil.impl.client.render.shader.injection.util; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.List; + +@ApiStatus.Internal +public record ValidationResult(@Unmodifiable List diagnostics) { + + public ValidationResult { + diagnostics = List.copyOf(diagnostics); + } + + public boolean isValid() { + if (this.diagnostics.isEmpty()) { + return true; + } + + for (Diagnostic diagnostic : this.diagnostics) { + if (diagnostic.severity() == Severity.ERROR) { + return false; + } + } + return true; + } +} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/InputShaderModification.java b/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/InputShaderModification.java deleted file mode 100644 index 4090686c1..000000000 --- a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/InputShaderModification.java +++ /dev/null @@ -1,29 +0,0 @@ -package foundry.veil.impl.client.render.shader.modifier; - -import io.github.ocelot.glslprocessor.api.GlslParser; -import io.github.ocelot.glslprocessor.api.GlslSyntaxException; -import io.github.ocelot.glslprocessor.api.node.GlslTree; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public class InputShaderModification implements ShaderModification { - - private final int priority; - private final String input; - - public InputShaderModification(int priority, String input) { - this.priority = priority; - this.input = input; - } - - @Override - public void inject(GlslTree tree, VeilJobParameters parameters) throws GlslSyntaxException { - tree.getBody().addAll(0, GlslParser.parse(this.input).getBody()); -// tree.parseAndInjectNodes(parser, ASTInjectionPoint.BEFORE_DECLARATIONS, this.input.split("\n")); - } - - @Override - public int priority() { - return this.priority; - } -} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ReplaceShaderModification.java b/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ReplaceShaderModification.java deleted file mode 100644 index 63efe0ccf..000000000 --- a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ReplaceShaderModification.java +++ /dev/null @@ -1,15 +0,0 @@ -package foundry.veil.impl.client.render.shader.modifier; - -import io.github.ocelot.glslprocessor.api.GlslSyntaxException; -import io.github.ocelot.glslprocessor.api.node.GlslTree; -import net.minecraft.resources.ResourceLocation; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public record ReplaceShaderModification(int priority, ResourceLocation veilShader) implements ShaderModification { - - @Override - public void inject(GlslTree tree, VeilJobParameters parameters) throws GlslSyntaxException { - throw new UnsupportedOperationException("Replace modification replaces file"); - } -} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ShaderModification.java b/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ShaderModification.java deleted file mode 100644 index 45bb313ca..000000000 --- a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ShaderModification.java +++ /dev/null @@ -1,50 +0,0 @@ -package foundry.veil.impl.client.render.shader.modifier; - -import io.github.ocelot.glslprocessor.api.GlslSyntaxException; -import io.github.ocelot.glslprocessor.api.node.GlslTree; -import org.jetbrains.annotations.ApiStatus; - -import java.io.IOException; -import java.util.regex.Pattern; - -/** - * Allows shader source files to be modified without overwriting the file. - * - * @author Ocelot - */ -@ApiStatus.Internal -public interface ShaderModification { - - Pattern VERSION_PATTERN = Pattern.compile("^#version\\s+(\\d+)\\s*\\w*\\s*", Pattern.MULTILINE); - Pattern OUT_PATTERN = Pattern.compile("^out (\\w+) (\\w+)\\s*;\\s*", Pattern.MULTILINE); - Pattern IN_PATTERN = Pattern.compile("^(?:layout\\(.*\\))?\\s*in (\\w+) (\\w+)\\s*;\\s*", Pattern.MULTILINE); - Pattern UNIFORM_PATTERN = Pattern.compile("^uniform \\w+ \\w+\\s*;\\s*", Pattern.MULTILINE); - Pattern RETURN_PATTERN = Pattern.compile("return\\s+.+;"); - Pattern PLACEHOLDER_PATTERN = Pattern.compile("#(\\w+)"); - - /** - * Injects this modification into the specified shader source. - * - * @param tree The source to modify - * @param parameters The parameters to use when injecting - * @throws GlslSyntaxException If an error occurs when parsing GLSL code - * @throws IOException If an error occurs with the format or applying the modifications - */ - void inject(GlslTree tree, VeilJobParameters parameters) throws GlslSyntaxException, IOException; - - /** - * @return The priority of this modification. A higher priority will be applied before a lower priority modification - */ - int priority(); - - static ShaderModification parse(String input, boolean vertex) throws ShaderModificationSyntaxException { - return ShaderModificationParser.parse(ShaderModifierLexer.createTokens(input), vertex); - } - - record Function(String name, int parameters, boolean head, String code) { - - public static Function create(String name, int parameters, boolean head, String code) { - return new Function(name, parameters, head, code); - } - } -} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ShaderModificationParser.java b/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ShaderModificationParser.java deleted file mode 100644 index ea36a9533..000000000 --- a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ShaderModificationParser.java +++ /dev/null @@ -1,284 +0,0 @@ -package foundry.veil.impl.client.render.shader.modifier; - -import net.minecraft.resources.ResourceLocation; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; - -/** - * @author Ocelot - */ -@ApiStatus.Internal -public final class ShaderModificationParser { - - public static ShaderModification parse(ShaderModifierLexer.Token[] tokens, boolean vertex) throws ShaderModificationSyntaxException { - TokenReader reader = new TokenReader(tokens); - reader.skipWhitespace(); // Skip comments, garbage, etc - - int version = -1; - int priority = 1000; - List includes = new ArrayList<>(); - while (reader.canRead()) { - switch (reader.peek().type()) { - case VERSION -> { - if (version != -1) { - throw error("Version can only be set once", reader); - } - reader.skip(); - version = consumeInt(reader); - reader.skipWhitespace(); - continue; - } - case PRIORITY -> { - reader.skip(); - priority = consumeInt(reader); - reader.skipWhitespace(); - continue; - } - case REPLACE -> { - reader.skip(); - ResourceLocation file = consumeLocation(reader); - reader.skipWhitespace(); - if (reader.canRead()) { - throw error("Trailing statement", reader); - } - return new ReplaceShaderModification(priority, file); - } - case INCLUDE -> { - while (reader.canRead() && reader.peek().type() == ShaderModifierLexer.TokenType.INCLUDE) { - reader.skip(); - includes.add(consumeLocation(reader)); - reader.skipWhitespace(); - } - continue; - } - } - break; - } - - Context context = new Context(new ArrayList<>(), new StringBuilder(), new StringBuilder(), new HashMap<>()); - while (reader.canRead()) { - switch (reader.peek().type()) { - case COMMENT, NEWLINE -> reader.skipWhitespace(); - case LEFT_BRACKET -> { - reader.skip(); - parseCommand(reader, context, vertex); - } - default -> throw error("Unexpected Token", reader); - } - } - - ShaderModification.Function[] functions = new ShaderModification.Function[context.functions.size()]; - int i = 0; - for (Map.Entry entry : context.functions.entrySet()) { - FunctionInject inject = entry.getKey(); - functions[i] = ShaderModification.Function.create(inject.name, inject.parameters, inject.head, entry.getValue().toString()); - i++; - } - - return vertex ? - new VertexShaderModification(version, - priority, - includes.toArray(ResourceLocation[]::new), - context.output.toString().trim(), - context.uniform.toString().trim(), - functions, - context.attributes.toArray(VertexShaderModification.Attribute[]::new) - ) : - new SimpleShaderModification(version, - priority, - includes.toArray(ResourceLocation[]::new), - context.output.toString().trim(), - context.uniform.toString().trim(), - functions); - } - - private static void parseCommand(TokenReader reader, Context context, boolean vertex) throws ShaderModificationSyntaxException { - switch (reader.peek().type()) { - case GET_ATTRIBUTE -> { - if (!vertex) { - throw error("Only vertex shader modifications can get attributes", reader); - } - - reader.skip(); - int index = consumeInt(reader); - consume(reader, ShaderModifierLexer.TokenType.RIGHT_BRACKET); - String definition = consume(reader, ShaderModifierLexer.TokenType.DEFINITION); - - Matcher matcher = ShaderModifierLexer.TokenType.DEFINITION.getPattern().matcher(definition); - if (!matcher.matches()) { - throw new IllegalStateException(); - } - - context.attributes.add(new VertexShaderModification.Attribute(index, matcher.group(1), matcher.group(2))); - } - case OUTPUT -> { - reader.skip(); - consume(reader, ShaderModifierLexer.TokenType.RIGHT_BRACKET); - - reader.skipWhitespace(); - context.output.append(consumeGLSL(reader)); - } - case UNIFORM -> { - reader.skip(); - consume(reader, ShaderModifierLexer.TokenType.RIGHT_BRACKET); - - reader.skipWhitespace(); - context.uniform.append(consumeGLSL(reader)); - } - case FUNCTION -> { - reader.skip(); - - String name = consume(reader, ShaderModifierLexer.TokenType.ALPHANUMERIC); - int parameters = -1; - if (reader.peek().type() == ShaderModifierLexer.TokenType.LEFT_PARENTHESIS) { - reader.skip(); - parameters = consumeInt(reader); - consume(reader, ShaderModifierLexer.TokenType.RIGHT_PARENTHESIS); - } - - boolean head; - if (reader.peek().type() == ShaderModifierLexer.TokenType.HEAD) { - reader.skip(); - head = true; - } else { - consume(reader, ShaderModifierLexer.TokenType.TAIL); - head = false; - } - - consume(reader, ShaderModifierLexer.TokenType.RIGHT_BRACKET); - reader.skipWhitespace(); - context.functions.computeIfAbsent(new FunctionInject(name, parameters, head), unused -> new StringBuilder()).append(consumeGLSL(reader)); - } - default -> throw error("Unexpected Token: " + reader.peek(), reader); - } - } - - private static String consumeGLSL(TokenReader reader) { - StringBuilder code = new StringBuilder(); - while (reader.canRead()) { - ShaderModifierLexer.Token token = reader.peek(); - if (token.type() == ShaderModifierLexer.TokenType.LEFT_BRACKET) { - ShaderModifierLexer.Token next = reader.peek(1); - if (next != null && next.type().isCommand()) { - // Only stop if a new command is detected - break; - } - } - - code.append(token.value()); - if (token.type() != ShaderModifierLexer.TokenType.NEWLINE) { - code.append(' '); - } - reader.skip(); - } - return code.toString().trim() + '\n'; - } - - private static ResourceLocation consumeLocation(TokenReader reader) throws ShaderModificationSyntaxException { - String namespace = consume(reader, ShaderModifierLexer.TokenType.ALPHANUMERIC); - if (reader.peek().type() == ShaderModifierLexer.TokenType.COLON) { - reader.skip(); - - StringBuilder path = new StringBuilder(); - while (reader.canRead() && reader.peek().type().isValidLocation()) { - path.append(reader.peek().value()); - reader.skip(); - } - - if (path.isEmpty()) { - throw error("Unexpected Token", reader); - } - - return ResourceLocation.fromNamespaceAndPath(namespace, path.toString()); - } - return ResourceLocation.parse(namespace); - } - - private static int consumeInt(TokenReader reader) throws ShaderModificationSyntaxException { - return Integer.parseInt(consume(reader, ShaderModifierLexer.TokenType.NUMERAL)); - } - - private static String consume(TokenReader reader, ShaderModifierLexer.TokenType token) throws ShaderModificationSyntaxException { - expect(reader, token); - String value = reader.peek().value(); - reader.skip(); - return value; - } - - private static void expect(TokenReader reader, ShaderModifierLexer.TokenType token) throws ShaderModificationSyntaxException { - if (!reader.canRead() || reader.peek().type() != token) { - throw error("Expected " + token, reader); - } - } - - private static ShaderModificationSyntaxException error(String error, TokenReader reader) { - return new ShaderModificationSyntaxException(error, reader.getString(), reader.getCursorOffset()); - } - - private record Context(List attributes, - StringBuilder output, - StringBuilder uniform, - Map functions) { - } - - private record FunctionInject(String name, int parameters, boolean head) { - } - - private static class TokenReader { - - private final ShaderModifierLexer.Token[] tokens; - private int cursor; - - public TokenReader(ShaderModifierLexer.Token[] tokens) { - this.tokens = tokens; - } - - public String getString() { - StringBuilder builder = new StringBuilder(); - for (ShaderModifierLexer.Token token : this.tokens) { - builder.append(token.value()); - } - return builder.toString(); - } - - public boolean canRead(int length) { - return this.cursor + length <= this.tokens.length; - } - - public boolean canRead() { - return this.canRead(1); - } - - public int getCursorOffset() { - int offset = 0; - for (int i = 0; i <= Math.min(this.cursor, this.tokens.length - 1); i++) { - offset += this.tokens[i].value().length(); - } - return offset; - } - - public ShaderModifierLexer.Token peek() { - return this.peek(0); - } - - public @Nullable ShaderModifierLexer.Token peek(int amount) { - return this.cursor + amount < this.tokens.length ? this.tokens[this.cursor + amount] : null; - } - - public void skip() { - this.cursor++; - } - - public void skipWhitespace() { - while (this.canRead() && this.peek().type().isWhitespace()) { - this.skip(); - } - } - } -} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ShaderModificationSyntaxException.java b/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ShaderModificationSyntaxException.java deleted file mode 100644 index 80e12bd11..000000000 --- a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ShaderModificationSyntaxException.java +++ /dev/null @@ -1,50 +0,0 @@ -package foundry.veil.impl.client.render.shader.modifier; - -import org.jetbrains.annotations.ApiStatus; - -/** - * @author Ocelot - */ -@ApiStatus.Internal -public class ShaderModificationSyntaxException extends Exception { - - public static final int CONTEXT_AMOUNT = 64; - - private final String message; - private final String input; - private final int cursor; - - public ShaderModificationSyntaxException(String message, String input, int cursor) { - super(message); - this.message = message; - this.input = input; - this.cursor = cursor; - } - - @Override - public String getMessage() { - String message = this.message; - String context = this.getContext(); - if (context != null) { - message += " at position " + this.cursor + ": " + context; - } - return message; - } - - private String getContext() { - if (this.input == null || this.cursor < 0) { - return null; - } - StringBuilder builder = new StringBuilder(); - int cursor = Math.min(this.input.length(), this.cursor); - - if (cursor > CONTEXT_AMOUNT) { - builder.append("..."); - } - - builder.append(this.input, Math.max(0, cursor - CONTEXT_AMOUNT), cursor); - builder.append("<--[HERE]"); - - return builder.toString(); - } -} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ShaderModifierLexer.java b/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ShaderModifierLexer.java deleted file mode 100644 index 58b8235f3..000000000 --- a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/ShaderModifierLexer.java +++ /dev/null @@ -1,112 +0,0 @@ -package foundry.veil.impl.client.render.shader.modifier; - -import com.mojang.brigadier.StringReader; -import org.jetbrains.annotations.ApiStatus; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * @author Ocelot - */ -@ApiStatus.Internal -public final class ShaderModifierLexer { - - private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\t"); - - public static Token[] createTokens(String input) { - StringReader reader = new StringReader(WHITESPACE_PATTERN.matcher(input).replaceAll("")); - List tokens = new ArrayList<>(); - - while (reader.canRead()) { - while (reader.canRead() && reader.peek() != '\n' && Character.isWhitespace(reader.peek())) { - reader.skip(); - } - - Token token = getToken(reader); - if (token != null) { - tokens.add(token); - continue; - } - - throw new IllegalStateException("Unknown Token"); - } - - return tokens.toArray(Token[]::new); - } - - private static Token getToken(StringReader reader) { - String remaining = reader.getRemaining(); - for (TokenType type : TokenType.values()) { - Matcher matcher = type.pattern.matcher(remaining); - if (matcher.find() && matcher.start() == 0) { - reader.setCursor(reader.getCursor() + matcher.end()); - return new Token(type, remaining.substring(0, matcher.end())); - } - } - - return null; - } - - public record Token(TokenType type, String value) { - public String lowercaseValue() { - return this.value.toLowerCase(Locale.ROOT); - } - - @Override - public String toString() { - return this.type + "[" + this.value + "]"; - } - } - - public enum TokenType { - COMMENT("\\/\\/.+"), - VERSION("#version"), - PRIORITY("#priority"), - INCLUDE("#include"), - REPLACE("#replace"), - GET_ATTRIBUTE("GET_ATTRIBUTE"), - OUTPUT("OUTPUT"), - UNIFORM("UNIFORM"), - FUNCTION("FUNCTION"), - HEAD("HEAD"), - TAIL("TAIL"), - DEFINITION("(int|ivec2|ivec3|ivec4|uint|uvec2|uvec3|uvec4|float|vec2|vec3|vec4|double|dvec2|dvec3|dvec4|mat2|mat2x3|mat2x4|mat3|mat3x2|mat3x4|mat4|mat4x2|mat4x3)\\s+(\\w+)\\s*;"), - NUMERAL("-?\\d+"), - ALPHANUMERIC("\\w+"), - COLON(":"), - NAMESPACE("[a-z0-9_\\-.]+"), - PATH("[a-z0-9_\\-./]+"), - LEFT_BRACKET("\\["), - RIGHT_BRACKET("\\]"), - LEFT_PARENTHESIS("\\("), - RIGHT_PARENTHESIS("\\)"), - NEWLINE("\n"), - CODE(".+"); - - private final Pattern pattern; - - TokenType(String regex) { - this.pattern = Pattern.compile(regex); - } - - public Pattern getPattern() { - return this.pattern; - } - - public boolean isValidLocation() { - return this == ALPHANUMERIC || this == NAMESPACE || this == PATH || this == COLON; - } - - public boolean isCommand() { - return this == GET_ATTRIBUTE || this == OUTPUT || this == UNIFORM || this == FUNCTION; - } - - public boolean isWhitespace() { - return this == COMMENT || this == NEWLINE; - } - } -} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/SimpleShaderModification.java b/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/SimpleShaderModification.java deleted file mode 100644 index 6f09c33f8..000000000 --- a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/SimpleShaderModification.java +++ /dev/null @@ -1,120 +0,0 @@ -package foundry.veil.impl.client.render.shader.modifier; - -import io.github.ocelot.glslprocessor.api.GlslParser; -import io.github.ocelot.glslprocessor.api.GlslSyntaxException; -import io.github.ocelot.glslprocessor.api.grammar.GlslVersionStatement; -import io.github.ocelot.glslprocessor.api.node.GlslNode; -import io.github.ocelot.glslprocessor.api.node.GlslTree; -import io.github.ocelot.glslprocessor.api.node.function.GlslFunctionNode; -import net.minecraft.resources.ResourceLocation; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.util.List; -import java.util.regex.Matcher; - -@ApiStatus.Internal -public class SimpleShaderModification implements ShaderModification { - - private final int version; - private final int priority; - private final ResourceLocation[] includes; - private final String output; - private final String uniform; - private final Function[] functions; - - public SimpleShaderModification(int version, int priority, ResourceLocation[] includes, @Nullable String output, @Nullable String uniform, Function[] functions) { - this.version = version; - this.priority = priority; - this.includes = includes; - this.output = output; - this.uniform = uniform; - this.functions = functions; - } - - @Override - public void inject(GlslTree tree, VeilJobParameters parameters) throws GlslSyntaxException, IOException { - if (parameters.applyVersion()) { - GlslVersionStatement version = tree.getVersionStatement(); - if (version.getVersion() < this.version) { - version.setVersion(this.version); - } - } - - List directives = tree.getDirectives(); - for (ResourceLocation include : this.includes) { - directives.add("#include " + include); - } - - if (this.output != null && !this.output.isEmpty()) { - tree.getBody().addAll(0, GlslParser.parse(this.fillPlaceholders(this.output)).getBody()); - } - - if (this.uniform != null && !this.uniform.isEmpty()) { - tree.getBody().addAll(0, GlslParser.parse(this.fillPlaceholders(this.uniform)).getBody()); - } - - for (Function function : this.functions) { - String name = function.name(); - List body = tree.functions().filter(definition -> { - if (definition == null) { - return false; - } - - if (definition.getBody() == null) { - return false; - } - - int paramCount = function.parameters(); - if (paramCount == -1) { - return true; - } - return definition.getHeader().getParameters().size() == paramCount; - }) - .findFirst() - .map(GlslFunctionNode::getBody) - .orElseThrow(() -> { - int paramCount = function.parameters(); - if (paramCount == -1) { - return new IOException("Unknown function: " + name); - } - return new IOException("Unknown function with " + paramCount + " parameters: " + name); - }); - - GlslNode insert = GlslNode.compound(GlslParser.parseExpressionList(this.fillPlaceholders(function.code()))); - if (function.head()) { - body.addFirst(insert); - } else { - body.add(insert); - } - } - } - - public String fillPlaceholders(String code) { - Matcher matcher = PLACEHOLDER_PATTERN.matcher(code); - if (!matcher.find()) { - return code; - } - - StringBuilder builder = new StringBuilder(); - do { - matcher.appendReplacement(builder, this.getPlaceholder(matcher.group(1))); - } while (matcher.find()); - matcher.appendTail(builder); - return builder.toString(); - } - - protected String getPlaceholder(String key) { - return key; - } - - @Override - public int priority() { - return this.priority; - } - - public String getOutput() { - return this.output; - } -} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/VeilJobParameters.java b/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/VeilJobParameters.java deleted file mode 100644 index 42ef4c62e..000000000 --- a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/VeilJobParameters.java +++ /dev/null @@ -1,32 +0,0 @@ -package foundry.veil.impl.client.render.shader.modifier; - -import foundry.veil.api.client.render.shader.ShaderModificationManager; -import net.minecraft.resources.ResourceLocation; - -import java.util.Collection; - -public record VeilJobParameters(ShaderModificationManager modificationManager, - ResourceLocation shaderId, - int flags) { - - /** - * Whether the version is required and will be applied - */ - public static final int APPLY_VERSION = 0b01; - /** - * Whether [OUT] is a valid command - */ - public static final int ALLOW_OUT = 0b10; - - public Collection modifiers() { - return this.modificationManager.getModifiers(this.shaderId); - } - - public boolean applyVersion() { - return (this.flags & APPLY_VERSION) > 0; - } - - public boolean allowOut() { - return (this.flags & ALLOW_OUT) > 0; - } -} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/VertexShaderModification.java b/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/VertexShaderModification.java deleted file mode 100644 index fac487288..000000000 --- a/common/src/main/java/foundry/veil/impl/client/render/shader/modifier/VertexShaderModification.java +++ /dev/null @@ -1,122 +0,0 @@ -package foundry.veil.impl.client.render.shader.modifier; - -import io.github.ocelot.glslprocessor.api.GlslParser; -import io.github.ocelot.glslprocessor.api.GlslSyntaxException; -import io.github.ocelot.glslprocessor.api.grammar.GlslSpecifiedType; -import io.github.ocelot.glslprocessor.api.grammar.GlslTypeQualifier; -import io.github.ocelot.glslprocessor.api.node.GlslTree; -import io.github.ocelot.glslprocessor.api.visitor.GlslNodeStringWriter; -import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; -import net.minecraft.resources.ResourceLocation; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -@ApiStatus.Internal -public class VertexShaderModification extends SimpleShaderModification { - - private final Attribute[] attributes; - private final Map mapper; - - public VertexShaderModification(int version, int priority, ResourceLocation[] includes, @Nullable String output, @Nullable String uniform, Function[] functions, Attribute[] attributes) { - super(version, priority, includes, output, uniform, functions); - this.attributes = attributes; - this.mapper = new HashMap<>(this.attributes.length); - } - -// @Override -// public void inject(ASTParser parser, TranslationUnit tree, VeilJobParameters parameters) throws IOException { -// if (this.attributes.length > 0) { -// Map validInputs = new Int2ObjectArrayMap<>(); -// -// Root root = tree.getRoot(); -// -// root.processMatches(parser, tree.getChildren().stream().filter(dec -> dec.hasAncestor(INPUT.getPatternClass())), INPUT, externalDeclaration -> { -// String[] parts = {null, null}; -// ASTWalker.walk(new ASTListener() { -// @Override -// public void enterTypeSpecifier(TypeSpecifier node) { -// parts[0] = ASTPrinter.printSimple(node); -// } -// -// @Override -// public void enterDeclarationMember(DeclarationMember node) { -// parts[1] = node.getName().getName(); -// } -// }, externalDeclaration); -// validInputs.put(validInputs.size(), new Attribute(validInputs.size(), parts[0], parts[1])); -// }); -// -// this.mapper.clear(); -// for (Attribute attribute : this.attributes) { -// Attribute sourceAttribute = validInputs.get(attribute.index); -// if (sourceAttribute == null) { -// // TODO this might be messed up on mac. It needs to be tested -// tree.parseAndInjectNode(parser, ASTInjectionPoint.BEFORE_DECLARATIONS, "layout(location = " + attribute.index + ") in " + attribute.type + " " + attribute.name + ";"); -// this.mapper.put(attribute.name, attribute.name); -// continue; -// } -// -// if (!sourceAttribute.type.equals(attribute.type)) { -// throw new IOException("Expected attribute " + attribute.index + " to be " + attribute.type + " but was " + sourceAttribute.type); -// } -// -// this.mapper.put(attribute.name, sourceAttribute.name); -// } -// } -// -// super.inject(parser, tree, parameters); -// } - - - @Override - public void inject(GlslTree tree, VeilJobParameters parameters) throws GlslSyntaxException, IOException { - if (this.attributes.length > 0) { - Map validInputs = new Int2ObjectArrayMap<>(); - - tree.fields().forEach(node -> { - GlslSpecifiedType type = node.getType(); - - for (GlslTypeQualifier qualifier : type.getQualifiers()) { - if (qualifier instanceof GlslTypeQualifier.StorageType storage && storage == GlslTypeQualifier.StorageType.IN) { - GlslNodeStringWriter writer = new GlslNodeStringWriter(true); - writer.visitTypeSpecifier(type.getSpecifier()); - validInputs.put(validInputs.size(), new Attribute(validInputs.size(), writer.toString(), node.getName())); - break; - } - } - }); - - this.mapper.clear(); - for (Attribute attribute : this.attributes) { - Attribute sourceAttribute = validInputs.get(attribute.index); - if (sourceAttribute == null) { - // TODO this might be messed up on mac. It needs to be tested - tree.getBody().add(0, GlslParser.parseExpression("layout(location = " + attribute.index + ") in " + attribute.type + " " + attribute.name)); - this.mapper.put(attribute.name, attribute.name); - continue; - } - - if (!sourceAttribute.type.equals(attribute.type)) { - throw new IOException("Expected attribute " + attribute.index + " to be " + attribute.type + " but was " + sourceAttribute.type); - } - - this.mapper.put(attribute.name, sourceAttribute.name); - } - } - - super.inject(tree, parameters); - } - - @Override - protected String getPlaceholder(String key) { - String name = this.mapper.get(key); - return name != null ? name : super.getPlaceholder(key); - } - - public record Attribute(int index, String type, String name) { - } -} diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/processor/SodiumShaderProcessor.java b/common/src/main/java/foundry/veil/impl/client/render/shader/processor/SodiumShaderProcessor.java index d42aa5191..722fadcda 100644 --- a/common/src/main/java/foundry/veil/impl/client/render/shader/processor/SodiumShaderProcessor.java +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/processor/SodiumShaderProcessor.java @@ -4,7 +4,7 @@ import foundry.veil.api.client.render.VeilRenderSystem; import foundry.veil.api.client.render.dynamicbuffer.DynamicBufferType; import foundry.veil.api.client.render.shader.processor.ShaderImporter; -import foundry.veil.api.client.render.shader.processor.ShaderModifyProcessor; +import foundry.veil.api.client.render.shader.processor.ShaderInjectProcessor; import foundry.veil.api.client.render.shader.processor.ShaderPreProcessor; import foundry.veil.impl.client.render.dynamicbuffer.DynamicBufferProcessor; import foundry.veil.impl.compat.sodium.SodiumShaderPreProcessor; @@ -43,7 +43,7 @@ public static void setShaderType(int type, ResourceLocation shaderName, GLCapabi public static void setup(ResourceProvider provider) { ShaderProcessorList list = new ShaderProcessorList(provider); - list.addPreprocessor(new ShaderModifyProcessor(), false); + list.addPreprocessor(new ShaderInjectProcessor(), false); list.addPreprocessor(new DynamicBufferProcessor(), false); list.addPreprocessor(new SodiumShaderPreProcessor()); VeilClient.clientPlatform().onRegisterShaderPreProcessors(provider, list); diff --git a/common/src/main/java/foundry/veil/impl/client/render/shader/processor/VanillaShaderProcessor.java b/common/src/main/java/foundry/veil/impl/client/render/shader/processor/VanillaShaderProcessor.java index 09fd5229e..061e271d1 100644 --- a/common/src/main/java/foundry/veil/impl/client/render/shader/processor/VanillaShaderProcessor.java +++ b/common/src/main/java/foundry/veil/impl/client/render/shader/processor/VanillaShaderProcessor.java @@ -5,7 +5,7 @@ import foundry.veil.api.client.render.VeilRenderSystem; import foundry.veil.api.client.render.dynamicbuffer.DynamicBufferType; import foundry.veil.api.client.render.shader.processor.ShaderImporter; -import foundry.veil.api.client.render.shader.processor.ShaderModifyProcessor; +import foundry.veil.api.client.render.shader.processor.ShaderInjectProcessor; import foundry.veil.api.client.render.shader.processor.ShaderPreProcessor; import foundry.veil.impl.client.render.dynamicbuffer.DynamicBufferProcessor; import io.github.ocelot.glslprocessor.api.GlslParser; @@ -32,7 +32,7 @@ public class VanillaShaderProcessor { public static void setup(ResourceProvider provider) { ShaderProcessorList list = new ShaderProcessorList(provider); - list.addPreprocessor(new ShaderModifyProcessor(), false); + list.addPreprocessor(new ShaderInjectProcessor(), false); list.addPreprocessor(new DynamicBufferProcessor(), false); VeilClient.clientPlatform().onRegisterShaderPreProcessors(provider, list); PROCESSOR.set(list); diff --git a/common/src/main/java/foundry/veil/mixin/shader/client/ShaderGameRendererMixin.java b/common/src/main/java/foundry/veil/mixin/shader/client/ShaderGameRendererMixin.java index 10366af0c..767e8dd65 100644 --- a/common/src/main/java/foundry/veil/mixin/shader/client/ShaderGameRendererMixin.java +++ b/common/src/main/java/foundry/veil/mixin/shader/client/ShaderGameRendererMixin.java @@ -6,10 +6,8 @@ import foundry.veil.api.client.render.VeilRenderBridge; import foundry.veil.api.client.render.VeilRenderSystem; import foundry.veil.api.client.render.VeilRenderer; -import foundry.veil.api.client.render.shader.ShaderModificationManager; import foundry.veil.api.client.render.shader.program.ShaderProgram; -import foundry.veil.impl.client.render.shader.modifier.ReplaceShaderModification; -import foundry.veil.impl.client.render.shader.modifier.ShaderModification; +import foundry.veil.impl.client.render.shader.injection.ShaderInjectionManager; import net.minecraft.client.renderer.GameRenderer; import net.minecraft.client.renderer.ShaderInstance; import net.minecraft.resources.ResourceLocation; @@ -31,24 +29,33 @@ public void replaceShaders(CallbackInfo ci, @Local(ordinal = 1) List> pair : loadedShaders) { - ResourceLocation loc = ResourceLocation.tryParse(pair.getFirst().getName()); - if (loc == null) { - Veil.LOGGER.error("Failed to replace vanilla shader '{}' with veil shader: Malformed name", pair.getFirst().getName()); + String name = pair.getFirst().getName(); + ResourceLocation target = ResourceLocation.tryParse(name); + if (target == null) { + Veil.LOGGER.warn("Couldn't parse shader name '{}' as resource location", name); continue; } - List modifiers = modificationManager.getModifiers(loc.withPrefix("shaders/core/")); - if (modifiers.size() == 1 && modifiers.getFirst() instanceof ReplaceShaderModification replaceModification) { - ShaderProgram shader = renderer.getShaderManager().getShader(replaceModification.veilShader()); - if (shader != null) { - pair.getSecond().accept(VeilRenderBridge.toShaderInstance(shader)); - continue; - } + ResourceLocation replacementId = injectionManager.getReplacement(target); + if (replacementId == null) { + Veil.LOGGER.debug("No replacement found for {}", name); + continue; + } - Veil.LOGGER.error("Failed to replace vanilla shader '{}' with veil shader: {}", loc, replaceModification.veilShader()); + ShaderProgram shader = renderer.getShaderManager().getShader(replacementId); + if (shader == null) { + Veil.LOGGER.error("Failed to replace vanilla shader '{}': replacement '{}' not found", name, replacementId); + continue; } + + ShaderInstance oldInstance = pair.getFirst(); + ShaderInstance newInstance = VeilRenderBridge.toShaderInstance(shader); + pair.getSecond().accept(newInstance); + Veil.LOGGER.info("Replaced vanilla shader '{}' with '{}'", name, replacementId); + oldInstance.close(); } } } diff --git a/wiki/Home.md b/wiki/Home.md index c71181fdb..cc83fecf7 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -83,7 +83,7 @@ dependencies { - [Veil Events](Events) - [Better Vertex Buffers](VertexArray) -- [Shader Modifications](ShaderModification) +- [Shader Inject](ShaderInject) - [Dynamic Buffers](DynamicBuffer.md) - [Custom Shaders](Shader) - [Custom Framebuffers](Framebuffer) diff --git a/wiki/ShaderInject.md b/wiki/ShaderInject.md new file mode 100644 index 000000000..9cb49726c --- /dev/null +++ b/wiki/ShaderInject.md @@ -0,0 +1,260 @@ +Veil provides a **JSON-based shader injection system** that allows modifying vanilla and pinwheel shaders without +replacing the entire file. Injections are written as plain GLSL with a `void tail()` or `void head()` marker function. + +# Defining Injections + +Injection definitions are located in `assets/modid/pinwheel/shader_injection/*.json`. +Each definition references one or more GLSL files that contain the actual shader code. + +# JSON Format + +```json5 +{ + // The shader(s) to inject into. Can be a single string or array. + "target": "minecraft:shaders/core/rendertype_solid.fsh", + // The GLSL file(s) to inject. Required unless "replace" is used. + "redirect": "modid:example.glsl", + // Optional. Replace the target shader entirely with another Veil shader. + // Mutually exclusive with redirect. + "replace": "modid:custom_shader", + // Optional. Lower values execute first. Default 1000. + "priority": 1000, + // Optional. Logs parsed body and globals for debugging. + "debug": true +} +``` + +### Fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `target` | `string` or `string[]` | **required** | Shader(s) to inject into | +| `redirect` | `string` or `string[]` | *see note* | GLSL file(s) providing the injection code | +| `replace` | `string` | — | Replace the target shader entirely with another Veil shader | +| `priority` | `int` | `1000` | Injection order (lower = sooner) | +| `debug` | `bool` | `false` | Log injection body and globals to console | + +> `redirect` and `replace` are mutually exclusive. One of them must be present. + +### Target Format + +- **`redirect`** targets **must** include the shader extension (e.g. `.fsh`, `.vsh`): + ``` + "target": "minecraft:shaders/core/rendertype_solid.fsh" + ``` +- **`replace`** targets do **not** include the extension: + ``` + "target": "minecraft:shaders/core/rendertype_solid" + ``` + The system strips shader extensions automatically for replace lookups. + +### Multi-Target Example + +```json +{ + "target": [ + "minecraft:shaders/core/rendertype_solid.vsh", + "minecraft:shaders/core/rendertype_cutout.vsh" + ], + "redirect": "modid:scale.glsl" +} +``` + +### Replace + +`replace` completely swaps the target shader with another Veil shader program defined in `pinwheel/shaders/program/`. Unlike `redirect`, which injects code into the existing shader, `replace` discards the original entirely and substitutes a different `ShaderProgram`. + +- The target must be a vanilla Minecraft shader (e.g. `minecraft:shaders/core/rendertype_solid`) **without** file extension. +- The replacement must be a valid Veil shader program ID. +- Only vanilla Minecraft shaders can be replaced. To replace a Veil shader, use `redirect` with head/tail injections. + +```json +{ + "target": "minecraft:shaders/core/rendertype_solid", + "replace": "modid:custom_shader" +} +``` + +### Full Examples + +#### head_example + +**JSON** (`assets/modid/pinwheel/shader_injection/head_example.json`): +```json +{ + "target": "minecraft:shaders/core/rendertype_translucent.fsh", + "redirect": "modid:head_example.glsl" +} +``` + +**GLSL** (`assets/modid/pinwheel/shader_injection/head_example.glsl`): +```glsl +// Affects translucent blocks: water, stained glass, ice, slime blocks, honey blocks. +// Desaturates to grayscale. +void tail() { + float gray = dot(fragColor.rgb, vec3(0.299, 0.587, 0.114)); + fragColor.rgb = vec3(gray); +} +``` + +#### tail_example + +**JSON** (`assets/modid/pinwheel/shader_injection/tail_example.json`): +```json +{ + "target": "minecraft:shaders/core/rendertype_cutout_mipped.fsh", + "redirect": "modid:tail_example.glsl" +} +``` + +**GLSL** (`assets/modid/pinwheel/shader_injection/tail_example.glsl`): +```glsl +// Affects mipmapped cutout blocks: leaves, powered rails, decorated pots. +// Inverts the red and green channels for a psychedelic look. +void tail() { + fragColor.r = 1.0 - fragColor.r; + fragColor.g = 1.0 - fragColor.g; +} +``` + +#### multi_target + +**JSON** (`assets/modid/pinwheel/shader_injection/multi_target.json`): +```json +{ + "target": [ + "minecraft:shaders/core/rendertype_entity_solid.fsh", + "minecraft:shaders/core/rendertype_entity_cutout.fsh", + "minecraft:shaders/core/rendertype_entity_translucent.fsh" + ], + "redirect": "modid:multi_target.glsl" +} +``` + +**GLSL** (`assets/modid/pinwheel/shader_injection/multi_target.glsl`): +```glsl +// Affects all entity rendering: mobs, players, item entities, armor stands. +// Applies a red overlay to everything. +void tail() { + fragColor.rgb = mix(fragColor.rgb, vec3(1.0, 0.0, 0.0), 0.3); +} +``` + +#### replace_example + +Requires a `color` shader program in `assets/modid/pinwheel/shaders/program/`. + +**Program JSON** (`assets/modid/pinwheel/shaders/program/color.json`): +```json +{ + "vertex": "modid:color", + "fragment": "modid:color" +} +``` + +**Vertex** (`assets/modid/pinwheel/shaders/program/color.vsh`): +```glsl +layout(location = 0) in vec3 Position; +layout(location = 1) in vec2 UV0; +layout(location = 2) in vec2 UV1; +layout(location = 3) in vec2 UV2; +layout(location = 4) in vec4 Color; +layout(location = 5) in vec3 Normal; + +uniform mat4 ModelViewMat; +uniform mat4 ProjMat; +uniform vec3 ChunkOffset; + +out vec2 texCoord0; +out vec4 vertexColor; + +void main() { + vec3 pos = Position + ChunkOffset; + gl_Position = ProjMat * ModelViewMat * vec4(pos, 1.0); + texCoord0 = UV0; + vertexColor = Color; +} +``` + +**Fragment** (`assets/modid/pinwheel/shaders/program/color.fsh`): +```glsl +uniform sampler2D Sampler0; + +in vec2 texCoord0; +in vec4 vertexColor; + +out vec4 fragColor; + +void main() { + fragColor = vertexColor; +} +``` + +**Injection JSON** (`assets/modid/pinwheel/shader_injection/replace_example.json`): +```json +{ + "target": "minecraft:shaders/core/rendertype_solid", + "replace": "modid:color" +} +``` + +# GLSL Format + +Write standard GLSL. Use **`void tail()`** to inject at the **end** of the target function (usually `main`), or +**`void head()`** to inject at the **start**. + +```glsl +void tail() { + gl_Position = gl_Position * 2.0; +} +``` + +> **Warning:** If neither `void head()` nor `void tail()` is present, the injection is silently skipped and a warning is logged. At least one marker function is required. + +### Globals + +Code placed **outside** the marker function is treated as globals and injected at the top of the shader. This is +useful for uniforms, helper functions, or shared variables. + +```glsl +vec4 tintColor(vec4 color) { + return color * vec4(1.0, 0.5, 0.5, 1.0); +} + +void tail() { + fragColor = tintColor(fragColor); +} +``` + +### Version + +If your injection requires a specific GLSL version, add `#version` at the top of the file. It will be auto-detected +and applied if the target shader's version is lower. + +```glsl +#version 330 + +void tail() { + gl_Position = gl_Position * 2.0; +} +``` + +If omitted, the version is handled automatically. + +Standard GLSL syntax is fully supported, including comments, control flow, multi-line statements, and nested blocks. + +See [Shader.md](Shader.md) for #include + +# Migration from the Old Format + +| Old (`.txt`) | New (JSON + GLSL) | +|--------------|-------------------| +| `#version 330` | Optional. Auto-detected from GLSL file if present. | +| `#priority 1000` | `"priority": 1000` in JSON | +| `#include modid:path` | `#include "path.glsl"` in GLSL file | +| `#replace modid:path` | `"replace": "modid:path"` in JSON | +| `[FUNCTION main(0) HEAD]` | `void head() { }` in GLSL file | +| `[FUNCTION main(0) TAIL]` | `void tail() { }` in GLSL file | +| `[OUTPUT]`, `[UNIFORM]`, `[GET_ATTRIBUTE]` | Write directly as globals outside `tail()`/`head()` | +| `#name` placeholders | Use variables directly | +| `assets/.../shader_modifiers/*.txt` | `assets/.../shader_injection/*.json` | diff --git a/wiki/ShaderModification.md b/wiki/ShaderModification.md deleted file mode 100644 index fd8cb0616..000000000 --- a/wiki/ShaderModification.md +++ /dev/null @@ -1,73 +0,0 @@ -Veil adds a custom "scripting language" for injecting into shader sources. This system is intended to adding small hooks -to vanilla shaders to make changes without replacing the entire file. - -# Defining Modifications - -Shader modifications are loaded before any shaders are loaded. They are located -in `assets/modid/pinwheel/shader_modifiers/shaderid/path/to/shader/filename.vsh.txt`. For example, a modification -for `minecraft:shaders/core/rendertype_solid.vsh` would be located -at `assets/modid/pinwheel/shader_modifiers/minecraft/shaders/core/rendertype_solid.vsh.txt` - -This system allows a shader modifier to modify any shader, including pinwheel shaders. Pinwheel shader modification -works exactly like vanilla shader modification. - -# Format - -The basic syntax is as follows: - -```text -#version 330 // required -#priority 1000 // default 1000 -#include veil:camera // Test include - -// Replaces this with the defined shader -// #replace veil:shader/test - -// Vertex Only -[GET_ATTRIBUTE 0] vec3 InPos; // test -[GET_ATTRIBUTE 4] vec3 Nom; - -[OUTPUT] // Outputs are guaranteed to be unique -out vec4 Test; -out vec3 TestNormal; - -[UNIFORM] -uniform sampler2D Sampler8; - -[FUNCTION main(0) HEAD] -TestNormal = #Nom; -Test = vec4(#InPos, 1.0); -``` - -## Header - -`#version` specifies the minimum shader version for this modification. If unsure, use the version in the shader being -modified. - -`#priority` is used to determine the order modifications are loaded in. Modifications with a lower priority will load -before others. - -`#include` includes another file as specified in [Shader Includes](Shader#includes) - -`#replace` is a special option that replaces the targeted shader with another veil shader. This causes all other -modifications to be ignored and should only be used as a last resort. - -## Commands - -Commands can be added in any order and define where to inject shader code. - -- `[GET_ATTRIBUTE #] type name;` Retrieves the specified attribute by id and assigns it to the specified alias for this - modification. If the attribute doesn't exist it will be added. `#name` can be used anywhere in the file to use that - attribute. -- `[OUTPUT]` Adds all code following as input variables. An input is automatically added to the next shader in the - pipeline. -- `[UNIFORM]` Adds all following code at the end of the uniforms block. -- `[FUNCTION name(params) location]` Adds all following code into the specified method at the `HEAD` or `TAIL` - -### Functions - -This directive specifies what method to inject into. The number of params is optional and is used to specify exactly -what method to inject into. If not specified, all methods with the same name are matched. - -The last option determines where to inject in the method. `HEAD` adds code before the method contents and `TAIL` adds -code after the method contents. \ No newline at end of file