Cleaner compilation

- Check for errors immediately after loading shaders
   - Done through FileResolution
   - Add class SourceChecks to
 - Throw away GatherContextEvent, do everything on client init
 - Unify FileResolution and Resolver, PartialModel style
 - Pass around error reporter to detect errors during load
This commit is contained in:
Jozufozu 2022-05-17 12:56:09 -07:00
parent a7a716b469
commit c65992ec0e
51 changed files with 443 additions and 551 deletions

View file

@ -76,15 +76,16 @@ public class Flywheel {
forgeEventBus.<ReloadRenderersEvent>addListener(ProgramCompiler::invalidateAll); forgeEventBus.<ReloadRenderersEvent>addListener(ProgramCompiler::invalidateAll);
forgeEventBus.addListener(Models::onReload); forgeEventBus.addListener(Models::onReload);
modEventBus.addListener(LayoutShaders::flwInit);
modEventBus.addListener(InstanceShaders::flwInit);
modEventBus.addListener(MaterialShaders::flwInit);
modEventBus.addListener(Contexts::flwInit);
modEventBus.addListener(PartialModel::onModelRegistry); modEventBus.addListener(PartialModel::onModelRegistry);
modEventBus.addListener(PartialModel::onModelBake); modEventBus.addListener(PartialModel::onModelBake);
modEventBus.addListener(StitchedSprite::onTextureStitchPre); modEventBus.addListener(StitchedSprite::onTextureStitchPre);
modEventBus.addListener(StitchedSprite::onTextureStitchPost); modEventBus.addListener(StitchedSprite::onTextureStitchPost);
LayoutShaders.init();
InstanceShaders.init();
Contexts.init();
MaterialShaders.init();
VanillaInstances.init(); VanillaInstances.init();
// https://github.com/Jozufozu/Flywheel/issues/69 // https://github.com/Jozufozu/Flywheel/issues/69

View file

@ -1,18 +1,17 @@
package com.jozufozu.flywheel.backend; package com.jozufozu.flywheel.backend;
import com.jozufozu.flywheel.backend.instancing.InstancedRenderDispatcher; import com.jozufozu.flywheel.backend.instancing.InstancedRenderDispatcher;
import com.jozufozu.flywheel.core.GameStateRegistry;
import com.jozufozu.flywheel.core.crumbling.CrumblingRenderer; import com.jozufozu.flywheel.core.crumbling.CrumblingRenderer;
import com.jozufozu.flywheel.core.source.Resolver; import com.jozufozu.flywheel.core.source.FileResolution;
import com.jozufozu.flywheel.core.source.ShaderLoadingException;
import com.jozufozu.flywheel.core.source.ShaderSources; import com.jozufozu.flywheel.core.source.ShaderSources;
import com.jozufozu.flywheel.event.GatherContextEvent; import com.jozufozu.flywheel.core.source.error.ErrorReporter;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.server.packs.resources.ReloadableResourceManager; import net.minecraft.server.packs.resources.ReloadableResourceManager;
import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.server.packs.resources.ResourceManagerReloadListener; import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
import net.minecraftforge.fml.ModLoader;
/** /**
* The main entity for loading shaders. * The main entity for loading shaders.
@ -22,7 +21,6 @@ import net.minecraftforge.fml.ModLoader;
* </p> * </p>
*/ */
public class Loader implements ResourceManagerReloadListener { public class Loader implements ResourceManagerReloadListener {
private boolean firstLoad = true;
Loader() { Loader() {
// Can be null when running datagenerators due to the unfortunate time we call this // Can be null when running datagenerators due to the unfortunate time we call this
@ -39,15 +37,15 @@ public class Loader implements ResourceManagerReloadListener {
public void onResourceManagerReload(ResourceManager manager) { public void onResourceManagerReload(ResourceManager manager) {
Backend.refresh(); Backend.refresh();
GameStateRegistry._clear(); var errorReporter = new ErrorReporter();
ShaderSources sources = new ShaderSources(errorReporter, manager);
Resolver.INSTANCE.invalidate(); FileResolution.run(errorReporter, sources);
ModLoader.get()
.postEvent(new GatherContextEvent(firstLoad));
ShaderSources sources = new ShaderSources(manager); if (errorReporter.hasErrored()) {
errorReporter.dump();
Resolver.INSTANCE.run(sources); throw new ShaderLoadingException("Failed to resolve all source files, see log for details");
}
Backend.LOGGER.info("Loaded all shader sources."); Backend.LOGGER.info("Loaded all shader sources.");
@ -58,6 +56,5 @@ public class Loader implements ResourceManagerReloadListener {
CrumblingRenderer.reset(); CrumblingRenderer.reset();
} }
firstLoad = false;
} }
} }

View file

@ -1,8 +1,16 @@
package com.jozufozu.flywheel.backend.gl; package com.jozufozu.flywheel.backend.gl;
public enum GLSLVersion { public enum GLSLVersion {
V110(110),
V120(120),
V130(130),
V140(140),
V150(150), V150(150),
V330(330), V330(330),
V400(400),
V410(410),
V420(420),
V430(430),
; ;
public final int version; public final int version;

View file

@ -1,34 +1,37 @@
package com.jozufozu.flywheel.backend.gl.shader; package com.jozufozu.flywheel.backend.gl.shader;
import java.io.File;
import java.io.FileWriter;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.lwjgl.opengl.GL20; import org.lwjgl.opengl.GL20;
import com.jozufozu.flywheel.backend.gl.GlObject; import com.jozufozu.flywheel.backend.gl.GlObject;
import com.jozufozu.flywheel.backend.gl.versioned.GlCompat; import com.jozufozu.flywheel.backend.gl.versioned.GlCompat;
import com.jozufozu.flywheel.core.shader.ShaderConstants;
import com.jozufozu.flywheel.core.source.ShaderLoadingException; import com.jozufozu.flywheel.core.source.ShaderLoadingException;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
public class GlShader extends GlObject { public class GlShader extends GlObject {
public final ResourceLocation name;
public final ShaderType type; public final ShaderType type;
private final List<ResourceLocation> parts;
private final ShaderConstants constants;
public GlShader(ResourceLocation name, ShaderType type, String source) { public GlShader(String source, ShaderType type, List<ResourceLocation> parts, ShaderConstants constants) {
this.name = name; this.parts = parts;
this.type = type; this.type = type;
this.constants = constants;
int handle = GL20.glCreateShader(type.glEnum); int handle = GL20.glCreateShader(type.glEnum);
GlCompat.safeShaderSource(handle, source); GlCompat.safeShaderSource(handle, source);
GL20.glCompileShader(handle); GL20.glCompileShader(handle);
// File dir = new File(Minecraft.getInstance().gameDirectory, "flywheel_sources"); dumpSource(source, type);
// dir.mkdirs();
// File file = new File(dir, name.toString().replaceAll("[:/]", "_"));
// try (FileWriter writer = new FileWriter(file)) {
// writer.write(source);
// } catch (Exception e) {
// e.printStackTrace();
// }
// String log = GL20.glGetShaderInfoLog(handle); // String log = GL20.glGetShaderInfoLog(handle);
// //
@ -38,7 +41,7 @@ public class GlShader extends GlObject {
// } // }
if (GL20.glGetShaderi(handle, GL20.GL_COMPILE_STATUS) != GL20.GL_TRUE) { if (GL20.glGetShaderi(handle, GL20.GL_COMPILE_STATUS) != GL20.GL_TRUE) {
throw new ShaderLoadingException("Could not compile " + name + ". See log for details."); throw new ShaderLoadingException("Could not compile " + getName() + ". See log for details.");
} }
setHandle(handle); setHandle(handle);
@ -49,4 +52,22 @@ public class GlShader extends GlObject {
GL20.glDeleteShader(handle); GL20.glDeleteShader(handle);
} }
public String getName() {
return parts.stream()
.map(ResourceLocation::toString)
.map(s -> s.replaceAll("/", "_")
.replaceAll(":", "\\$"))
.collect(Collectors.joining(";")) + ';' + Integer.toHexString(constants.hashCode());
}
private void dumpSource(String source, ShaderType type) {
File dir = new File(Minecraft.getInstance().gameDirectory, "flywheel_sources");
dir.mkdirs();
File file = new File(dir, type.getFileName(getName()));
try (FileWriter writer = new FileWriter(file)) {
writer.write(source);
} catch (Exception e) {
e.printStackTrace();
}
}
} }

View file

@ -3,21 +3,27 @@ package com.jozufozu.flywheel.backend.gl.shader;
import org.lwjgl.opengl.GL20; import org.lwjgl.opengl.GL20;
public enum ShaderType { public enum ShaderType {
VERTEX("vertex", "VERTEX_SHADER", GL20.GL_VERTEX_SHADER), VERTEX("vertex", "VERTEX_SHADER", "vert", GL20.GL_VERTEX_SHADER),
FRAGMENT("fragment", "FRAGMENT_SHADER", GL20.GL_FRAGMENT_SHADER), FRAGMENT("fragment", "FRAGMENT_SHADER", "frag", GL20.GL_FRAGMENT_SHADER),
; ;
public final String name; public final String name;
public final String define; public final String define;
public final String extension;
public final int glEnum; public final int glEnum;
ShaderType(String name, String define, int glEnum) { ShaderType(String name, String define, String extension, int glEnum) {
this.name = name; this.name = name;
this.define = define; this.define = define;
this.extension = extension;
this.glEnum = glEnum; this.glEnum = glEnum;
} }
public String getDefineStatement() { public String getDefineStatement() {
return "#define " + define + "\n"; return "#define " + define + "\n";
} }
public String getFileName(String baseName) {
return baseName + "." + extension;
}
} }

View file

@ -20,7 +20,6 @@ import com.jozufozu.flywheel.core.CoreShaderInfoMap.CoreShaderInfo;
import com.jozufozu.flywheel.core.GameStateRegistry; import com.jozufozu.flywheel.core.GameStateRegistry;
import com.jozufozu.flywheel.core.RenderContext; import com.jozufozu.flywheel.core.RenderContext;
import com.jozufozu.flywheel.core.compile.ProgramCompiler; import com.jozufozu.flywheel.core.compile.ProgramCompiler;
import com.jozufozu.flywheel.core.compile.ProgramContext;
import com.jozufozu.flywheel.core.shader.WorldProgram; import com.jozufozu.flywheel.core.shader.WorldProgram;
import com.jozufozu.flywheel.core.vertex.Formats; import com.jozufozu.flywheel.core.vertex.Formats;
import com.jozufozu.flywheel.util.Textures; import com.jozufozu.flywheel.util.Textures;
@ -155,7 +154,7 @@ public class InstancingEngine<P extends WorldProgram> implements Engine {
alphaDiscard = 0; alphaDiscard = 0;
} }
P program = context.getProgram(new ProgramContext(Formats.POS_TEX_NORMAL, instanceType.getInstanceShader(), material.getVertexShader(), material.getFragmentShader(), alphaDiscard, coreShaderInfo.fogType(), GameStateRegistry.takeSnapshot())); P program = context.getProgram(new ProgramCompiler.Context(Formats.POS_TEX_NORMAL, instanceType.getInstanceShader(), material.getVertexShader(), material.getFragmentShader(), alphaDiscard, coreShaderInfo.fogType(), GameStateRegistry.takeSnapshot()));
program.bind(); program.bind();
program.uploadUniforms(camX, camY, camZ, viewProjection, level); program.uploadUniforms(camX, camY, camZ, viewProjection, level);

View file

@ -1,13 +1,13 @@
package com.jozufozu.flywheel.core; package com.jozufozu.flywheel.core;
import com.jozufozu.flywheel.Flywheel; import com.jozufozu.flywheel.Flywheel;
import com.jozufozu.flywheel.backend.gl.GLSLVersion;
import com.jozufozu.flywheel.core.compile.ProgramCompiler; import com.jozufozu.flywheel.core.compile.ProgramCompiler;
import com.jozufozu.flywheel.core.crumbling.CrumblingProgram; import com.jozufozu.flywheel.core.crumbling.CrumblingProgram;
import com.jozufozu.flywheel.core.shader.NormalDebugStateProvider; import com.jozufozu.flywheel.core.shader.NormalDebugStateProvider;
import com.jozufozu.flywheel.core.shader.WorldProgram; import com.jozufozu.flywheel.core.shader.WorldProgram;
import com.jozufozu.flywheel.core.source.FileResolution; import com.jozufozu.flywheel.core.source.FileResolution;
import com.jozufozu.flywheel.core.source.Resolver; import com.jozufozu.flywheel.core.source.SourceChecks;
import com.jozufozu.flywheel.event.GatherContextEvent;
import com.jozufozu.flywheel.util.ResourceUtil; import com.jozufozu.flywheel.util.ResourceUtil;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@ -17,16 +17,23 @@ public class Contexts {
public static ProgramCompiler<WorldProgram> WORLD; public static ProgramCompiler<WorldProgram> WORLD;
public static ProgramCompiler<CrumblingProgram> CRUMBLING; public static ProgramCompiler<CrumblingProgram> CRUMBLING;
public static void flwInit(GatherContextEvent event) { public static void init() {
GameStateRegistry.register(NormalDebugStateProvider.INSTANCE); GameStateRegistry.register(NormalDebugStateProvider.INSTANCE);
FileResolution worldVertex = Resolver.INSTANCE.get(ResourceUtil.subPath(Names.WORLD, ".vert")); var checkFrag = SourceChecks.checkFunctionArity("flw_contextFragment", 0);
FileResolution worldFragment = Resolver.INSTANCE.get(ResourceUtil.subPath(Names.WORLD, ".frag")); var checkVert = SourceChecks.checkFunctionArity("flw_contextVertex", 0);
FileResolution crumblingVertex = Resolver.INSTANCE.get(ResourceUtil.subPath(Names.CRUMBLING, ".vert"));
FileResolution crumblingFragment = Resolver.INSTANCE.get(ResourceUtil.subPath(Names.CRUMBLING, ".frag"));
WORLD = ProgramCompiler.create(WorldProgram::new, worldVertex, worldFragment); var worldVertex = FileResolution.get(ResourceUtil.subPath(Names.WORLD, ".vert"))
CRUMBLING = ProgramCompiler.create(CrumblingProgram::new, crumblingVertex, crumblingFragment); .validateWith(checkVert);
var worldFragment = FileResolution.get(ResourceUtil.subPath(Names.WORLD, ".frag"))
.validateWith(checkFrag);
var crumblingVertex = FileResolution.get(ResourceUtil.subPath(Names.CRUMBLING, ".vert"))
.validateWith(checkVert);
var crumblingFragment = FileResolution.get(ResourceUtil.subPath(Names.CRUMBLING, ".frag"))
.validateWith(checkFrag);
WORLD = ProgramCompiler.create(WorldProgram::new, worldVertex, worldFragment, GLSLVersion.V330);
CRUMBLING = ProgramCompiler.create(CrumblingProgram::new, crumblingVertex, crumblingFragment, GLSLVersion.V330);
} }
public static class Names { public static class Names {

View file

@ -51,8 +51,4 @@ public class GameStateRegistry {
} }
return shaderConstants; return shaderConstants;
} }
public static void _clear() {
PROVIDERS.clear();
}
} }

View file

@ -2,9 +2,14 @@ package com.jozufozu.flywheel.core.compile;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jetbrains.annotations.NotNull;
import com.jozufozu.flywheel.backend.gl.GLSLVersion; import com.jozufozu.flywheel.backend.gl.GLSLVersion;
import com.jozufozu.flywheel.backend.gl.shader.ShaderType; import com.jozufozu.flywheel.backend.gl.shader.ShaderType;
import com.jozufozu.flywheel.core.source.SourceFile;
public class CompileUtil { public class CompileUtil {
@ -45,4 +50,11 @@ public class CompileUtil {
return 1; return 1;
} }
@NotNull
public static String generateDebugName(SourceFile... stages) {
return Stream.of(stages)
.map(SourceFile::toString)
.collect(Collectors.joining(" -> "));
}
} }

View file

@ -1,7 +1,6 @@
package com.jozufozu.flywheel.core.compile; package com.jozufozu.flywheel.core.compile;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.jozufozu.flywheel.backend.gl.GLSLVersion; import com.jozufozu.flywheel.backend.gl.GLSLVersion;
@ -12,92 +11,57 @@ import com.jozufozu.flywheel.core.shader.ShaderConstants;
import com.jozufozu.flywheel.core.shader.StateSnapshot; import com.jozufozu.flywheel.core.shader.StateSnapshot;
import com.jozufozu.flywheel.core.source.FileIndexImpl; import com.jozufozu.flywheel.core.source.FileIndexImpl;
import com.jozufozu.flywheel.core.source.FileResolution; import com.jozufozu.flywheel.core.source.FileResolution;
import com.jozufozu.flywheel.core.source.ShaderLoadingException;
import com.jozufozu.flywheel.core.source.SourceFile; import com.jozufozu.flywheel.core.source.SourceFile;
import com.jozufozu.flywheel.core.source.error.ErrorReporter;
import com.jozufozu.flywheel.core.source.parse.ShaderFunction;
import com.jozufozu.flywheel.core.source.parse.Variable;
/**
* Handles compilation and deletion of fragment shaders.
*/
public class FragmentCompiler extends Memoizer<FragmentCompiler.Context, GlShader> { public class FragmentCompiler extends Memoizer<FragmentCompiler.Context, GlShader> {
private final FileResolution contextShader; private final FileResolution contextShader;
private final GLSLVersion glslVersion;
public FragmentCompiler(FileResolution contextShader) { public FragmentCompiler(FileResolution contextShader, GLSLVersion glslVersion) {
this.contextShader = contextShader; this.contextShader = contextShader;
this.glslVersion = glslVersion;
} }
@Override @Override
protected GlShader _create(Context key) { protected GlShader _create(Context key) {
StringBuilder finalSource = new StringBuilder(); StringBuilder finalSource = new StringBuilder();
finalSource.append(CompileUtil.generateHeader(GLSLVersion.V150, ShaderType.FRAGMENT)); finalSource.append(CompileUtil.generateHeader(glslVersion, ShaderType.FRAGMENT));
key.getShaderConstants().writeInto(finalSource); ShaderConstants shaderConstants = key.getShaderConstants();
shaderConstants.writeInto(finalSource);
finalSource.append('\n'); finalSource.append('\n');
FileIndexImpl index = new FileIndexImpl(); FileIndexImpl index = new FileIndexImpl();
// // MATERIAL
SourceFile materialShader = key.materialShader; SourceFile materialShader = key.materialShader;
Optional<ShaderFunction> maybeMaterialFragment = materialShader.findFunction("flw_materialFragment");
if (maybeMaterialFragment.isEmpty()) {
ErrorReporter.generateMissingFunction(materialShader, "flw_materialFragment", "\"flw_materialFragment\" function not defined");
throw new ShaderLoadingException();
}
ShaderFunction materialFragment = maybeMaterialFragment.get();
ImmutableList<Variable> params = materialFragment.getParameters();
if (params.size() != 0) {
ErrorReporter.generateSpanError(materialFragment.getArgs(), "\"flw_materialFragment\" function must not have any arguments");
throw new ShaderLoadingException();
}
materialShader.generateFinalSource(index, finalSource); materialShader.generateFinalSource(index, finalSource);
// // CONTEXT
SourceFile contextShaderSource = contextShader.getFile(); SourceFile contextShaderSource = contextShader.getFile();
Optional<ShaderFunction> maybeContextFragment = contextShaderSource.findFunction("flw_contextFragment");
if (maybeContextFragment.isEmpty()) {
ErrorReporter.generateMissingFunction(contextShaderSource, "flw_contextFragment", "\"flw_contextFragment\" function not defined");
throw new ShaderLoadingException();
}
ShaderFunction contextFragment = maybeContextFragment.get();
params = contextFragment.getParameters();
if (params.size() != 0) {
ErrorReporter.generateSpanError(contextFragment.getArgs(), "\"flw_contextFragment\" function must not have any arguments");
throw new ShaderLoadingException();
}
contextShaderSource.generateFinalSource(index, finalSource); contextShaderSource.generateFinalSource(index, finalSource);
// // MAIN
finalSource.append(generateFooter()); finalSource.append(generateFooter());
return new GlShader(contextShader.getFile().name, ShaderType.FRAGMENT, finalSource.toString()); return new GlShader(finalSource.toString(), ShaderType.FRAGMENT, ImmutableList.of(materialShader.name, contextShaderSource.name), shaderConstants);
} }
protected String generateFooter() { protected String generateFooter() {
StringBuilder footer = new StringBuilder(); return """
footer.append("""
void main() { void main() {
flw_materialFragment(); flw_materialFragment();
flw_contextFragment(); flw_contextFragment();
} }
""" """;
);
return footer.toString();
} }
@Override @Override
@ -107,34 +71,12 @@ public class FragmentCompiler extends Memoizer<FragmentCompiler.Context, GlShade
/** /**
* Represents the conditions under which a shader is compiled. * Represents the conditions under which a shader is compiled.
* @param materialShader The fragment material shader source.
* @param alphaDiscard Alpha threshold below which fragments are discarded.
* @param fogType Which type of fog should be applied.
* @param ctx The shader constants to apply.
*/ */
public static final class Context { public record Context(SourceFile materialShader, float alphaDiscard, FogType fogType, StateSnapshot ctx) {
/**
* The fragment material shader source.
*/
private final SourceFile materialShader;
/**
* Alpha threshold below which fragments are discarded.
*/
private final float alphaDiscard;
/**
* Which type of fog should be applied.
*/
private final FogType fogType;
/**
* The shader constants to apply.
*/
private final StateSnapshot ctx;
public Context(SourceFile materialShader, float alphaDiscard, FogType fogType, StateSnapshot ctx) {
this.materialShader = materialShader;
this.alphaDiscard = alphaDiscard;
this.fogType = fogType;
this.ctx = ctx;
}
public ShaderConstants getShaderConstants() { public ShaderConstants getShaderConstants() {
ShaderConstants shaderConstants = ctx.getShaderConstants(); ShaderConstants shaderConstants = ctx.getShaderConstants();
@ -146,23 +88,5 @@ public class FragmentCompiler extends Memoizer<FragmentCompiler.Context, GlShade
return shaderConstants; return shaderConstants;
} }
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Context) obj;
return materialShader == that.materialShader && Objects.equals(this.ctx, that.ctx) && Float.floatToIntBits(this.alphaDiscard) == Float.floatToIntBits(that.alphaDiscard) && fogType == that.fogType;
}
@Override
public int hashCode() {
return Objects.hash(materialShader, alphaDiscard, fogType, ctx);
}
@Override
public String toString() {
return "Context[" + "materialShader=" + materialShader + ", " + "alphaDiscard=" + alphaDiscard + ", " + "fogType=" + fogType + ", " + "ctx=" + ctx + ']';
}
} }
} }

