Fix* the errors

- *make the errors understandable without actually solving them
 - Straighten out shader compilation/error generation
 - Interpret errors in generated code
 - Compilation returns a result type instead of throwing an exception
 - Still need to reimplement source checks?
 - Make error messages prettier
This commit is contained in:
Jozufozu 2022-10-11 20:16:15 -07:00
parent 99e4105e94
commit 7a080a5538
23 changed files with 647 additions and 395 deletions

View file

@ -8,7 +8,6 @@ import com.jozufozu.flywheel.backend.RenderWork;
import com.jozufozu.flywheel.backend.ShadersModHandler;
import com.jozufozu.flywheel.backend.instancing.InstancedRenderDispatcher;
import com.jozufozu.flywheel.backend.instancing.batching.DrawBuffer;
import com.jozufozu.flywheel.backend.instancing.compile.FlwCompiler;
import com.jozufozu.flywheel.config.BackendTypeArgument;
import com.jozufozu.flywheel.config.FlwCommands;
import com.jozufozu.flywheel.config.FlwConfig;
@ -80,7 +79,6 @@ public class Flywheel {
forgeEventBus.addListener(FlwCommands::registerClientCommands);
forgeEventBus.addListener(EventPriority.HIGHEST, QuadConverter::onReloadRenderers);
forgeEventBus.addListener(FlwCompiler::onReloadRenderers);
forgeEventBus.addListener(Models::onReloadRenderers);
forgeEventBus.addListener(DrawBuffer::onReloadRenderers);

View file

@ -39,6 +39,10 @@ public class Loader implements ResourceManagerReloadListener {
var errorReporter = new ErrorReporter();
ShaderSources sources = new ShaderSources(errorReporter, manager);
if (FlwCompiler.INSTANCE != null) {
FlwCompiler.INSTANCE.delete();
}
FlwCompiler.INSTANCE = new FlwCompiler(sources);
ClientLevel level = Minecraft.getInstance().level;

View file

@ -1,38 +1,17 @@
package com.jozufozu.flywheel.backend.gl.shader;
import java.io.File;
import java.io.FileWriter;
import java.util.List;
import java.util.stream.Collectors;
import org.lwjgl.opengl.GL20;
import com.jozufozu.flywheel.backend.Backend;
import com.jozufozu.flywheel.backend.gl.GlObject;
import com.jozufozu.flywheel.backend.gl.versioned.GlCompat;
import com.jozufozu.flywheel.backend.instancing.compile.ShaderCompilationException;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation;
public class GlShader extends GlObject {
public final ShaderType type;
private final List<ResourceLocation> parts;
private final String name;
public GlShader(String source, ShaderType type, List<ResourceLocation> parts) throws ShaderCompilationException {
this.parts = parts;
public GlShader(int handle, ShaderType type, String name) {
this.type = type;
int handle = GL20.glCreateShader(type.glEnum);
GlCompat.safeShaderSource(handle, source);
GL20.glCompileShader(handle);
dumpSource(source, type);
if (GL20.glGetShaderi(handle, GL20.GL_COMPILE_STATUS) != GL20.GL_TRUE) {
throw new ShaderCompilationException("Could not compile " + getName(), handle);
}
this.name = name;
setHandle(handle);
}
@ -42,26 +21,9 @@ public class GlShader extends GlObject {
GL20.glDeleteShader(handle);
}
public String getName() {
return parts.stream()
.map(ResourceLocation::toString)
.map(s -> s.replaceAll("/", "_")
.replaceAll(":", "\\$"))
.collect(Collectors.joining(";"));
@Override
public String toString() {
return "GlShader{" + type.name + handle() + " " + name + "}";
}
private void dumpSource(String source, ShaderType type) {
if (!Backend.DUMP_SHADER_SOURCE) {
return;
}
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

@ -0,0 +1,149 @@
package com.jozufozu.flywheel.backend.instancing.compile;
import java.io.File;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.lwjgl.opengl.GL20;
import com.jozufozu.flywheel.backend.Backend;
import com.jozufozu.flywheel.backend.gl.GLSLVersion;
import com.jozufozu.flywheel.backend.gl.shader.GlShader;
import com.jozufozu.flywheel.backend.gl.shader.ShaderType;
import com.jozufozu.flywheel.backend.gl.versioned.GlCompat;
import com.jozufozu.flywheel.core.SourceComponent;
import com.jozufozu.flywheel.core.source.SourceFile;
import com.jozufozu.flywheel.util.StringUtil;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation;
/**
* Builder style class for compiling shaders.
* <p>
* Keeps track of the source files and components used to compile a shader,
* and interprets/pretty prints any errors that occur.
*/
public class Compilation {
private final List<SourceFile> files = new ArrayList<>();
private final List<ResourceLocation> componentNames = new ArrayList<>();
private final StringBuilder generatedSource;
private final StringBuilder fullSource;
private final GLSLVersion glslVersion;
private final ShaderType shaderType;
private int generatedLines = 0;
public Compilation(GLSLVersion glslVersion, ShaderType shaderType) {
this.generatedSource = new StringBuilder();
this.fullSource = new StringBuilder(CompileUtil.generateHeader(glslVersion, shaderType));
this.glslVersion = glslVersion;
this.shaderType = shaderType;
}
@NotNull
public CompilationResult compile() {
int handle = GL20.glCreateShader(shaderType.glEnum);
var source = fullSource.toString();
GlCompat.safeShaderSource(handle, source);
GL20.glCompileShader(handle);
var shaderName = buildShaderName();
dumpSource(source, shaderType.getFileName(shaderName));
if (compiledSuccessfully(handle)) {
return CompilationResult.success(new GlShader(handle, shaderType, shaderName));
}
var errorLog = GL20.glGetShaderInfoLog(handle);
GL20.glDeleteShader(handle);
return CompilationResult.failure(new FailedCompilation(shaderName, files, generatedSource.toString(), errorLog));
}
public void enableExtension(String ext) {
fullSource.append("#extension ")
.append(ext)
.append(" : enable\n");
}
public void addComponentName(ResourceLocation name) {
componentNames.add(name);
}
public void appendComponent(SourceComponent component) {
var source = component.source();
if (component instanceof SourceFile file) {
fullSource.append(sourceHeader(file));
} else {
fullSource.append(generatedHeader(source, component.name()
.toString()));
}
fullSource.append(source);
}
private String sourceHeader(SourceFile sourceFile) {
return "#line " + 0 + ' ' + getOrCreateFileID(sourceFile) + " // " + sourceFile.name + '\n';
}
private String generatedHeader(String generatedCode, String comment) {
generatedSource.append(generatedCode);
int lines = StringUtil.countLines(generatedCode);
// all generated code is put in file 0,
var out = "#line " + generatedLines + ' ' + 0;
generatedLines += lines;
return out + " // (generated) " + comment + '\n';
}
/**
* Returns an arbitrary file ID for use this compilation context, or generates one if missing.
*
* @param sourceFile The file to retrieve the ID for.
* @return A file ID unique to the given sourceFile.
*/
private int getOrCreateFileID(SourceFile sourceFile) {
int i = files.indexOf(sourceFile);
if (i != -1) {
return i + 1;
}
files.add(sourceFile);
return files.size();
}
@NotNull
private String buildShaderName() {
var components = componentNames.stream()
.map(ResourceLocation::toString)
.map(s -> s.replaceAll("/", "_")
.replaceAll(":", "\\$"))
.collect(Collectors.joining(";"));
return shaderType.name + glslVersion + ';' + components;
}
private static void dumpSource(String source, String fileName) {
if (!Backend.DUMP_SHADER_SOURCE) {
return;
}
File dir = new File(Minecraft.getInstance().gameDirectory, "flywheel_sources");
dir.mkdirs();
File file = new File(dir, fileName);
try (FileWriter writer = new FileWriter(file)) {
writer.write(source);
} catch (Exception e) {
Backend.LOGGER.error("Could not dump source.", e);
}
}
private static boolean compiledSuccessfully(int handle) {
return GL20.glGetShaderi(handle, GL20.GL_COMPILE_STATUS) == GL20.GL_TRUE;
}
}

View file

@ -0,0 +1,29 @@
package com.jozufozu.flywheel.backend.instancing.compile;
import org.jetbrains.annotations.Nullable;
import com.jozufozu.flywheel.backend.gl.shader.GlShader;
public sealed interface CompilationResult {
@Nullable
default GlShader unwrap() {
if (this instanceof Success s) {
return s.shader();
}
return null;
}
record Success(GlShader shader) implements CompilationResult {
}
record Failure(FailedCompilation failure) implements CompilationResult {
}
static CompilationResult success(GlShader program) {
return new Success(program);
}
static CompilationResult failure(FailedCompilation failure) {
return new Failure(failure);
}
}

View file

@ -0,0 +1,81 @@
package com.jozufozu.flywheel.backend.instancing.compile;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jetbrains.annotations.NotNull;
import com.jozufozu.flywheel.core.source.SourceFile;
import com.jozufozu.flywheel.core.source.SourceLines;
import com.jozufozu.flywheel.core.source.error.ErrorBuilder;
import com.jozufozu.flywheel.core.source.span.Span;
import com.jozufozu.flywheel.util.ConsoleColors;
public class FailedCompilation {
private static final Pattern ERROR_LINE = Pattern.compile("(\\d+)\\((\\d+)\\) : (.*)");
private final List<SourceFile> files;
private final SourceLines generatedSource;
private final String errorLog;
private final String shaderName;
public FailedCompilation(String shaderName, List<SourceFile> files, String generatedSource, String errorLog) {
this.shaderName = shaderName;
this.files = files;
this.generatedSource = new SourceLines(generatedSource);
this.errorLog = errorLog;
}
public String getMessage() {
return ConsoleColors.RED_BOLD_BRIGHT + "Failed to compile " + shaderName + ":\n" + errorString();
}
public String errorString() {
return errorStream().map(ErrorBuilder::build)
.collect(Collectors.joining("\n"));
}
@NotNull
private Stream<ErrorBuilder> errorStream() {
return errorLog.lines()
.map(this::interpretErrorLine);
}
private ErrorBuilder interpretErrorLine(String s) {
Matcher matcher = ERROR_LINE.matcher(s);
if (matcher.find()) {
int fileId = Integer.parseInt(matcher.group(1));
int lineNo = Integer.parseInt(matcher.group(2));
var msg = matcher.group(3);
if (fileId == 0) {
return interpretGeneratedError(lineNo, msg);
} else {
return interpretSourceError(fileId, lineNo, msg);
}
}
return ErrorBuilder.create()
.error(s);
}
private ErrorBuilder interpretSourceError(int fileId, int lineNo, String msg) {
var sourceFile = files.get(fileId - 1);
Span span = sourceFile.getLineSpanNoWhitespace(lineNo);
return ErrorBuilder.create()
.error(msg)
.pointAtFile(sourceFile)
.pointAt(span, 1);
}
private ErrorBuilder interpretGeneratedError(int lineNo, String msg) {
return ErrorBuilder.create()
.error(msg)
.pointAtFile("[in generated source]")
.pointAtLine(generatedSource, lineNo, 1)
.note("This generally indicates a bug in Flywheel, not your shader code.");
}
}

View file

@ -2,12 +2,10 @@ package com.jozufozu.flywheel.backend.instancing.compile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.NotNull;
import java.util.stream.Collectors;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
@ -20,7 +18,6 @@ import com.jozufozu.flywheel.api.vertex.VertexType;
import com.jozufozu.flywheel.backend.Backend;
import com.jozufozu.flywheel.backend.gl.GLSLVersion;
import com.jozufozu.flywheel.backend.gl.shader.GlProgram;
import com.jozufozu.flywheel.backend.gl.shader.GlShader;
import com.jozufozu.flywheel.backend.gl.shader.ShaderType;
import com.jozufozu.flywheel.backend.instancing.indirect.IndirectComponent;
import com.jozufozu.flywheel.core.BackendTypes;
@ -29,85 +26,73 @@ import com.jozufozu.flywheel.core.Components;
import com.jozufozu.flywheel.core.Pipelines;
import com.jozufozu.flywheel.core.SourceComponent;
import com.jozufozu.flywheel.core.pipeline.SimplePipeline;
import com.jozufozu.flywheel.core.source.CompilationContext;
import com.jozufozu.flywheel.core.source.ShaderLoadingException;
import com.jozufozu.flywheel.core.source.ShaderSources;
import com.jozufozu.flywheel.core.source.SourceFile;
import com.jozufozu.flywheel.event.ReloadRenderersEvent;
import com.jozufozu.flywheel.util.StringUtil;
import net.minecraft.resources.ResourceLocation;
public class FlwCompiler {
public static FlwCompiler INSTANCE;
public static void onReloadRenderers(ReloadRenderersEvent t) {
}
final Map<PipelineContext, GlProgram> pipelinePrograms = new HashMap<>();
final Map<StructType<?>, GlProgram> cullingPrograms = new HashMap<>();
boolean needsCrash = false;
final long compileStart = System.nanoTime();
final Multimap<Set<UniformProvider>, PipelineContext> uniformProviderGroups = ArrayListMultimap.create();
final List<PipelineContext> pipelineContexts = new ArrayList<>();
private final ShaderSources sources;
private final VertexMaterialComponent vertexMaterialComponent;
private final FragmentMaterialComponent fragmentMaterialComponent;
private final List<PipelineContext> pipelineContexts;
final ShaderCompiler shaderCompiler;
final Multimap<Set<UniformProvider>, PipelineContext> uniformProviderGroups = ArrayListMultimap.create();
final Map<PipelineContext, GlProgram> pipelinePrograms = new HashMap<>();
final Map<StructType<?>, GlProgram> cullingPrograms = new HashMap<>();
final List<FailedCompilation> errors = new ArrayList<>();
public FlwCompiler(ShaderSources sources) {
this.shaderCompiler = new ShaderCompiler(errors::add);
this.sources = sources;
this.vertexMaterialComponent = new VertexMaterialComponent(sources, ComponentRegistry.materials.vertexSources());
this.fragmentMaterialComponent = new FragmentMaterialComponent(sources, ComponentRegistry.materials.fragmentSources());
for (SimplePipeline pipelineShader : BackendTypes.availablePipelineShaders()) {
for (StructType<?> structType : ComponentRegistry.structTypes) {
for (VertexType vertexType : ComponentRegistry.vertexTypes) {
// TODO: context ubershaders, or not?
pipelineContexts.add(new PipelineContext(vertexType, structType, Components.WORLD, pipelineShader));
}
}
}
this.pipelineContexts = buildPipelineSet();
// TODO: analyze uniform providers and group them into sets; break up this ctor
for (PipelineContext context : pipelineContexts) {
try {
var glProgram = compilePipelineContext(context);
pipelinePrograms.put(context, glProgram);
} catch (ShaderCompilationException e) {
needsCrash = true;
Backend.LOGGER.error(e.errors);
}
compilePipelineContext(context);
}
for (StructType<?> type : ComponentRegistry.structTypes) {
try {
var glProgram = compileComputeCuller(type);
cullingPrograms.put(type, glProgram);
} catch (ShaderCompilationException e) {
needsCrash = true;
Backend.LOGGER.error(e.errors);
}
compileComputeCuller(type);
}
finish();
}
public void finish() {
private void finish() {
long compileEnd = System.nanoTime();
int programCount = pipelineContexts.size() + ComponentRegistry.structTypes.size();
int shaderCount = shaderCompiler.shaderCount();
int errorCount = errors.size();
var elapsed = StringUtil.formatTime(compileEnd - compileStart);
Backend.LOGGER.info("Compiled " + pipelineContexts.size() + " programs in " + StringUtil.formatTime(compileEnd - compileStart));
Backend.LOGGER.info("Compiled " + programCount + " programs and " + shaderCount + " shaders in " + elapsed + " with " + errorCount + " errors.");
if (needsCrash) {
throw new ShaderLoadingException("Compilation failed");
if (errorCount > 0) {
var details = errors.stream()
.map(FailedCompilation::getMessage)
.collect(Collectors.joining("\n"));
throw new ShaderLoadingException("Compilation failed.\n" + details);
}
}
public void delete() {
pipelinePrograms.values()
.forEach(GlProgram::delete);
cullingPrograms.values()
.forEach(GlProgram::delete);
shaderCompiler.delete();
}
public GlProgram getPipelineProgram(VertexType vertexType, StructType<?> structType, ContextShader contextShader, SimplePipeline pipelineShader) {
return pipelinePrograms.get(new PipelineContext(vertexType, structType, contextShader, pipelineShader));
}
@ -116,28 +101,36 @@ public class FlwCompiler {
return cullingPrograms.get(structType);
}
protected GlProgram compilePipelineContext(PipelineContext ctx) throws ShaderCompilationException {
private void compilePipelineContext(PipelineContext ctx) {
var glslVersion = ctx.pipelineShader()
.glslVersion();
var vertex = compileShader(glslVersion, ShaderType.VERTEX, getVertexComponents(ctx));
var fragment = compileShader(glslVersion, ShaderType.FRAGMENT, getFragmentComponents(ctx));
var vertex = shaderCompiler.compile(glslVersion, ShaderType.VERTEX, getVertexComponents(ctx));
var fragment = shaderCompiler.compile(glslVersion, ShaderType.FRAGMENT, getFragmentComponents(ctx));
return ctx.contextShader()
if (vertex == null || fragment == null) {
return;
}
pipelinePrograms.put(ctx, ctx.contextShader()
.factory()
.create(new ProgramAssembler().attachShader(vertex)
.attachShader(fragment)
.link());
.link()));
}
protected GlProgram compileComputeCuller(StructType<?> structType) {
private void compileComputeCuller(StructType<?> structType) {
var result = shaderCompiler.compile(GLSLVersion.V460, ShaderType.COMPUTE, getComputeComponents(structType));
return new GlProgram(new ProgramAssembler().attachShader(compileShader(GLSLVersion.V460, ShaderType.COMPUTE, getComputeComponents(structType)))
.link());
if (result == null) {
return;
}
cullingPrograms.put(structType, new GlProgram(new ProgramAssembler().attachShader(result)
.link()));
}
ImmutableList<SourceComponent> getVertexComponents(PipelineContext ctx) {
private ImmutableList<SourceComponent> getVertexComponents(PipelineContext ctx) {
var instanceAssembly = ctx.pipelineShader()
.assemble(new Pipeline.InstanceAssemblerContext(sources, ctx.vertexType(), ctx.structType()));
@ -157,7 +150,7 @@ public class FlwCompiler {
return ImmutableList.of(vertexMaterialComponent, instanceAssembly, layout, instance, context, pipeline);
}
ImmutableList<SourceComponent> getFragmentComponents(PipelineContext ctx) {
private ImmutableList<SourceComponent> getFragmentComponents(PipelineContext ctx) {
var context = sources.find(ctx.contextShader()
.fragmentShader()
.resourceLocation());
@ -167,7 +160,7 @@ public class FlwCompiler {
return ImmutableList.of(fragmentMaterialComponent, context, pipeline);
}
@NotNull ImmutableList<SourceComponent> getComputeComponents(StructType<?> structType) {
private ImmutableList<SourceComponent> getComputeComponents(StructType<?> structType) {
var instanceAssembly = new IndirectComponent(sources, structType);
var instance = sources.find(structType.getInstanceShader()
.resourceLocation());
@ -176,55 +169,16 @@ public class FlwCompiler {
return ImmutableList.of(instanceAssembly, instance, pipeline);
}
protected GlShader compileShader(GLSLVersion glslVersion, ShaderType shaderType, ImmutableList<SourceComponent> sourceComponents) {
StringBuilder finalSource = new StringBuilder(CompileUtil.generateHeader(glslVersion, shaderType));
finalSource.append("#extension GL_ARB_explicit_attrib_location : enable\n");
finalSource.append("#extension GL_ARB_conservative_depth : enable\n");
var ctx = new CompilationContext();
var names = ImmutableList.<ResourceLocation>builder();
for (var include : depthFirstInclude(sourceComponents)) {
appendFinalSource(finalSource, ctx, include);
private static List<PipelineContext> buildPipelineSet() {
ImmutableList.Builder<PipelineContext> builder = ImmutableList.builder();
for (SimplePipeline pipelineShader : BackendTypes.availablePipelineShaders()) {
for (StructType<?> structType : ComponentRegistry.structTypes) {
for (VertexType vertexType : ComponentRegistry.vertexTypes) {
// TODO: context ubershaders, or not?
builder.add(new PipelineContext(vertexType, structType, Components.WORLD, pipelineShader));
}
}
}
for (var component : sourceComponents) {
appendFinalSource(finalSource, ctx, component);
names.add(component.name());
}
try {
return new GlShader(finalSource.toString(), shaderType, names.build());
} catch (ShaderCompilationException e) {
throw e.withErrorLog(ctx);
}
}
private static void appendFinalSource(StringBuilder finalSource, CompilationContext ctx, SourceComponent component) {
var source = component.source();
if (component instanceof SourceFile file) {
finalSource.append(ctx.sourceHeader(file));
} else {
finalSource.append(ctx.generatedHeader(source, component.name()
.toString()));
}
finalSource.append(source);
}
protected static Set<SourceComponent> depthFirstInclude(ImmutableList<SourceComponent> root) {
var included = new LinkedHashSet<SourceComponent>(); // linked to preserve order
for (var component : root) {
recursiveDepthFirstInclude(included, component);
}
return included;
}
protected static void recursiveDepthFirstInclude(Set<SourceComponent> included, SourceComponent component) {
for (var include : component.included()) {
recursiveDepthFirstInclude(included, include);
}
included.addAll(component.included());
return builder.build();
}
}

View file

@ -1,32 +0,0 @@
package com.jozufozu.flywheel.backend.instancing.compile;
import org.lwjgl.opengl.GL20;
import com.jozufozu.flywheel.core.source.CompilationContext;
import com.jozufozu.flywheel.core.source.ShaderLoadingException;
public class ShaderCompilationException extends ShaderLoadingException {
private final int shaderHandle;
public String errors = "";
public ShaderCompilationException(String message, int shaderHandle) {
super(message);
this.shaderHandle = shaderHandle;
}
public ShaderLoadingException withErrorLog(CompilationContext ctx) {
if (this.shaderHandle == -1)
return this;
this.errors = ctx.parseErrors(GL20.glGetShaderInfoLog(this.shaderHandle));
return this;
}
@Override
public String getMessage() {
return super.getMessage() + '\n' + this.errors;
}
}

View file

@ -0,0 +1,99 @@
package com.jozufozu.flywheel.backend.instancing.compile;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.google.common.collect.ImmutableList;
import com.jozufozu.flywheel.backend.gl.GLSLVersion;
import com.jozufozu.flywheel.backend.gl.shader.GlShader;
import com.jozufozu.flywheel.backend.gl.shader.ShaderType;
import com.jozufozu.flywheel.core.SourceComponent;
public class ShaderCompiler {
private final Map<ShaderKey, CompilationResult> shaderCache = new HashMap<>();
private final Consumer<FailedCompilation> errorConsumer;
public ShaderCompiler(Consumer<FailedCompilation> errorConsumer) {
this.errorConsumer = errorConsumer;
}
public int shaderCount() {
return shaderCache.size();
}
@Nullable
public GlShader compile(GLSLVersion glslVersion, ShaderType shaderType, ImmutableList<SourceComponent> sourceComponents) {
var key = new ShaderKey(glslVersion, shaderType, sourceComponents);
var cached = shaderCache.get(key);
if (cached != null) {
return cached.unwrap();
}
CompilationResult out = compileUncached(glslVersion, shaderType, sourceComponents);
shaderCache.put(key, out);
return unwrapAndReportError(out);
}
public void delete() {
shaderCache.values()
.stream()
.map(CompilationResult::unwrap)
.filter(Objects::nonNull)
.forEach(GlShader::delete);
}
@Nullable
private GlShader unwrapAndReportError(CompilationResult result) {
if (result instanceof CompilationResult.Success s) {
return s.shader();
} else if (result instanceof CompilationResult.Failure f) {
errorConsumer.accept(f.failure());
}
return null;
}
@NotNull
private static CompilationResult compileUncached(GLSLVersion glslVersion, ShaderType shaderType, ImmutableList<SourceComponent> sourceComponents) {
var ctx = new Compilation(glslVersion, shaderType);
ctx.enableExtension("GL_ARB_explicit_attrib_location");
ctx.enableExtension("GL_ARB_conservative_depth");
for (var include : depthFirstInclude(sourceComponents)) {
ctx.appendComponent(include);
}
for (var component : sourceComponents) {
ctx.appendComponent(component);
ctx.addComponentName(component.name());
}
return ctx.compile();
}
private static Set<SourceComponent> depthFirstInclude(ImmutableList<SourceComponent> root) {
var included = new LinkedHashSet<SourceComponent>(); // linked to preserve order
for (var component : root) {
recursiveDepthFirstInclude(included, component);
}
return included;
}
private static void recursiveDepthFirstInclude(Set<SourceComponent> included, SourceComponent component) {
for (var include : component.included()) {
recursiveDepthFirstInclude(included, include);
}
included.addAll(component.included());
}
private record ShaderKey(GLSLVersion glslVersion, ShaderType shaderType,
ImmutableList<SourceComponent> sourceComponents) {
}
}

View file

@ -1,95 +0,0 @@
package com.jozufozu.flywheel.core.source;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import com.jozufozu.flywheel.core.source.error.ErrorBuilder;
import com.jozufozu.flywheel.core.source.span.Span;
public class CompilationContext {
public final List<SourceFile> files = new ArrayList<>();
private String generatedSource = "";
private int generatedLines = 0;
public String sourceHeader(SourceFile sourceFile) {
return "#line " + 0 + ' ' + getOrCreateFileID(sourceFile) + " // " + sourceFile.name + '\n';
}
public String generatedHeader(String generatedCode, @Nullable String comment) {
generatedSource += generatedCode;
int lines = generatedCode.split("\n").length;
var out = "#line " + generatedLines + ' ' + 0;
generatedLines += lines;
if (comment == null) {
comment = "";
}
return out + " // (generated) " + comment + '\n';
}
public boolean contains(SourceFile sourceFile) {
return files.contains(sourceFile);
}
/**
* Returns an arbitrary file ID for use this compilation context, or generates one if missing.
*
* @param sourceFile The file to retrieve the ID for.
* @return A file ID unique to the given sourceFile.
*/
private int getOrCreateFileID(SourceFile sourceFile) {
int i = files.indexOf(sourceFile);
if (i != -1) {
return i + 1;
}
files.add(sourceFile);
return files.size();
}
public Span getLineSpan(int fileId, int lineNo) {
if (fileId == 0) {
// TODO: Valid spans for generated code.
return null;
}
return getFile(fileId).getLineSpanNoWhitespace(lineNo);
}
private SourceFile getFile(int fileId) {
return files.get(fileId - 1);
}
public String parseErrors(String log) {
List<String> lines = log.lines()
.toList();
StringBuilder errors = new StringBuilder();
for (String line : lines) {
ErrorBuilder builder = parseCompilerError(line);
if (builder != null) {
errors.append(builder.build());
} else {
errors.append(line).append('\n');
}
}
return errors.toString();
}
@Nullable
private ErrorBuilder parseCompilerError(String line) {
try {
return ErrorBuilder.fromLogLine(this, line);
} catch (Exception ignored) {
}
return null;
}
}

View file

@ -98,15 +98,15 @@ public class SourceFile implements SourceComponent {
}
public Span getLineSpan(int lineNo) {
int begin = source.getLineStart(lineNo);
int end = begin + source.getLine(lineNo)
int begin = source.lineStartIndex(lineNo);
int end = begin + source.lineString(lineNo)
.length();
return new StringSpan(this, source.getCharPos(begin), source.getCharPos(end));
}
public Span getLineSpanNoWhitespace(int line) {
int begin = source.getLineStart(line);
int end = begin + source.getLine(line)
int begin = source.lineStartIndex(line);
int end = begin + source.lineString(line)
.length();
while (begin < end && Character.isWhitespace(source.charAt(begin))) {

View file

@ -11,6 +11,7 @@ import com.jozufozu.flywheel.util.StringUtil;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntLists;
public class SourceLines implements CharSequence {
@ -33,15 +34,15 @@ public class SourceLines implements CharSequence {
this.lines = getLines(raw, lineStarts);
}
public int getLineCount() {
public int count() {
return lines.size();
}
public String getLine(int lineNo) {
public String lineString(int lineNo) {
return lines.get(lineNo);
}
public int getLineStart(int lineNo) {
public int lineStartIndex(int lineNo) {
return lineStarts.getInt(lineNo);
}
@ -76,9 +77,12 @@ public class SourceLines implements CharSequence {
/**
* Scan the source for line breaks, recording the position of the first character of each line.
* @param source
*/
private static IntList createLineLookup(String source) {
if (source.isEmpty()) {
return IntLists.emptyList();
}
IntList l = new IntArrayList();
l.add(0); // first line is always at position 0
@ -87,6 +91,7 @@ public class SourceLines implements CharSequence {
while (matcher.find()) {
l.add(matcher.end());
}
return l;
}
@ -121,4 +126,28 @@ public class SourceLines implements CharSequence {
public int length() {
return raw.length();
}
public int lineStartCol(int spanLine) {
return 0;
}
public int lineWidth(int spanLine) {
return lines.get(spanLine)
.length();
}
public int lineStartColTrimmed(final int line) {
final var lineString = lineString(line);
final int end = lineString.length();
int col = 0;
while (col < end && Character.isWhitespace(charAt(col))) {
col++;
}
return col;
}
public int lineStartPosTrimmed(final int line) {
return lineStartIndex(line) + lineStartColTrimmed(line);
}
}

View file

@ -2,12 +2,9 @@ package com.jozufozu.flywheel.core.source.error;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.Nullable;
import com.jozufozu.flywheel.core.source.CompilationContext;
import com.jozufozu.flywheel.core.source.SourceFile;
import com.jozufozu.flywheel.core.source.SourceLines;
import com.jozufozu.flywheel.core.source.error.lines.ErrorLine;
@ -17,72 +14,61 @@ import com.jozufozu.flywheel.core.source.error.lines.SourceLine;
import com.jozufozu.flywheel.core.source.error.lines.SpanHighlightLine;
import com.jozufozu.flywheel.core.source.error.lines.TextLine;
import com.jozufozu.flywheel.core.source.span.Span;
import com.jozufozu.flywheel.util.FlwUtil;
import com.jozufozu.flywheel.util.ConsoleColors;
import com.jozufozu.flywheel.util.StringUtil;
public class ErrorBuilder {
private static final Pattern ERROR_LINE = Pattern.compile("(\\d+)\\((\\d+)\\) : (.*)");
private final List<ErrorLine> lines = new ArrayList<>();
public ErrorBuilder() {
private ErrorBuilder() {
}
public static ErrorBuilder error(CharSequence msg) {
return new ErrorBuilder()
.header(ErrorLevel.ERROR, msg);
public static ErrorBuilder create() {
return new ErrorBuilder();
}
public static ErrorBuilder compError(CharSequence msg) {
return new ErrorBuilder()
.extra(msg);
public ErrorBuilder error(String msg) {
return header(ErrorLevel.ERROR, msg);
}
public static ErrorBuilder warn(CharSequence msg) {
return new ErrorBuilder()
.header(ErrorLevel.WARN, msg);
public ErrorBuilder warn(String msg) {
return header(ErrorLevel.WARN, msg);
}
@Nullable
public static ErrorBuilder fromLogLine(CompilationContext env, String s) {
Matcher matcher = ERROR_LINE.matcher(s);
if (matcher.find()) {
String fileId = matcher.group(1);
String lineNo = matcher.group(2);
String msg = matcher.group(3);
Span span = env.getLineSpan(Integer.parseInt(fileId), Integer.parseInt(lineNo));
if (span == null) {
return ErrorBuilder.compError("Error in generated code");
}
return ErrorBuilder.compError(msg)
.pointAtFile(span.getSourceFile())
.pointAt(span, 1);
} else {
return null;
}
public ErrorBuilder hint(String msg) {
return header(ErrorLevel.HINT, msg);
}
public ErrorBuilder header(ErrorLevel level, CharSequence msg) {
public ErrorBuilder note(String msg) {
return header(ErrorLevel.NOTE, msg);
}
public ErrorBuilder header(ErrorLevel level, String msg) {
lines.add(new HeaderLine(level.toString(), msg));
return this;
}
public ErrorBuilder extra(CharSequence msg) {
lines.add(new TextLine(msg.toString()));
public ErrorBuilder extra(String msg) {
lines.add(new TextLine(msg));
return this;
}
public ErrorBuilder pointAtFile(SourceFile file) {
lines.add(new FileLine(file.name.toString()));
return pointAtFile(file.name.toString());
}
public ErrorBuilder pointAtFile(String file) {
lines.add(new FileLine(file));
return this;
}
public ErrorBuilder hintIncludeFor(@Nullable Span span, CharSequence msg) {
if (span == null) return this;
public ErrorBuilder hintIncludeFor(@Nullable Span span, String msg) {
if (span == null) {
return this;
}
SourceFile sourceFile = span.getSourceFile();
String builder = "add " + sourceFile.importStatement() + ' ' + msg + "\n defined here:";
@ -98,28 +84,37 @@ public class ErrorBuilder {
}
public ErrorBuilder pointAt(Span span, int ctxLines) {
if (span.lines() == 1) {
SourceLines lines = span.getSourceFile().source;
int spanLine = span.firstLine();
int firstCol = span.getStart()
.col();
int lastCol = span.getEnd()
.col();
int firstLine = Math.max(0, spanLine - ctxLines);
int lastLine = Math.min(lines.getLineCount(), spanLine + ctxLines);
pointAtLine(lines, spanLine, ctxLines, firstCol, lastCol);
}
int firstCol = span.getStart()
.col();
int lastCol = span.getEnd()
.col();
return this;
}
for (int i = firstLine; i <= lastLine; i++) {
CharSequence line = lines.getLine(i);
public ErrorBuilder pointAtLine(SourceLines lines, int spanLine, int ctxLines) {
return pointAtLine(lines, spanLine, ctxLines, lines.lineStartColTrimmed(spanLine), lines.lineWidth(spanLine));
}
this.lines.add(SourceLine.numbered(i + 1, line.toString()));
public ErrorBuilder pointAtLine(SourceLines lines, int spanLine, int ctxLines, int firstCol, int lastCol) {
int firstLine = Math.max(0, spanLine - ctxLines);
int lastLine = Math.min(lines.count(), spanLine + ctxLines);
if (i == spanLine) {
this.lines.add(new SpanHighlightLine(firstCol, lastCol));
}
for (int i = firstLine; i <= lastLine; i++) {
CharSequence line = lines.lineString(i);
this.lines.add(SourceLine.numbered(i + 1, line.toString()));
if (i == spanLine) {
this.lines.add(new SpanHighlightLine(firstCol, lastCol));
}
}
@ -127,23 +122,25 @@ public class ErrorBuilder {
}
public String build() {
int maxLength = -1;
int maxMargin = -1;
for (ErrorLine line : lines) {
int length = line.neededMargin();
int neededMargin = line.neededMargin();
if (length > maxLength) {
maxLength = length;
if (neededMargin > maxMargin) {
maxMargin = neededMargin;
}
}
StringBuilder builder = new StringBuilder();
builder.append('\n');
for (ErrorLine line : lines) {
int length = line.neededMargin();
int neededMargin = line.neededMargin();
builder.append(FlwUtil.repeatChar(' ', maxLength - length))
.append(line.build())
if (neededMargin >= 0) {
builder.append(StringUtil.repeatChar(' ', maxMargin - neededMargin));
}
builder.append(line.build())
.append(ConsoleColors.RESET)
.append('\n');
}

View file

@ -1,9 +1,12 @@
package com.jozufozu.flywheel.core.source.error;
import com.jozufozu.flywheel.util.ConsoleColors;
public enum ErrorLevel {
WARN("warn"),
ERROR("error"),
HINT("hint"),
WARN(ConsoleColors.YELLOW + "warn"),
ERROR(ConsoleColors.RED + "error"),
HINT(ConsoleColors.WHITE_BRIGHT + "hint"),
NOTE(ConsoleColors.WHITE_BRIGHT + "note"),
;
private final String error;

View file

@ -9,6 +9,7 @@ import com.jozufozu.flywheel.core.source.ShaderLoadingException;
import com.jozufozu.flywheel.core.source.SourceFile;
import com.jozufozu.flywheel.core.source.span.Span;
import com.jozufozu.flywheel.util.FlwUtil;
import com.jozufozu.flywheel.util.StringUtil;
public class ErrorReporter {
@ -60,18 +61,17 @@ public class ErrorReporter {
public ErrorBuilder generateSpanError(Span span, String message) {
SourceFile file = span.getSourceFile();
return error(message)
.pointAtFile(file)
return error(message).pointAtFile(file)
.pointAt(span, 2);
}
public ErrorBuilder generateFileError(SourceFile file, String message) {
return error(message)
.pointAtFile(file);
return error(message).pointAtFile(file);
}
public ErrorBuilder error(CharSequence msg) {
var out = ErrorBuilder.error(msg);
public ErrorBuilder error(String msg) {
var out = ErrorBuilder.create()
.error(msg);
reportedErrors.add(out);
return out;
}
@ -88,9 +88,7 @@ public class ErrorReporter {
return new ShaderLoadingException(allErrors);
}
public static void printLines(CharSequence source) {
String string = source.toString();
public static void printLines(String string) {
List<String> lines = string.lines()
.toList();
@ -103,7 +101,7 @@ public class ErrorReporter {
for (int i = 0; i < size; i++) {
builder.append(i)
.append(FlwUtil.repeatChar(' ', maxWidth - FlwUtil.numDigits(i)))
.append(StringUtil.repeatChar(' ', maxWidth - FlwUtil.numDigits(i)))
.append("| ")
.append(lines.get(i))
.append('\n');

View file

@ -2,7 +2,7 @@ package com.jozufozu.flywheel.core.source.error.lines;
public enum Divider {
BAR(" | "),
ARROW("-->"),
ARROW("-> "),
;
private final String s;

View file

@ -4,7 +4,7 @@ public record FileLine(String fileName) implements ErrorLine {
@Override
public String left() {
return "--";
return "-";
}
@Override

View file

@ -4,7 +4,7 @@ public record HeaderLine(String level, CharSequence message) implements ErrorLin
@Override
public int neededMargin() {
return 0;
return -1;
}
@Override

View file

@ -8,8 +8,6 @@ import java.util.stream.Collectors;
import com.jozufozu.flywheel.util.Pair;
public class GlslBuilder {
public static final String INDENT = " ";
private final List<GlslRootElement> elements = new ArrayList<>();
public void define(String name, String value) {
@ -39,16 +37,12 @@ public class GlslBuilder {
public String build() {
return elements.stream()
.map(GlslRootElement::minPrint)
.map(GlslRootElement::prettyPrint)
.collect(Collectors.joining("\n"));
}
public static String indent(int indentation) {
return INDENT.repeat(indentation);
}
public interface GlslRootElement {
String minPrint();
String prettyPrint();
}
public enum Separators implements GlslRootElement {
@ -62,14 +56,14 @@ public class GlslBuilder {
}
@Override
public String minPrint() {
public String prettyPrint() {
return separator;
}
}
public record Define(String name, String value) implements GlslRootElement {
@Override
public String minPrint() {
public String prettyPrint() {
return "#define " + name + " " + value;
}
}
@ -96,7 +90,7 @@ public class GlslBuilder {
}
@Override
public String minPrint() {
public String prettyPrint() {
return "layout(location = " + binding + ") in " + type + " " + name + ";";
}
}
@ -116,16 +110,16 @@ public class GlslBuilder {
private String buildFields() {
return fields.stream()
.map(p -> INDENT + p.first() + ' ' + p.second() + ';')
.map(p -> p.first() + ' ' + p.second() + ';')
.collect(Collectors.joining("\n"));
}
public String minPrint() {
public String prettyPrint() {
return """
struct %s {
%s
};
""".formatted(name, buildFields());
""".formatted(name, buildFields().indent(4));
}
}
@ -160,12 +154,13 @@ public class GlslBuilder {
return this;
}
public String minPrint() {
public String prettyPrint() {
return """
%s %s(%s) {
%s
}
""".formatted(returnType, name, buildArguments(), body.prettyPrint());
""".formatted(returnType, name, buildArguments(), body.prettyPrint()
.indent(4));
}
private String buildArguments() {

View file

@ -54,7 +54,7 @@ public interface GlslStmt extends LangItem {
.prettyPrint();
return """
case %s:
%s""".formatted(variant, block);
%s""".formatted(variant, block.indent(4));
}
}
}

View file

@ -0,0 +1,77 @@
package com.jozufozu.flywheel.util;
// https://stackoverflow.com/a/45444716
public class ConsoleColors {
// Reset
public static final String RESET = "\033[0m"; // Text Reset
// Regular Colors
public static final String BLACK = "\033[0;30m"; // BLACK
public static final String RED = "\033[0;31m"; // RED
public static final String GREEN = "\033[0;32m"; // GREEN
public static final String YELLOW = "\033[0;33m"; // YELLOW
public static final String BLUE = "\033[0;34m"; // BLUE
public static final String PURPLE = "\033[0;35m"; // PURPLE
public static final String CYAN = "\033[0;36m"; // CYAN
public static final String WHITE = "\033[0;37m"; // WHITE
// Bold
public static final String BLACK_BOLD = "\033[1;30m"; // BLACK
public static final String RED_BOLD = "\033[1;31m"; // RED
public static final String GREEN_BOLD = "\033[1;32m"; // GREEN
public static final String YELLOW_BOLD = "\033[1;33m"; // YELLOW
public static final String BLUE_BOLD = "\033[1;34m"; // BLUE
public static final String PURPLE_BOLD = "\033[1;35m"; // PURPLE
public static final String CYAN_BOLD = "\033[1;36m"; // CYAN
public static final String WHITE_BOLD = "\033[1;37m"; // WHITE
// Underline
public static final String BLACK_UNDERLINED = "\033[4;30m"; // BLACK
public static final String RED_UNDERLINED = "\033[4;31m"; // RED
public static final String GREEN_UNDERLINED = "\033[4;32m"; // GREEN
public static final String YELLOW_UNDERLINED = "\033[4;33m"; // YELLOW
public static final String BLUE_UNDERLINED = "\033[4;34m"; // BLUE
public static final String PURPLE_UNDERLINED = "\033[4;35m"; // PURPLE
public static final String CYAN_UNDERLINED = "\033[4;36m"; // CYAN
public static final String WHITE_UNDERLINED = "\033[4;37m"; // WHITE
// Background
public static final String BLACK_BACKGROUND = "\033[40m"; // BLACK
public static final String RED_BACKGROUND = "\033[41m"; // RED
public static final String GREEN_BACKGROUND = "\033[42m"; // GREEN
public static final String YELLOW_BACKGROUND = "\033[43m"; // YELLOW
public static final String BLUE_BACKGROUND = "\033[44m"; // BLUE
public static final String PURPLE_BACKGROUND = "\033[45m"; // PURPLE
public static final String CYAN_BACKGROUND = "\033[46m"; // CYAN
public static final String WHITE_BACKGROUND = "\033[47m"; // WHITE
// High Intensity
public static final String BLACK_BRIGHT = "\033[0;90m"; // BLACK
public static final String RED_BRIGHT = "\033[0;91m"; // RED
public static final String GREEN_BRIGHT = "\033[0;92m"; // GREEN
public static final String YELLOW_BRIGHT = "\033[0;93m"; // YELLOW
public static final String BLUE_BRIGHT = "\033[0;94m"; // BLUE
public static final String PURPLE_BRIGHT = "\033[0;95m"; // PURPLE
public static final String CYAN_BRIGHT = "\033[0;96m"; // CYAN
public static final String WHITE_BRIGHT = "\033[0;97m"; // WHITE
// Bold High Intensity
public static final String BLACK_BOLD_BRIGHT = "\033[1;90m"; // BLACK
public static final String RED_BOLD_BRIGHT = "\033[1;91m"; // RED
public static final String GREEN_BOLD_BRIGHT = "\033[1;92m"; // GREEN
public static final String YELLOW_BOLD_BRIGHT = "\033[1;93m";// YELLOW
public static final String BLUE_BOLD_BRIGHT = "\033[1;94m"; // BLUE
public static final String PURPLE_BOLD_BRIGHT = "\033[1;95m";// PURPLE
public static final String CYAN_BOLD_BRIGHT = "\033[1;96m"; // CYAN
public static final String WHITE_BOLD_BRIGHT = "\033[1;97m"; // WHITE
// High Intensity backgrounds
public static final String BLACK_BACKGROUND_BRIGHT = "\033[0;100m";// BLACK
public static final String RED_BACKGROUND_BRIGHT = "\033[0;101m";// RED
public static final String GREEN_BACKGROUND_BRIGHT = "\033[0;102m";// GREEN
public static final String YELLOW_BACKGROUND_BRIGHT = "\033[0;103m";// YELLOW
public static final String BLUE_BACKGROUND_BRIGHT = "\033[0;104m";// BLUE
public static final String PURPLE_BACKGROUND_BRIGHT = "\033[0;105m"; // PURPLE
public static final String CYAN_BACKGROUND_BRIGHT = "\033[0;106m"; // CYAN
public static final String WHITE_BACKGROUND_BRIGHT = "\033[0;107m"; // WHITE
}

View file

@ -1,6 +1,5 @@
package com.jozufozu.flywheel.util;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Stream;
@ -22,14 +21,6 @@ public class FlwUtil {
return ((BlockEntityRenderDispatcherAccessor) mc.getBlockEntityRenderDispatcher()).flywheel$getRenderers();
}
public static String repeatChar(char c, int n) {
char[] arr = new char[n];
Arrays.fill(arr, c);
return new String(arr);
}
public static PoseStack copyPoseStack(PoseStack stack) {
PoseStack copy = new PoseStack();
copy.last().pose().load(stack.last().pose());

View file

@ -23,6 +23,11 @@ public class StringUtil {
private static final NumberFormat THREE_DECIMAL_PLACES = new DecimalFormat("#0.000");
public static int countLines(String s) {
return (int) s.lines()
.count();
}
public static String formatBytes(long bytes) {
if (bytes < 1024) {
return bytes + " B";
@ -114,4 +119,12 @@ public class StringUtil {
}
return bytebuffer;
}
public static String repeatChar(char c, int n) {
char[] arr = new char[n];
Arrays.fill(arr, c);
return new String(arr);
}
}