View file

@ -21,8 +21,6 @@ public class ProgramAssembler {
public final int program; public final int program;
private final ResourceLocation name; private final ResourceLocation name;
private final List<GlShader> shaders = new ObjectArrayList<>();
public ProgramAssembler(ResourceLocation name) { public ProgramAssembler(ResourceLocation name) {
this.name = name; this.name = name;
this.program = glCreateProgram(); this.program = glCreateProgram();
@ -49,13 +47,7 @@ public class ProgramAssembler {
return this; return this;
} }
public ProgramAssembler deleteLinkedShaders() {
shaders.forEach(GlShader::delete);
return this;
}
public ProgramAssembler attachShader(GlShader glShader) { public ProgramAssembler attachShader(GlShader glShader) {
shaders.add(glShader);
glAttachShader(this.program, glShader.handle()); glAttachShader(this.program, glShader.handle());
return this; return this;
} }

View file

@ -3,7 +3,11 @@ package com.jozufozu.flywheel.core.compile;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.jozufozu.flywheel.api.vertex.VertexType;
import com.jozufozu.flywheel.backend.gl.GLSLVersion;
import com.jozufozu.flywheel.backend.gl.shader.GlProgram; import com.jozufozu.flywheel.backend.gl.shader.GlProgram;
import com.jozufozu.flywheel.core.CoreShaderInfoMap;
import com.jozufozu.flywheel.core.shader.StateSnapshot;
import com.jozufozu.flywheel.core.source.FileResolution; import com.jozufozu.flywheel.core.source.FileResolution;
import com.jozufozu.flywheel.event.ReloadRenderersEvent; import com.jozufozu.flywheel.event.ReloadRenderersEvent;
@ -14,8 +18,11 @@ import com.jozufozu.flywheel.event.ReloadRenderersEvent;
* This class is responsible for compiling programs on the fly. An instance of this class will keep a cache of * This class is responsible for compiling programs on the fly. An instance of this class will keep a cache of
* compiled programs, and will only compile a program if it is not already in the cache. * compiled programs, and will only compile a program if it is not already in the cache.
* </p> * </p>
* <p>
* A ProgramCompiler is also responsible for deleting programs and shaders on renderer reload.
* </p>
*/ */
public class ProgramCompiler<P extends GlProgram> extends Memoizer<ProgramContext, P> { public class ProgramCompiler<P extends GlProgram> extends Memoizer<ProgramCompiler.Context, P> {
private static final List<ProgramCompiler<?>> ALL_COMPILERS = new ArrayList<>(); private static final List<ProgramCompiler<?>> ALL_COMPILERS = new ArrayList<>();
@ -39,8 +46,8 @@ public class ProgramCompiler<P extends GlProgram> extends Memoizer<ProgramContex
* @param <P> The type of program to compile. * @param <P> The type of program to compile.
* @return A program compiler. * @return A program compiler.
*/ */
public static <P extends GlProgram> ProgramCompiler<P> create(GlProgram.Factory<P> factory, FileResolution vertexContextShader, FileResolution fragmentContextShader) { public static <P extends GlProgram> ProgramCompiler<P> create(GlProgram.Factory<P> factory, FileResolution vertexContextShader, FileResolution fragmentContextShader, GLSLVersion glslVersion) {
return new ProgramCompiler<>(factory, new VertexCompiler(vertexContextShader), new FragmentCompiler(fragmentContextShader)); return new ProgramCompiler<>(factory, new VertexCompiler(vertexContextShader, glslVersion), new FragmentCompiler(fragmentContextShader, glslVersion));
} }
/** /**
@ -49,7 +56,7 @@ public class ProgramCompiler<P extends GlProgram> extends Memoizer<ProgramContex
* @param ctx The context of compilation. * @param ctx The context of compilation.
* @return A compiled GlProgram. * @return A compiled GlProgram.
*/ */
public P getProgram(ProgramContext ctx) { public P getProgram(Context ctx) {
return super.get(ctx); return super.get(ctx);
} }
@ -61,10 +68,10 @@ public class ProgramCompiler<P extends GlProgram> extends Memoizer<ProgramContex
} }
@Override @Override
protected P _create(ProgramContext ctx) { protected P _create(Context ctx) {
return new ProgramAssembler(ctx.instanceShader.getFileLoc()) return new ProgramAssembler(ctx.instanceShader().getFileLoc())
.attachShader(vertexCompiler.get(new VertexCompiler.Context(ctx.vertexType, ctx.instanceShader.getFile(), ctx.vertexMaterialShader.getFile(), ctx.ctx))) .attachShader(vertexCompiler.get(new VertexCompiler.Context(ctx.vertexType(), ctx.instanceShader().getFile(), ctx.vertexMaterialShader().getFile(), ctx.ctx())))
.attachShader(fragmentCompiler.get(new FragmentCompiler.Context(ctx.fragmentMaterialShader.getFile(), ctx.alphaDiscard, ctx.fogType, ctx.ctx))) .attachShader(fragmentCompiler.get(new FragmentCompiler.Context(ctx.fragmentMaterialShader().getFile(), ctx.alphaDiscard(), ctx.fogType(), ctx.ctx())))
.link() .link()
.build(this.factory); .build(this.factory);
} }
@ -77,4 +84,19 @@ public class ProgramCompiler<P extends GlProgram> extends Memoizer<ProgramContex
public static void invalidateAll(ReloadRenderersEvent ignored) { public static void invalidateAll(ReloadRenderersEvent ignored) {
ALL_COMPILERS.forEach(ProgramCompiler::invalidate); ALL_COMPILERS.forEach(ProgramCompiler::invalidate);
} }
/**
* Represents the entire context of a program's usage.
*
* @param vertexType The vertexType the program should be adapted for.
* @param instanceShader The instance shader to use.
* @param vertexMaterialShader The vertex material shader to use.
* @param fragmentMaterialShader The fragment material shader to use.
* @param alphaDiscard Alpha threshold below which pixels are discarded.
* @param fogType Which type of fog should be applied.
* @param ctx A snapshot of the game state.
*/
public record Context(VertexType vertexType, FileResolution instanceShader, FileResolution vertexMaterialShader,
FileResolution fragmentMaterialShader, float alphaDiscard, CoreShaderInfoMap.CoreShaderInfo.FogType fogType, StateSnapshot ctx) {
}
} }

View file

@ -1,59 +0,0 @@
package com.jozufozu.flywheel.core.compile;
import java.util.Objects;
import com.jozufozu.flywheel.api.vertex.VertexType;
import com.jozufozu.flywheel.core.CoreShaderInfoMap.CoreShaderInfo.FogType;
import com.jozufozu.flywheel.core.shader.StateSnapshot;
import com.jozufozu.flywheel.core.source.FileResolution;
/**
* Represents the entire context of a program's usage.
*/
public final class ProgramContext {
public final VertexType vertexType;
public final FileResolution instanceShader;
public final FileResolution vertexMaterialShader;
public final FileResolution fragmentMaterialShader;
public final float alphaDiscard;
public final FogType fogType;
public final StateSnapshot ctx;
/**
* @param vertexType The vertexType the program should be adapted for.
* @param instanceShader The instance shader to use.
* @param vertexMaterialShader The vertex material shader to use.
* @param fragmentMaterialShader The fragment material shader to use.
* @param alphaDiscard Alpha threshold below which pixels are discarded.
* @param fogType Which type of fog should be applied.
* @param ctx A snapshot of the game state.
*/
public ProgramContext(VertexType vertexType, FileResolution instanceShader, FileResolution vertexMaterialShader, FileResolution fragmentMaterialShader, float alphaDiscard, FogType fogType, StateSnapshot ctx) {
this.vertexType = vertexType;
this.instanceShader = instanceShader;
this.vertexMaterialShader = vertexMaterialShader;
this.fragmentMaterialShader = fragmentMaterialShader;
this.alphaDiscard = alphaDiscard;
this.fogType = fogType;
this.ctx = ctx;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
var that = (ProgramContext) o;
return vertexType == that.vertexType && instanceShader == that.instanceShader && vertexMaterialShader == that.vertexMaterialShader && fragmentMaterialShader == that.fragmentMaterialShader && ctx.equals(that.ctx) && Float.floatToIntBits(alphaDiscard) == Float.floatToIntBits(that.alphaDiscard) && fogType == that.fogType;
}
@Override
public int hashCode() {
return Objects.hash(vertexType, instanceShader, vertexMaterialShader, fragmentMaterialShader, alphaDiscard, fogType, ctx);
}
@Override
public String toString() {
return "ProgramContext{" + "vertexType=" + vertexType + ", instanceShader=" + instanceShader + ", vertexMaterialShader=" + vertexMaterialShader + ", fragmentMaterialShader=" + fragmentMaterialShader + ", alphaDiscard=" + alphaDiscard + ", fogType=" + fogType + ", ctx=" + ctx + '}';
}
}

View file

@ -1,8 +1,5 @@
package com.jozufozu.flywheel.core.compile; package com.jozufozu.flywheel.core.compile;
import java.util.Objects;
import java.util.Optional;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.jozufozu.flywheel.api.vertex.VertexType; import com.jozufozu.flywheel.api.vertex.VertexType;
import com.jozufozu.flywheel.backend.gl.GLSLVersion; import com.jozufozu.flywheel.backend.gl.GLSLVersion;
@ -11,132 +8,63 @@ import com.jozufozu.flywheel.backend.gl.shader.ShaderType;
import com.jozufozu.flywheel.core.shader.StateSnapshot; import com.jozufozu.flywheel.core.shader.StateSnapshot;
import com.jozufozu.flywheel.core.source.FileIndexImpl; import com.jozufozu.flywheel.core.source.FileIndexImpl;
import com.jozufozu.flywheel.core.source.FileResolution; import com.jozufozu.flywheel.core.source.FileResolution;
import com.jozufozu.flywheel.core.source.ShaderLoadingException;
import com.jozufozu.flywheel.core.source.SourceFile; import com.jozufozu.flywheel.core.source.SourceFile;
import com.jozufozu.flywheel.core.source.error.ErrorReporter;
import com.jozufozu.flywheel.core.source.parse.ShaderFunction;
import com.jozufozu.flywheel.core.source.parse.ShaderStruct; import com.jozufozu.flywheel.core.source.parse.ShaderStruct;
import com.jozufozu.flywheel.core.source.parse.StructField; import com.jozufozu.flywheel.core.source.parse.StructField;
import com.jozufozu.flywheel.core.source.parse.Variable;
import com.jozufozu.flywheel.core.source.span.Span;
/**
* Handles compilation and deletion of vertex shaders.
*/
public class VertexCompiler extends Memoizer<VertexCompiler.Context, GlShader> { public class VertexCompiler extends Memoizer<VertexCompiler.Context, GlShader> {
private final FileResolution contextShader; private final FileResolution contextShader;
private final GLSLVersion glslVersion;
public VertexCompiler(FileResolution contextShader) { public VertexCompiler(FileResolution contextShader, GLSLVersion glslVersion) {
this.contextShader = contextShader; this.contextShader = contextShader;
this.glslVersion = glslVersion;
} }
@Override @Override
protected GlShader _create(Context key) { protected GlShader _create(Context key) {
StringBuilder finalSource = new StringBuilder(); StringBuilder finalSource = new StringBuilder();
finalSource.append(CompileUtil.generateHeader(GLSLVersion.V330, ShaderType.VERTEX)); finalSource.append(CompileUtil.generateHeader(glslVersion, ShaderType.VERTEX));
key.ctx.getShaderConstants().writeInto(finalSource); var shaderConstants = key.ctx.getShaderConstants();
shaderConstants.writeInto(finalSource);
finalSource.append('\n'); finalSource.append('\n');
FileIndexImpl index = new FileIndexImpl(); var index = new FileIndexImpl();
// // LAYOUT
SourceFile layoutShader = key.vertexType.getLayoutShader().getFile();
Optional<ShaderFunction> maybeLayoutVertex = layoutShader.findFunction("flw_layoutVertex");
if (maybeLayoutVertex.isEmpty()) {
ErrorReporter.generateMissingFunction(layoutShader, "flw_layoutVertex", "\"flw_layoutVertex\" function not defined");
throw new ShaderLoadingException();
}
ShaderFunction layoutVertex = maybeLayoutVertex.get();
ImmutableList<Variable> params = layoutVertex.getParameters();
if (params.size() != 0) {
ErrorReporter.generateSpanError(layoutVertex.getArgs(), "\"flw_layoutVertex\" function must not have any arguments");
throw new ShaderLoadingException();
}
var layoutShader = key.vertexType.getLayoutShader().getFile();
layoutShader.generateFinalSource(index, finalSource); layoutShader.generateFinalSource(index, finalSource);
// // INSTANCE
SourceFile instanceShader = key.instanceShader;
Optional<ShaderFunction> maybeInstanceVertex = instanceShader.findFunction("flw_instanceVertex");
if (maybeInstanceVertex.isEmpty()) {
ErrorReporter.generateMissingFunction(instanceShader, "flw_instanceVertex", "\"flw_instanceVertex\" function not defined");
throw new ShaderLoadingException();
}
ShaderFunction instanceVertex = maybeInstanceVertex.get();
params = instanceVertex.getParameters();
if (params.size() != 1) {
ErrorReporter.generateSpanError(instanceVertex.getArgs(), "\"flw_instanceVertex\" function must have exactly 1 argument");
throw new ShaderLoadingException();
}
Span instanceName = params.get(0).type;
Optional<ShaderStruct> maybeInstance = instanceShader.findStruct(instanceName);
if (maybeInstance.isEmpty()) {
ErrorReporter.generateMissingStruct(instanceShader, instanceName, "instance struct not defined");
throw new ShaderLoadingException();
}
ShaderStruct instance = maybeInstance.get();
var instanceShader = key.instanceShader;
instanceShader.generateFinalSource(index, finalSource); instanceShader.generateFinalSource(index, finalSource);
// // MATERIAL
SourceFile materialShader = key.materialShader;
Optional<ShaderFunction> maybeMaterialVertex = materialShader.findFunction("flw_materialVertex");
if (maybeMaterialVertex.isEmpty()) {
ErrorReporter.generateMissingFunction(materialShader, "flw_materialVertex", "\"flw_materialVertex\" function not defined");
throw new ShaderLoadingException();
}
ShaderFunction materialVertex = maybeMaterialVertex.get();
params = materialVertex.getParameters();
if (params.size() != 0) {
ErrorReporter.generateSpanError(materialVertex.getArgs(), "\"flw_materialVertex\" function must not have any arguments");
throw new ShaderLoadingException();
}
var materialShader = key.materialShader;
materialShader.generateFinalSource(index, finalSource); materialShader.generateFinalSource(index, finalSource);
// // CONTEXT
SourceFile contextShaderSource = contextShader.getFile();
Optional<ShaderFunction> maybeContextVertex = contextShaderSource.findFunction("flw_contextVertex");
if (maybeContextVertex.isEmpty()) {
ErrorReporter.generateMissingFunction(contextShaderSource, "flw_contextVertex", "\"flw_contextVertex\" function not defined");
throw new ShaderLoadingException();
}
ShaderFunction contextVertex = maybeContextVertex.get();
params = contextVertex.getParameters();
if (params.size() != 0) {
ErrorReporter.generateSpanError(contextVertex.getArgs(), "\"flw_contextVertex\" function must not have any arguments");
throw new ShaderLoadingException();
}
var contextShaderSource = contextShader.getFile();
contextShaderSource.generateFinalSource(index, finalSource); contextShaderSource.generateFinalSource(index, finalSource);
// // MAIN
finalSource.append(generateFooter(key.vertexType, instance)); var instanceStruct = instanceShader.findFunction("flw_instanceVertex")
.flatMap(f -> f.getParameterType(0)
.findStruct())
.orElseThrow();
finalSource.append(generateFooter(key.vertexType, instanceStruct));
return new GlShader(instanceShader.name, ShaderType.VERTEX, finalSource.toString()); return new GlShader(finalSource.toString(), ShaderType.VERTEX, ImmutableList.of(layoutShader.name, instanceShader.name, materialShader.name, contextShaderSource.name), shaderConstants);
} }
protected String generateFooter(VertexType vertexType, ShaderStruct instance) { protected String generateFooter(VertexType vertexType, ShaderStruct instance) {
@ -203,45 +131,12 @@ public class VertexCompiler extends Memoizer<VertexCompiler.Context, GlShader> {
value.delete(); value.delete();
} }
public static class Context {
/** /**
* The vertex type to use. * @param vertexType The vertex type to use.
* @param instanceShader The instance shader source.
* @param materialShader The vertex material shader source.
* @param ctx The shader constants to apply.
*/ */
private final VertexType vertexType; public record Context(VertexType vertexType, SourceFile instanceShader, SourceFile materialShader, StateSnapshot ctx) {
/**
* The instance shader source.
*/
private final SourceFile instanceShader;
/**
* The vertex material shader source.
*/
private final SourceFile materialShader;
/**
* The shader constants to apply.
*/
private final StateSnapshot ctx;
public Context(VertexType vertexType, SourceFile instanceShader, SourceFile materialShader, StateSnapshot ctx) {
this.vertexType = vertexType;
this.instanceShader = instanceShader;
this.materialShader = materialShader;
this.ctx = ctx;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
var that = (Context) o;
return vertexType == that.vertexType && instanceShader == that.instanceShader && materialShader == that.materialShader && ctx.equals(that.ctx);
}
@Override
public int hashCode() {
return Objects.hash(vertexType, instanceShader, materialShader, ctx);
}
} }
} }

View file

@ -2,8 +2,7 @@ package com.jozufozu.flywheel.core.material;
import com.jozufozu.flywheel.Flywheel; import com.jozufozu.flywheel.Flywheel;
import com.jozufozu.flywheel.core.source.FileResolution; import com.jozufozu.flywheel.core.source.FileResolution;
import com.jozufozu.flywheel.core.source.Resolver; import com.jozufozu.flywheel.core.source.SourceChecks;
import com.jozufozu.flywheel.event.GatherContextEvent;
import com.jozufozu.flywheel.util.ResourceUtil; import com.jozufozu.flywheel.util.ResourceUtil;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@ -13,10 +12,16 @@ public class MaterialShaders {
public static FileResolution DEFAULT_FRAGMENT; public static FileResolution DEFAULT_FRAGMENT;
public static FileResolution SHADED_VERTEX; public static FileResolution SHADED_VERTEX;
public static void flwInit(GatherContextEvent event) { public static void init() {
DEFAULT_VERTEX = Resolver.INSTANCE.get(ResourceUtil.subPath(Names.DEFAULT, ".vert")); var checkVert = SourceChecks.checkFunctionArity("flw_materialVertex", 0);
DEFAULT_FRAGMENT = Resolver.INSTANCE.get(ResourceUtil.subPath(Names.DEFAULT, ".frag")); var checkFrag = SourceChecks.checkFunctionArity("flw_materialFragment", 0);
SHADED_VERTEX = Resolver.INSTANCE.get(ResourceUtil.subPath(Names.SHADED, ".vert"));
DEFAULT_VERTEX = FileResolution.get(ResourceUtil.subPath(Names.DEFAULT, ".vert"))
.validateWith(checkVert);
DEFAULT_FRAGMENT = FileResolution.get(ResourceUtil.subPath(Names.DEFAULT, ".frag"))
.validateWith(checkFrag);
SHADED_VERTEX = FileResolution.get(ResourceUtil.subPath(Names.SHADED, ".vert"))
.validateWith(checkVert);
} }
public static class Names { public static class Names {

View file

@ -3,6 +3,7 @@ package com.jozufozu.flywheel.core.shader;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
/** /**
* A class for manipulating a list of {@code #define} directives. * A class for manipulating a list of {@code #define} directives.
@ -52,4 +53,17 @@ public class ShaderConstants {
acc.append('\n'); acc.append('\n');
} }
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ShaderConstants that = (ShaderConstants) o;
return Objects.equals(definitions, that.definitions);
}
@Override
public int hashCode() {
return Objects.hash(definitions);
}
} }

View file

@ -33,7 +33,7 @@ public class FileIndexImpl implements FileIndex {
@Override @Override
public boolean exists(SourceFile sourceFile) { public boolean exists(SourceFile sourceFile) {
return files.indexOf(sourceFile) != -1; return files.contains(sourceFile);
} }
@Override @Override
@ -68,10 +68,7 @@ public class FileIndexImpl implements FileIndex {
@Nullable @Nullable
private ErrorBuilder parseCompilerError(String line) { private ErrorBuilder parseCompilerError(String line) {
try { try {
ErrorBuilder error = ErrorBuilder.fromLogLine(this, line); return ErrorBuilder.fromLogLine(this, line);
if (error != null) {
return error;
}
} catch (Exception ignored) { } catch (Exception ignored) {
} }

View file

@ -1,11 +1,13 @@
package com.jozufozu.flywheel.core.source; package com.jozufozu.flywheel.core.source;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.Map;
import java.util.function.BiConsumer;
import com.jozufozu.flywheel.backend.Backend;
import com.jozufozu.flywheel.core.source.error.ErrorBuilder; import com.jozufozu.flywheel.core.source.error.ErrorBuilder;
import com.jozufozu.flywheel.core.source.error.ErrorReporter;
import com.jozufozu.flywheel.core.source.span.Span; import com.jozufozu.flywheel.core.source.span.Span;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@ -21,17 +23,73 @@ import net.minecraft.resources.ResourceLocation;
*/ */
public class FileResolution { public class FileResolution {
private static final Map<ResourceLocation, FileResolution> ALL = new HashMap<>();
private static boolean tooLate = false;
/** /**
* Extra info about where this resolution is required. Includes ProgramSpecs and shader Spans. * Extra info about where this resolution is required. Includes shader Spans.
*/ */
private final List<Consumer<ErrorBuilder>> extraCrashInfoProviders = new ArrayList<>(); private final List<Span> neededAt = new ArrayList<>();
private final List<BiConsumer<ErrorReporter, SourceFile>> checks = new ArrayList<>();
private final ResourceLocation fileLoc; private final ResourceLocation fileLoc;
private SourceFile file; private SourceFile file;
FileResolution(ResourceLocation fileLoc) { FileResolution(ResourceLocation fileLoc) {
this.fileLoc = fileLoc; this.fileLoc = fileLoc;
} }
public static FileResolution get(ResourceLocation file) {
if (!tooLate) {
return ALL.computeIfAbsent(file, FileResolution::new);
} else {
// Lock the map after resolution has run.
FileResolution fileResolution = ALL.get(file);
// ...so crash immediately if the file isn't found.
if (fileResolution == null) {
throw new ShaderLoadingException("could not find source for file: " + file);
}
return fileResolution;
}
}
/**
* Try and resolve all referenced source files, printing errors if any aren't found.
*/
public static void run(ErrorReporter errorReporter, SourceFinder sources) {
for (FileResolution resolution : ALL.values()) {
resolution.resolveAndCheck(errorReporter, sources);
}
tooLate = true;
}
private void resolveAndCheck(ErrorReporter errorReporter, SourceFinder sources) {
file = sources.findSource(fileLoc);
if (file == null) {
ErrorBuilder builder = errorReporter.error(String.format("could not find source for file %s", fileLoc));
for (Span location : neededAt) {
builder.pointAtFile(location.getSourceFile())
.pointAt(location, 1);
}
} else {
runChecks(errorReporter);
}
// Let the GC do its thing
neededAt.clear();
}
private void runChecks(ErrorReporter errorReporter) {
for (var check : checks) {
check.accept(errorReporter, file);
}
}
public ResourceLocation getFileLoc() { public ResourceLocation getFileLoc() {
return fileLoc; return fileLoc;
} }
@ -53,45 +111,13 @@ public class FileResolution {
* @param span A span where this file is referenced. * @param span A span where this file is referenced.
*/ */
public FileResolution addSpan(Span span) { public FileResolution addSpan(Span span) {
extraCrashInfoProviders.add(builder -> builder.pointAtFile(span.getSourceFile()) neededAt.add(span);
.pointAt(span, 1));
return this; return this;
} }
public void addSpec(ResourceLocation name) { public FileResolution validateWith(BiConsumer<ErrorReporter, SourceFile> check) {
extraCrashInfoProviders.add(builder -> builder.extra("needed by spec: " + name + ".json")); checks.add(check);
} return this;
/**
* Check to see if this file actually resolves to something.
*
* <p>
* Called after all files are loaded. If we can't find the file here, it doesn't exist.
* </p>
*
* @return True if this file is resolved.
*/
boolean resolve(SourceFinder sources) {
file = sources.findSource(fileLoc);
if (file == null) {
ErrorBuilder builder = ErrorBuilder.error(String.format("could not find source for file %s", fileLoc));
for (Consumer<ErrorBuilder> consumer : extraCrashInfoProviders) {
consumer.accept(builder);
}
Backend.LOGGER.error(builder.build());
return false;
}
// Let the GC do its thing
extraCrashInfoProviders.clear();
return true;
}
void invalidate() {
extraCrashInfoProviders.clear();
file = null;
} }
@Override @Override

View file

@ -1,70 +0,0 @@
package com.jozufozu.flywheel.core.source;
import java.util.HashMap;
import java.util.Map;
import net.minecraft.resources.ResourceLocation;
/**
* Manages deferred file resolution.
*
* <p>
* Interns all file names in shader sources and program specs, deduplicating the final lookups and allowing for more
* dev-friendly error reporting.
* </p>
*
* @see FileResolution
*/
public class Resolver {
public static final Resolver INSTANCE = new Resolver();
private final Map<ResourceLocation, FileResolution> resolutions = new HashMap<>();
private boolean hasRun = false;
public FileResolution get(ResourceLocation file) {
if (!hasRun) {
return resolutions.computeIfAbsent(file, FileResolution::new);
} else {
// Lock the map after resolution has run.
FileResolution fileResolution = resolutions.get(file);
// ...so crash immediately if the file isn't found.
if (fileResolution == null) {
throw new RuntimeException("could not find source for file: " + file);
}
return fileResolution;
}
}
/**
* Try and resolve all referenced source files, printing errors if any aren't found.
*/
public void run(SourceFinder sources) {
boolean needsCrash = false;
for (FileResolution resolution : resolutions.values()) {
if (!resolution.resolve(sources)) {
needsCrash = true;
}
}
if (needsCrash) {
throw new ShaderLoadingException("Failed to resolve all source files, see log for details");
}
hasRun = true;
}
/**
* Invalidates all FileResolutions.
*
* <p>
* Called on resource reload.
* </p>
*/
public void invalidate() {
resolutions.values().forEach(FileResolution::invalidate);
hasRun = false;
}
}

View file

@ -2,9 +2,6 @@ package com.jozufozu.flywheel.core.source;
public class ShaderLoadingException extends RuntimeException { public class ShaderLoadingException extends RuntimeException {
public ShaderLoadingException() {
}
public ShaderLoadingException(String message) { public ShaderLoadingException(String message) {
super(message); super(message);
} }

View file

@ -9,6 +9,7 @@ import java.util.Map;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.jozufozu.flywheel.core.source.error.ErrorReporter;
import com.jozufozu.flywheel.util.ResourceUtil; import com.jozufozu.flywheel.util.ResourceUtil;
import com.jozufozu.flywheel.util.StringUtil; import com.jozufozu.flywheel.util.StringUtil;
@ -20,14 +21,14 @@ import net.minecraft.server.packs.resources.ResourceManager;
* The main object for loading and parsing source files. * The main object for loading and parsing source files.
*/ */
public class ShaderSources implements SourceFinder { public class ShaderSources implements SourceFinder {
public static final String SHADER_DIR = "flywheel/shaders/"; public static final String SHADER_DIR = "flywheel/";
public static final ArrayList<String> EXTENSIONS = Lists.newArrayList(".vert", ".vsh", ".frag", ".fsh", ".glsl"); public static final ArrayList<String> EXTENSIONS = Lists.newArrayList(".vert", ".vsh", ".frag", ".fsh", ".glsl");
private final Map<ResourceLocation, SourceFile> shaderSources = new HashMap<>(); private final Map<ResourceLocation, SourceFile> shaderSources = new HashMap<>();
public final Index index; public final Index index;
public ShaderSources(ResourceManager manager) { public ShaderSources(ErrorReporter errorReporter, ResourceManager manager) {
Collection<ResourceLocation> allShaders = manager.listResources(SHADER_DIR, s -> { Collection<ResourceLocation> allShaders = manager.listResources(SHADER_DIR, s -> {
for (String ext : EXTENSIONS) { for (String ext : EXTENSIONS) {
if (s.endsWith(ext)) return true; if (s.endsWith(ext)) return true;
@ -41,7 +42,7 @@ public class ShaderSources implements SourceFinder {
ResourceLocation name = ResourceUtil.removePrefixUnchecked(location, SHADER_DIR); ResourceLocation name = ResourceUtil.removePrefixUnchecked(location, SHADER_DIR);
shaderSources.put(name, new SourceFile(this, name, source)); shaderSources.put(name, new SourceFile(errorReporter, this, name, source));
} catch (IOException e) { } catch (IOException e) {
// //
} }

View file

@ -0,0 +1,57 @@
package com.jozufozu.flywheel.core.source;
import java.util.Optional;
import java.util.function.BiConsumer;
import org.jetbrains.annotations.Nullable;
import com.google.common.collect.ImmutableList;
import com.jozufozu.flywheel.core.source.error.ErrorReporter;
import com.jozufozu.flywheel.core.source.parse.ShaderFunction;
import com.jozufozu.flywheel.core.source.parse.Variable;
public class SourceChecks {
public static BiConsumer<ErrorReporter, SourceFile> checkFunctionArity(String name, int arity) {
return (errorReporter, file) -> checkFunctionArity(errorReporter, file, name, arity);
}
public static BiConsumer<ErrorReporter, SourceFile> checkFunctionParameterTypeExists(String name, int arity, int param) {
return (errorReporter, file) -> {
var func = checkFunctionArity(errorReporter, file, name, arity);
if (func == null) {
return;
}
var maybeStruct = func.getParameterType(param)
.findStruct();
if (maybeStruct.isEmpty()) {
errorReporter.generateMissingStruct(file, func.getParameterType(param), "struct not defined");
}
};
}
/**
* @return {@code null} if the function doesn't exist, or if the function has the wrong arity.
*/
@Nullable
private static ShaderFunction checkFunctionArity(ErrorReporter errorReporter, SourceFile file, String name, int arity) {
Optional<ShaderFunction> maybeFunc = file.findFunction(name);
if (maybeFunc.isEmpty()) {
errorReporter.generateMissingFunction(file, name, "\"" + name + "\" function not defined");
return null;
}
ShaderFunction func = maybeFunc.get();
ImmutableList<Variable> params = func.getParameters();
if (params.size() != arity) {
errorReporter.generateFunctionArgumentCountError(name, arity, func.getArgs());
return null;
}
return func;
}
}

View file

@ -11,9 +11,11 @@ import java.util.regex.Matcher;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.jozufozu.flywheel.core.source.error.ErrorReporter;
import com.jozufozu.flywheel.core.source.parse.Import; import com.jozufozu.flywheel.core.source.parse.Import;
import com.jozufozu.flywheel.core.source.parse.ShaderFunction; import com.jozufozu.flywheel.core.source.parse.ShaderFunction;
import com.jozufozu.flywheel.core.source.parse.ShaderStruct; import com.jozufozu.flywheel.core.source.parse.ShaderStruct;
import com.jozufozu.flywheel.core.source.parse.Variable;
import com.jozufozu.flywheel.core.source.span.ErrorSpan; import com.jozufozu.flywheel.core.source.span.ErrorSpan;
import com.jozufozu.flywheel.core.source.span.Span; import com.jozufozu.flywheel.core.source.span.Span;
import com.jozufozu.flywheel.core.source.span.StringSpan; import com.jozufozu.flywheel.core.source.span.StringSpan;
@ -55,7 +57,7 @@ public class SourceFile {
*/ */
public final ImmutableList<Import> imports; public final ImmutableList<Import> imports;
public SourceFile(ShaderSources parent, ResourceLocation name, String source) { public SourceFile(ErrorReporter errorReporter, ShaderSources parent, ResourceLocation name, String source) {
this.parent = parent; this.parent = parent;
this.name = name; this.name = name;
this.source = source; this.source = source;
@ -64,7 +66,7 @@ public class SourceFile {
List<Span> elisions = new ArrayList<>(); List<Span> elisions = new ArrayList<>();
this.imports = parseImports(elisions); this.imports = parseImports(errorReporter, elisions);
this.functions = parseFunctions(); this.functions = parseFunctions();
this.structs = parseStructs(); this.structs = parseStructs();
@ -233,7 +235,7 @@ public class SourceFile {
* Records the contents of the directive into an {@link Import} object, and marks the directive for elision. * Records the contents of the directive into an {@link Import} object, and marks the directive for elision.
* @param elisions * @param elisions
*/ */
private ImmutableList<Import> parseImports(List<Span> elisions) { private ImmutableList<Import> parseImports(ErrorReporter errorReporter, List<Span> elisions) {
Matcher uses = Import.PATTERN.matcher(source); Matcher uses = Import.PATTERN.matcher(source);
Set<String> importedFiles = new HashSet<>(); Set<String> importedFiles = new HashSet<>();
@ -245,9 +247,9 @@ public class SourceFile {
String fileName = file.get(); String fileName = file.get();
if (importedFiles.add(fileName)) { if (importedFiles.add(fileName)) {
Import import1 = Import.create(Resolver.INSTANCE, use, file); var checked = Import.create(errorReporter, use, file);
if (import1 != null) { if (checked != null) {
imports.add(import1); imports.add(checked);
} }
} }
@ -280,4 +282,15 @@ public class SourceFile {
public String toString() { public String toString() {
return name.toString(); return name.toString();
} }
@Override
public boolean equals(Object o) {
// SourceFiles are only equal by reference.
return this == o;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
} }

View file

@ -1,5 +1,6 @@
package com.jozufozu.flywheel.core.source.error; package com.jozufozu.flywheel.core.source.error;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -13,59 +14,79 @@ import com.jozufozu.flywheel.util.FlwUtil;
public class ErrorReporter { public class ErrorReporter {
public static void generateSpanError(Span span, String message) { private final List<ErrorBuilder> reportedErrors = new ArrayList<>();
SourceFile file = span.getSourceFile();
String error = ErrorBuilder.error(message) public void generateMissingStruct(SourceFile file, Span vertexName, CharSequence msg) {
.pointAtFile(file)
.pointAt(span, 2)
.build();
Backend.LOGGER.error(error);
}
public static void generateFileError(SourceFile file, String message) {
String error = ErrorBuilder.error(message)
.pointAtFile(file)
.build();
Backend.LOGGER.error(error);
}
public static void generateMissingStruct(SourceFile file, Span vertexName, CharSequence msg) {
generateMissingStruct(file, vertexName, msg, ""); generateMissingStruct(file, vertexName, msg, "");
} }
public static void generateMissingStruct(SourceFile file, Span vertexName, CharSequence msg, CharSequence hint) { public void generateMissingStruct(SourceFile file, Span vertexName, CharSequence msg, CharSequence hint) {
Optional<Span> span = file.parent.index.getStructDefinitionsMatching(vertexName) Optional<Span> span = file.parent.index.getStructDefinitionsMatching(vertexName)
.stream() .stream()
.findFirst() .findFirst()
.map(ShaderStruct::getName); .map(ShaderStruct::getName);
ErrorBuilder error = ErrorBuilder.error(msg) this.error(msg)
.pointAtFile(file) .pointAtFile(file)
.pointAt(vertexName, 1) .pointAt(vertexName, 1)
.hintIncludeFor(span.orElse(null), hint); .hintIncludeFor(span.orElse(null), hint);
Backend.LOGGER.error(error.build());
} }
public static void generateMissingFunction(SourceFile file, CharSequence functionName, CharSequence msg) { public void generateMissingFunction(SourceFile file, CharSequence functionName, CharSequence msg) {
generateMissingFunction(file, functionName, msg, ""); generateMissingFunction(file, functionName, msg, "");
} }
public static void generateMissingFunction(SourceFile file, CharSequence functionName, CharSequence msg, CharSequence hint) { public void generateMissingFunction(SourceFile file, CharSequence functionName, CharSequence msg, CharSequence hint) {
Optional<Span> span = file.parent.index.getFunctionDefinitionsMatching(functionName) Optional<Span> span = file.parent.index.getFunctionDefinitionsMatching(functionName)
.stream() .stream()
.findFirst() .findFirst()
.map(ShaderFunction::getName); .map(ShaderFunction::getName);
ErrorBuilder error = ErrorBuilder.error(msg) this.error(msg)
.pointAtFile(file) .pointAtFile(file)
.hintIncludeFor(span.orElse(null), hint); .hintIncludeFor(span.orElse(null), hint);
}
public ErrorBuilder generateFunctionArgumentCountError(String name, int requiredArguments, Span span) {
var msg = '"' + name + "\" function must ";
if (requiredArguments == 0) {
msg += "not have any arguments";
} else {
msg += "have exactly " + requiredArguments + " argument" + (requiredArguments == 1 ? "" : "s");
}
return generateSpanError(span, msg);
}
public ErrorBuilder generateSpanError(Span span, String message) {
SourceFile file = span.getSourceFile();
return error(message)
.pointAtFile(file)
.pointAt(span, 2);
}
public ErrorBuilder generateFileError(SourceFile file, String message) {
return error(message)
.pointAtFile(file);
}
public ErrorBuilder error(CharSequence msg) {
var out = ErrorBuilder.error(msg);
reportedErrors.add(out);
return out;
}
public boolean hasErrored() {
return !reportedErrors.isEmpty();
}
public void dump() {
for (var error : reportedErrors) {
Backend.LOGGER.error(error.build()); Backend.LOGGER.error(error.build());
} }
}
public static void printLines(CharSequence source) { public static void printLines(CharSequence source) {
String string = source.toString(); String string = source.toString();

View file

@ -6,7 +6,6 @@ import java.util.regex.Pattern;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import com.jozufozu.flywheel.core.source.FileResolution; import com.jozufozu.flywheel.core.source.FileResolution;
import com.jozufozu.flywheel.core.source.Resolver;
import com.jozufozu.flywheel.core.source.SourceFile; import com.jozufozu.flywheel.core.source.SourceFile;
import com.jozufozu.flywheel.core.source.error.ErrorReporter; import com.jozufozu.flywheel.core.source.error.ErrorReporter;
import com.jozufozu.flywheel.core.source.span.Span; import com.jozufozu.flywheel.core.source.span.Span;
@ -26,16 +25,16 @@ public class Import extends AbstractShaderElement {
} }
@Nullable @Nullable
public static Import create(Resolver resolver, Span self, Span file) { public static Import create(ErrorReporter errorReporter, Span self, Span file) {
ResourceLocation fileLocation; ResourceLocation fileLocation;
try { try {
fileLocation = new ResourceLocation(file.get()); fileLocation = new ResourceLocation(file.get());
} catch (ResourceLocationException e) { } catch (ResourceLocationException e) {
ErrorReporter.generateSpanError(file, "malformed source location"); errorReporter.generateSpanError(file, "malformed source location");
return null; return null;
} }
return new Import(self, resolver.get(fileLocation), file); return new Import(self, FileResolution.get(fileLocation), file);
} }
public FileResolution getResolution() { public FileResolution getResolution() {

View file

@ -52,6 +52,10 @@ public class ShaderFunction extends AbstractShaderElement {
return name + "(" + String.join(", ", args) + ")"; return name + "(" + String.join(", ", args) + ")";
} }
public Span getParameterType(int index) {
return parameters.get(index).type;
}
public ImmutableList<Variable> getParameters() { public ImmutableList<Variable> getParameters() {
return parameters; return parameters;
} }

View file

@ -1,8 +1,11 @@
package com.jozufozu.flywheel.core.source.span; package com.jozufozu.flywheel.core.source.span;
import java.util.Optional;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import com.jozufozu.flywheel.core.source.SourceFile; import com.jozufozu.flywheel.core.source.SourceFile;
import com.jozufozu.flywheel.core.source.parse.ShaderFunction;
import com.jozufozu.flywheel.core.source.parse.ShaderStruct;
/** /**
* A segment of code in a {@link SourceFile}. * A segment of code in a {@link SourceFile}.
@ -121,4 +124,18 @@ public abstract class Span implements CharSequence {
public static Span fromMatcher(Span superSpan, Matcher m) { public static Span fromMatcher(Span superSpan, Matcher m) {
return superSpan.subSpan(m.start(), m.end()); return superSpan.subSpan(m.start(), m.end());
} }
public Optional<ShaderStruct> findStruct() {
if (isErr()) {
return Optional.empty();
}
return in.findStruct(this);
}
public Optional<ShaderFunction> findFunction() {
if (isErr()) {
return Optional.empty();
}
return in.findFunction(this);
}
} }

View file

@ -2,8 +2,7 @@ package com.jozufozu.flywheel.core.structs;
import com.jozufozu.flywheel.Flywheel; import com.jozufozu.flywheel.Flywheel;
import com.jozufozu.flywheel.core.source.FileResolution; import com.jozufozu.flywheel.core.source.FileResolution;
import com.jozufozu.flywheel.core.source.Resolver; import com.jozufozu.flywheel.core.source.SourceChecks;
import com.jozufozu.flywheel.event.GatherContextEvent;
import com.jozufozu.flywheel.util.ResourceUtil; import com.jozufozu.flywheel.util.ResourceUtil;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@ -12,9 +11,13 @@ public class InstanceShaders {
public static FileResolution MODEL; public static FileResolution MODEL;
public static FileResolution ORIENTED; public static FileResolution ORIENTED;
public static void flwInit(GatherContextEvent event) { public static void init() {
MODEL = Resolver.INSTANCE.get(ResourceUtil.subPath(Names.MODEL, ".vert")); var check = SourceChecks.checkFunctionParameterTypeExists("flw_instanceVertex", 1, 0);
ORIENTED = Resolver.INSTANCE.get(ResourceUtil.subPath(Names.ORIENTED, ".vert"));
MODEL = FileResolution.get(ResourceUtil.subPath(Names.MODEL, ".vert"))
.validateWith(check);
ORIENTED = FileResolution.get(ResourceUtil.subPath(Names.ORIENTED, ".vert"))
.validateWith(check);
} }
public static class Names { public static class Names {

View file

@ -2,8 +2,7 @@ package com.jozufozu.flywheel.core.vertex;
import com.jozufozu.flywheel.Flywheel; import com.jozufozu.flywheel.Flywheel;
import com.jozufozu.flywheel.core.source.FileResolution; import com.jozufozu.flywheel.core.source.FileResolution;
import com.jozufozu.flywheel.core.source.Resolver; import com.jozufozu.flywheel.core.source.SourceChecks;
import com.jozufozu.flywheel.event.GatherContextEvent;
import com.jozufozu.flywheel.util.ResourceUtil; import com.jozufozu.flywheel.util.ResourceUtil;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@ -12,9 +11,14 @@ public class LayoutShaders {
public static FileResolution BLOCK; public static FileResolution BLOCK;
public static FileResolution POS_TEX_NORMAL; public static FileResolution POS_TEX_NORMAL;
public static void flwInit(GatherContextEvent event) { public static void init() {
BLOCK = Resolver.INSTANCE.get(ResourceUtil.subPath(Names.BLOCK, ".vert")); var check = SourceChecks.checkFunctionArity("flw_layoutVertex", 0);
POS_TEX_NORMAL = Resolver.INSTANCE.get(ResourceUtil.subPath(Names.POS_TEX_NORMAL, ".vert"));
BLOCK = FileResolution.get(ResourceUtil.subPath(Names.BLOCK, ".vert"))
.validateWith(check);
POS_TEX_NORMAL = FileResolution.get(ResourceUtil.subPath(Names.POS_TEX_NORMAL, ".vert"))
.validateWith(check);
} }
public static class Names { public static class Names {

View file

@ -6,6 +6,7 @@ import com.jozufozu.flywheel.light.LightUpdater;
import com.jozufozu.flywheel.util.WorldAttached; import com.jozufozu.flywheel.util.WorldAttached;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.RenderGameOverlayEvent; import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.event.TickEvent; import net.minecraftforge.event.TickEvent;
@ -32,8 +33,10 @@ public class ForgeEvents {
@SubscribeEvent @SubscribeEvent
public static void tickLight(TickEvent.ClientTickEvent e) { public static void tickLight(TickEvent.ClientTickEvent e) {
if (e.phase == TickEvent.Phase.END && Backend.isGameActive()) if (e.phase == TickEvent.Phase.END && Backend.isGameActive()) {
LightUpdater.get(Minecraft.getInstance().level).tick(); LightUpdater.get(Minecraft.getInstance().level)
.tick();
}
} }
} }

View file

@ -1,20 +0,0 @@
package com.jozufozu.flywheel.event;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.event.IModBusEvent;
public class GatherContextEvent extends Event implements IModBusEvent {
private final boolean firstLoad;
public GatherContextEvent(boolean firstLoad) {
this.firstLoad = firstLoad;
}
/**
* @return true iff it is the first time the event is fired.
*/
public boolean isFirstLoad() {
return firstLoad;
}
}