Compare commits

...
8 Commits
Author SHA1 Message Date
Eclipse-Sol 159f7ff8cc Fix NaNs on EP (#2231)
* Fix NaNs on EP

* Also add hidden writes handling

* optimize this mess

* Another optimization pass
2026-10-07 17:59:21 -04:00
Ranzu 0d959c2b90 Add Darkmode Transformer target (#2221) 2026-10-07 13:55:18 -07:00
Alexdoru ce2462cad3 Fix server thread leak in ThreadedBlockData (#2230) 2026-10-07 12:30:09 -07:00
Death Fuel 5457df5369 Marginal improvements (#2222) 2026-10-07 12:23:32 -07:00
Eclipse-Sol acedba4eac Modern Font Sheets (#2225)
* Initial commit for modern-font-sheets

* Fix dev env error log

* Fix private use characters still using their own unifont character
2026-10-07 11:19:19 -04:00
Alexdoru 21a054a4db fix world client leak in CloudRenderer (#2227) 2026-10-06 06:22:08 -07:00
Jason Mitchell d056bf6fa7 Run image atomics natively on Metal where the runtime allows (#2226) 2026-10-05 22:57:30 -07:00
Eclipse-Sol 5ed1c92590 SDL Vertex Replay + Dragon API (#2224) 2026-10-05 20:23:36 -07:00
69 changed files with 4160 additions and 561 deletions

No files matched your search

+11
View File
@@ -148,6 +148,17 @@ fun Test.configureAngelicaJava8() {
}
}
if (isMacOs && providers.gradleProperty("clientJvmVendor").isPresent) {
for (javaVersion in listOf(17, 21, 25)) {
tasks.named<JavaExec>("runClient$javaVersion") {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(javaVersion)
vendor = JvmVendorSpec.matching(providers.gradleProperty("clientJvmVendor").get())
}
}
}
}
tasks.test {
useJUnitPlatform { excludeTags = setOf("gl-core") }
configureAngelicaJava8()
@@ -62,6 +62,7 @@ public final class SystemProperties {
public static final long FLYBY_TIME_OF_DAY = Math.floorMod(Long.getLong("angelica.flyby.timeOfDay", 6000L), 24000L);
public static final String FLYBY_COMMANDS = System.getProperty("angelica.flyby.commands", "");
public static final String FLYBY_ORIGIN = System.getProperty("angelica.flyby.origin", "");
public static final Integer FLYBY_DIMENSION = Integer.getInteger("angelica.flyby.dimension");
public static final FlybyPacing FLYBY_PACING = parseEnum("angelica.flyby.pacing", FlybyPacing.UNCAPPED, FlybyPacing.UNCAPPED, FlybyPacing.class);
public static final FlybyWeather FLYBY_WEATHER = parseEnum("angelica.flyby.weather", FlybyWeather.CLEAR, FlybyWeather.CLEAR, FlybyWeather.class);
public static final FlybyCamera FLYBY_CAMERA = parseEnum("angelica.flyby.camera", FlybyCamera.FIRST_PERSON, FlybyCamera.FIRST_PERSON, FlybyCamera.class);
@@ -4,6 +4,7 @@ import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
import com.gtnewhorizons.angelica.glsm.shader.ShaderIndex;
import com.gtnewhorizons.angelica.glsm.shader.SpirvShaderTranslator;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
@@ -456,12 +457,7 @@ public class CompatShaderTransformer {
continue;
}
final List<GLSLParser.Typeless_declarationContext> declarators = new ArrayList<>();
declarators.add(decl.typeless_declaration());
if (decl.getParent() instanceof GLSLParser.Init_declarator_listContext list) {
declarators.addAll(list.typeless_declaration());
}
for (GLSLParser.Typeless_declarationContext d : declarators) {
for (GLSLParser.Typeless_declarationContext d : ShaderIndex.declarators((GLSLParser.Init_declarator_listContext) decl.getParent())) {
if (d.IDENTIFIER() == null) continue;
final int slots = baseSlots(baseType) * typeArraySlots * arraySlots(d.array_specifier());
final String arraySuffix = typeArray + arraySuffix(d.array_specifier());
@@ -1,21 +1,25 @@
package com.gtnewhorizons.angelica.glsm;
import org.antlr.v4.runtime.BailErrorStrategy;
import org.antlr.v4.runtime.BufferedTokenStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonToken;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.DefaultErrorStrategy;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.WritableToken;
import org.antlr.v4.runtime.atn.PredictionMode;
import org.antlr.v4.runtime.misc.ParseCancellationException;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.taumc.glsl.grammar.GLSLLexer;
import org.taumc.glsl.grammar.GLSLParser;
import org.taumc.glsl.grammar.GLSLPreParser;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -131,72 +135,35 @@ public class GlslTransformUtils {
public record QuietParse(GLSLParser.Translation_unitContext full, GLSLPreParser.Translation_unitContext pre) {}
public static GLSLParser.Translation_unitContext parseFullQuiet(String source) {
final GLSLLexer lexer = new GLSLLexer(CharStreams.fromString(source));
lexer.removeErrorListeners();
final GLSLParser parser = new GLSLParser(new CommonTokenStream(lexer));
parser.removeErrorListeners();
parser.setBuildParseTree(true);
return parser.translation_unit();
}
public static GLSLPreParser.Translation_unitContext parsePreQuiet(String source) {
final GLSLLexer lexer = new GLSLLexer(CharStreams.fromString(source));
lexer.removeErrorListeners();
final GLSLPreParser preParser = new GLSLPreParser(new CommonTokenStream(lexer, GLSLLexer.DIRECTIVES));
preParser.removeErrorListeners();
preParser.setBuildParseTree(true);
return preParser.translation_unit();
return parseQuiet(new GLSLParser(new CommonTokenStream(quietLexer(source))), GLSLParser::translation_unit);
}
public static QuietParse parseBothQuiet(String source) {
final GLSLLexer lexer = new GLSLLexer(CharStreams.fromString(source));
lexer.removeErrorListeners();
final GLSLPreParser preParser = new GLSLPreParser(new BufferedTokenStream(lexer));
preParser.removeErrorListeners();
preParser.setBuildParseTree(true);
final GLSLPreParser.Translation_unitContext pre = preParser.translation_unit();
final GLSLLexer lexer = quietLexer(source);
final GLSLPreParser.Translation_unitContext pre = parseQuiet(new GLSLPreParser(new BufferedTokenStream(lexer)), GLSLPreParser::translation_unit);
lexer.reset();
final GLSLParser parser = new GLSLParser(new CommonTokenStream(lexer));
parser.removeErrorListeners();
parser.setBuildParseTree(true);
return new QuietParse(parser.translation_unit(), pre);
return new QuietParse(parseQuiet(new GLSLParser(new CommonTokenStream(lexer)), GLSLParser::translation_unit), pre);
}
public static Set<String> identifiersInDirectiveText(String source) {
private static <P extends Parser, T> T parseQuiet(P parser, Function<P, T> rule) {
parser.removeErrorListeners();
parser.setErrorHandler(new BailErrorStrategy());
parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
try {
return rule.apply(parser);
} catch (ParseCancellationException e) {
// Rewinds the token stream, which keeps every token SLL already pulled from the lexer.
parser.reset();
parser.setErrorHandler(new DefaultErrorStrategy());
parser.getInterpreter().setPredictionMode(PredictionMode.LL);
return rule.apply(parser);
}
}
public static GLSLLexer quietLexer(String source) {
final GLSLLexer lexer = new GLSLLexer(CharStreams.fromString(source));
lexer.removeErrorListeners();
final BufferedTokenStream tokens = new BufferedTokenStream(lexer);
tokens.fill();
final Set<String> names = new HashSet<>();
for (Token token : tokens.getTokens()) {
switch (token.getType()) {
case GLSLLexer.PROGRAM_TEXT, GLSLLexer.MACRO_TEXT, GLSLLexer.CONSTANT_EXPRESSION -> collectIdentifiers(token.getText(), names);
default -> { }
}
}
return names;
}
private static void collectIdentifiers(String text, Set<String> out) {
if (text == null) return;
final int len = text.length();
int i = 0;
while (i < len) {
final char c = text.charAt(i);
if (c != '_' && !Character.isLetter(c)) {
i++;
continue;
}
int end = i + 1;
while (end < len) {
final char n = text.charAt(end);
if (n != '_' && !Character.isLetterOrDigit(n)) break;
end++;
}
out.add(text.substring(i, end));
i = end;
}
return lexer;
}
public static String getFormattedShader(ParseTree tree, String header) {
@@ -276,6 +276,12 @@ public abstract class RenderBackend {
public void voxelizeRegion(long pass, boolean rebindVertexBuffer, float x, float y, float z, int rangeBase, int rangeCount, int vertexTotal) {}
public void endVoxelizationBatch(long pass) {}
public void setVertexWriteReplay(int graphicsProgram, VertexWriteReplaySetup setup) {}
public boolean supportsComputeImageAtomics() { return true; }
public boolean supportsComputeImageAtomics(int width, int height, int depth) { return supportsComputeImageAtomics(); }
public abstract void enable(int cap);
public abstract void enablei(int cap, int index);
public abstract void disable(int cap);
@@ -0,0 +1,23 @@
package com.gtnewhorizons.angelica.glsm.backend;
import java.util.function.IntSupplier;
/**
* How to replay a graphics program's vertex-stage image writes.
*
* @param inputLocations per input, the declared location or -1 for whatever the graphics program linked
* @param images per image the replay uses, in binding order: the written ones, then the read-only ones
* @param writtenImageCount how many of {@code images} the replay writes
* @param samplerNames samplers the graphics program may not have, because only the stripped writes read them
* @param samplerUnits per sampler name, the texture unit it reads at each draw, or -1 to use samplerTextures
* @param samplerTextures per sampler name, its texture when samplerUnits is -1
* @param samplerObjects per sampler name, the GL sampler object to sample it with when samplerUnits is -1, or 0
* @param vertexBufferBinding storage buffer binding of the first vertex buffer the replay program declares
* @param vertexBufferCount how many vertex buffers it declares, at consecutive bindings
* @param indexBufferBinding storage buffer binding of its index buffer
*/
public record VertexWriteReplaySetup(int replayProgram, String[] inputNames, int[] inputLocations,
IntSupplier[] images, int writtenImageCount,
String[] samplerNames, int[] samplerUnits, IntSupplier[] samplerTextures, int[] samplerObjects,
int vertexBufferBinding, int vertexBufferCount, int indexBufferBinding) {
}
@@ -4,17 +4,11 @@ import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.opengl.GL20;
import org.taumc.glsl.grammar.GLSLLexer;
import org.taumc.glsl.grammar.GLSLParser;
import org.taumc.glsl.grammar.GLSLPreParser;
import org.taumc.glsl.grammar.GLSLParserBaseListener;
import java.util.ArrayList;
import java.util.Comparator;
@@ -99,7 +93,7 @@ public final class GlslVulkanPreprocess {
}
final List<Edit> edits = new ArrayList<>();
final Metadata meta = collectEdits(source, root, glShaderType, debugName, separateReadOnlyImages, edits);
final Metadata meta = collectEdits(new ShaderIndex(root, source), glShaderType, debugName, separateReadOnlyImages, edits);
final Result out = new Result(applyEdits(source, edits), meta.boolUniforms(), meta.explicitVsInputs());
synchronized (CACHE) {
@@ -111,129 +105,100 @@ public final class GlslVulkanPreprocess {
public record Metadata(Set<String> boolUniforms, Set<String> explicitVsInputs, boolean needsSamplerless) {}
public static Metadata collectEdits(String source, GLSLParser.Translation_unitContext root, int glShaderType, String debugName, boolean separateReadOnlyImages, List<Edit> edits) {
public static Metadata collectEdits(ShaderIndex index, int glShaderType, String debugName, boolean separateReadOnlyImages, List<Edit> edits) {
final boolean isVertex = glShaderType == GL20.GL_VERTEX_SHADER;
final Set<String> bools = new HashSet<>();
final Set<String> explicitInputs = new HashSet<>();
final Set<String> readOnlyImages = new HashSet<>();
final boolean[] needsSamplerless = { false };
boolean needsSamplerless = false;
final int[] maxExplicitLoc = { -1 };
int maxExplicitLoc = -1;
final List<Integer> unlocatedVsInputStarts = new ArrayList<>();
ParseTreeWalker.DEFAULT.walk(new GLSLParserBaseListener() {
@Override
public void enterInit_declarator_list(GLSLParser.Init_declarator_listContext ctx) {
final GLSLParser.Single_declarationContext single = ctx.single_declaration();
if (single == null || single.fully_specified_type() == null) return;
for (GLSLParser.DeclarationContext declaration : index.globalDeclarations()) {
final GLSLParser.Init_declarator_listContext list = declaration.init_declarator_list();
if (list == null) continue;
final GLSLParser.Single_declarationContext single = list.single_declaration();
if (single == null || single.fully_specified_type() == null) continue;
final GLSLParser.Fully_specified_typeContext fst = single.fully_specified_type();
final GLSLParser.Type_qualifierContext tq = fst.type_qualifier();
if (tq == null) return;
final GLSLParser.Fully_specified_typeContext fst = single.fully_specified_type();
final GLSLParser.Type_qualifierContext tq = fst.type_qualifier();
if (tq == null) continue;
boolean hasUniform = false, hasIn = false, hasLocation = false, hasReadonly = false;
int locValue = -1;
int bindingValue = -1;
for (GLSLParser.Single_type_qualifierContext stq : tq.single_type_qualifier()) {
if (stq.storage_qualifier() != null) {
final String s = stq.storage_qualifier().getText();
if ("uniform".equals(s)) hasUniform = true;
else if ("in".equals(s)) hasIn = true;
else if ("readonly".equals(s)) hasReadonly = true;
} else if (stq.layout_qualifier() != null) {
for (GLSLParser.Layout_qualifier_idContext id : stq.layout_qualifier().layout_qualifier_id_list().layout_qualifier_id()) {
if (id.IDENTIFIER() == null) continue;
final String qualifier = id.IDENTIFIER().getText();
if ("location".equals(qualifier)) {
hasLocation = true;
if (id.constant_expression() != null) {
try { locValue = Integer.parseInt(id.constant_expression().getText()); } catch (NumberFormatException ignored) {}
}
} else if ("binding".equals(qualifier) && id.constant_expression() != null) {
try { bindingValue = Integer.parseInt(id.constant_expression().getText()); } catch (NumberFormatException ignored) {}
boolean hasUniform = false, hasIn = false, hasLocation = false, hasReadonly = false;
int locValue = -1;
int bindingValue = -1;
for (GLSLParser.Single_type_qualifierContext stq : tq.single_type_qualifier()) {
if (stq.storage_qualifier() != null) {
final String s = stq.storage_qualifier().getText();
if ("uniform".equals(s)) hasUniform = true;
else if ("in".equals(s)) hasIn = true;
else if ("readonly".equals(s)) hasReadonly = true;
} else if (stq.layout_qualifier() != null) {
for (GLSLParser.Layout_qualifier_idContext id : stq.layout_qualifier().layout_qualifier_id_list().layout_qualifier_id()) {
if (id.IDENTIFIER() == null) continue;
final String qualifier = id.IDENTIFIER().getText();
if ("location".equals(qualifier)) {
hasLocation = true;
if (id.constant_expression() != null) {
try { locValue = Integer.parseInt(id.constant_expression().getText()); } catch (NumberFormatException ignored) {}
}
} else if ("binding".equals(qualifier) && id.constant_expression() != null) {
try { bindingValue = Integer.parseInt(id.constant_expression().getText()); } catch (NumberFormatException ignored) {}
}
}
}
}
final boolean boolDecl = hasUniform && isBool(fst);
final boolean explicitInputDecl = isVertex && hasIn && hasLocation;
final boolean boolDecl = hasUniform && isBool(fst);
final boolean explicitInputDecl = isVertex && hasIn && hasLocation;
final List<GLSLParser.Typeless_declarationContext> declarators = ShaderIndex.declarators(list);
for (GLSLParser.Typeless_declarationContext td : declarators) {
handleDeclarator(td, hasUniform, boolDecl, explicitInputDecl, bools, explicitInputs, edits);
}
if (single.typeless_declaration() != null) {
handleDeclarator(single.typeless_declaration(), hasUniform, boolDecl, explicitInputDecl, bools, explicitInputs, edits);
}
for (GLSLParser.Typeless_declarationContext td : ctx.typeless_declaration()) {
handleDeclarator(td, hasUniform, boolDecl, explicitInputDecl, bools, explicitInputs, edits);
}
if (separateReadOnlyImages && hasUniform && fst.type_specifier() != null) {
final String typeText = fst.type_specifier().getText();
if (separateTextureType(typeText) != null) {
needsSamplerless[0] = true;
} else if (hasReadonly) {
final String asTexture = separateTextureTypeForImage(typeText);
if (asTexture != null) {
needsSamplerless[0] = true;
collectDeclaredNames(single, ctx, readOnlyImages);
final String binding = bindingValue >= 0 ? "layout(binding = " + bindingValue + ") " : "";
edits.add(new Edit(startIdx(fst), stopIdx(fst), binding + "uniform " + asTexture));
if (separateReadOnlyImages && hasUniform && fst.type_specifier() != null) {
final String typeText = fst.type_specifier().getText();
if (separateTextureType(typeText) != null) {
needsSamplerless = true;
} else if (hasReadonly) {
final String asTexture = separateTextureTypeForImage(typeText);
if (asTexture != null) {
needsSamplerless = true;
for (GLSLParser.Typeless_declarationContext td : declarators) {
if (td.IDENTIFIER() != null) readOnlyImages.add(td.IDENTIFIER().getText());
}
}
}
if (isVertex && hasIn) {
if (hasLocation && locValue >= 0) {
if (locValue > maxExplicitLoc[0]) maxExplicitLoc[0] = locValue;
} else if (!hasLocation) {
unlocatedVsInputStarts.add(ctx.getStart().getStartIndex());
final String binding = bindingValue >= 0 ? "layout(binding = " + bindingValue + ") " : "";
edits.add(new Edit(startIdx(fst), stopIdx(fst), binding + "uniform " + asTexture));
}
}
}
@Override
public void enterPostfix_expression(GLSLParser.Postfix_expressionContext ctx) {
if (readOnlyImages.isEmpty() || ctx.LEFT_PAREN() == null || ctx.RIGHT_PAREN() == null) return;
final GLSLParser.Function_call_parametersContext params = ctx.function_call_parameters();
if (params == null || params.assignment_expression() == null || params.assignment_expression().isEmpty()) return;
if (!readOnlyImages.contains(params.assignment_expression(0).getText())) return;
final ParseTree callee = ctx.getChild(0);
if (!(callee instanceof ParserRuleContext calleeCtx)) return;
final String replacement = switch (calleeCtx.getText()) {
case "imageLoad" -> "texelFetch";
case "imageSize" -> "textureSize";
default -> null;
};
if (replacement == null) return;
edits.add(new Edit(startIdx(calleeCtx), stopIdx(calleeCtx), replacement));
final int rparen = ctx.RIGHT_PAREN().getSymbol().getStartIndex();
edits.add(new Edit(rparen, rparen - 1, ", 0"));
}
@Override
public void visitTerminal(TerminalNode node) {
final Token tok = node.getSymbol();
if (tok.getType() != GLSLLexer.IDENTIFIER) return;
final String text = tok.getText();
switch (text) {
case "sampler" -> edits.add(new Edit(tok.getStartIndex(), tok.getStopIndex(), SAMPLER_RENAMED));
case "gl_VertexID" -> edits.add(new Edit(tok.getStartIndex(), tok.getStopIndex(), "gl_VertexIndex"));
case "gl_InstanceID" -> edits.add(new Edit(tok.getStartIndex(), tok.getStopIndex(), "gl_InstanceIndex"));
default -> {
if (TARGET_RESERVED.contains(text)) edits.add(new Edit(tok.getStartIndex(), tok.getStopIndex(), RESERVED_RENAMED_PREFIX + text));
}
if (isVertex && hasIn) {
if (hasLocation && locValue >= 0) {
if (locValue > maxExplicitLoc) maxExplicitLoc = locValue;
} else if (!hasLocation) {
unlocatedVsInputStarts.add(list.getStart().getStartIndex());
}
}
}, root);
if (needsSamplerless[0] && source != null) {
final int at = versionDirectiveEnd(source);
if (at >= 0) edits.add(new Edit(at, at - 1, "\n" + SAMPLERLESS_EXTENSION));
}
if (maxExplicitLoc[0] >= 0 && !unlocatedVsInputStarts.isEmpty()) {
int next = maxExplicitLoc[0] + 1;
if (!readOnlyImages.isEmpty()) {
rewriteReadOnlyImageCalls(index.callsTo("imageLoad"), "texelFetch", readOnlyImages, edits);
rewriteReadOnlyImageCalls(index.callsTo("imageSize"), "textureSize", readOnlyImages, edits);
}
if (needsSamplerless) {
final int versionEnd = index.directives().versionEnd();
if (versionEnd >= 0) edits.add(new Edit(versionEnd, versionEnd - 1, "\n" + SAMPLERLESS_EXTENSION));
}
rename(index, "sampler", SAMPLER_RENAMED, edits);
rename(index, "gl_VertexID", "gl_VertexIndex", edits);
rename(index, "gl_InstanceID", "gl_InstanceIndex", edits);
for (String reserved : TARGET_RESERVED) rename(index, reserved, RESERVED_RENAMED_PREFIX + reserved, edits);
if (maxExplicitLoc >= 0 && !unlocatedVsInputStarts.isEmpty()) {
int next = maxExplicitLoc + 1;
int dropped = 0;
for (int start : unlocatedVsInputStarts) {
if (next >= MAX_VS_INPUT_LOCATIONS) { dropped++; continue; }
@@ -245,22 +210,31 @@ public final class GlslVulkanPreprocess {
}
}
return new Metadata(bools, explicitInputs, needsSamplerless[0]);
return new Metadata(bools, explicitInputs, needsSamplerless);
}
private static void rename(ShaderIndex index, String name, String replacement, List<Edit> edits) {
for (Token tok : index.occurrences(name)) edits.add(new Edit(tok.getStartIndex(), tok.getStopIndex(), replacement));
}
private static void rewriteReadOnlyImageCalls(List<GLSLParser.Postfix_expressionContext> calls, String replacement, Set<String> readOnlyImages, List<Edit> edits) {
for (GLSLParser.Postfix_expressionContext call : calls) {
if (call.RIGHT_PAREN() == null) continue;
final GLSLParser.Function_call_parametersContext params = call.function_call_parameters();
if (params == null || params.assignment_expression().isEmpty()) continue;
if (!readOnlyImages.contains(params.assignment_expression(0).getText())) continue;
final ParserRuleContext callee = (ParserRuleContext) call.getChild(0);
edits.add(new Edit(startIdx(callee), stopIdx(callee), replacement));
final int rparen = call.RIGHT_PAREN().getSymbol().getStartIndex();
edits.add(new Edit(rparen, rparen - 1, ", 0"));
}
}
public static void clearCache() {
synchronized (CACHE) { CACHE.clear(); }
}
private static void collectDeclaredNames(GLSLParser.Single_declarationContext single, GLSLParser.Init_declarator_listContext ctx, Set<String> out) {
if (single.typeless_declaration() != null && single.typeless_declaration().IDENTIFIER() != null) {
out.add(single.typeless_declaration().IDENTIFIER().getText());
}
for (GLSLParser.Typeless_declarationContext td : ctx.typeless_declaration()) {
if (td.IDENTIFIER() != null) out.add(td.IDENTIFIER().getText());
}
}
private static @Nullable String separateTextureTypeForImage(String typeText) {
final String dim = switch (typeText) {
case "image1D", "iimage1D", "uimage1D" -> "1D";
@@ -281,26 +255,6 @@ public final class GlslVulkanPreprocess {
};
}
private static int versionDirectiveEnd(String source) {
final GLSLPreParser.Translation_unitContext pre;
try {
pre = GlslTransformUtils.parsePreQuiet(source);
} catch (Exception e) {
return -1;
}
final GLSLPreParser.Version_directiveContext ctx = firstVersionDirective(pre);
return ctx == null || ctx.getStop() == null ? -1 : ctx.getStop().getStopIndex() + 1;
}
private static GLSLPreParser.@Nullable Version_directiveContext firstVersionDirective(ParseTree node) {
if (node instanceof GLSLPreParser.Version_directiveContext v) return v;
for (int i = 0; i < node.getChildCount(); i++) {
final GLSLPreParser.Version_directiveContext found = firstVersionDirective(node.getChild(i));
if (found != null) return found;
}
return null;
}
private static void handleDeclarator(GLSLParser.Typeless_declarationContext td, boolean hasUniform, boolean asBool, boolean asInput, Set<String> bools, Set<String> inputs, List<Edit> edits) {
if (td.IDENTIFIER() == null) return;
final String name = td.IDENTIFIER().getText();
@@ -0,0 +1,153 @@
package com.gtnewhorizons.angelica.glsm.shader;
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.jetbrains.annotations.Nullable;
import org.taumc.glsl.grammar.GLSLLexer;
import org.taumc.glsl.grammar.GLSLParser;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* What the source passes look up in a parsed shader.
*/
public final class ShaderIndex {
public record Directives(Set<String> identifiers, boolean hasConditionalOrMacro, int versionEnd) {
private static final Directives NONE = new Directives(Set.of(), false, -1);
}
private final GLSLParser.Translation_unitContext root;
private final String source;
private final Map<String, List<Token>> identifiers = new HashMap<>();
private final Map<String, List<GLSLParser.Postfix_expressionContext>> calls = new HashMap<>();
private final List<GLSLParser.DeclarationContext> globalDeclarations = new ArrayList<>();
private @Nullable Directives directives;
public ShaderIndex(GLSLParser.Translation_unitContext root, String source) {
this.root = root;
this.source = source;
collect(root);
for (GLSLParser.External_declarationContext external : root.external_declaration()) {
if (external.declaration() != null) globalDeclarations.add(external.declaration());
}
}
private void collect(ParseTree node) {
if (node instanceof TerminalNode terminal) {
final Token token = terminal.getSymbol();
if (token.getType() == GLSLLexer.IDENTIFIER) identifiers.computeIfAbsent(token.getText(), k -> new ArrayList<>(2)).add(token);
return;
}
if (node instanceof GLSLParser.Postfix_expressionContext p && p.LEFT_PAREN() != null) {
calls.computeIfAbsent(calleeName(p), k -> new ArrayList<>(2)).add(p);
}
for (int i = 0, n = node.getChildCount(); i < n; i++) collect(node.getChild(i));
}
public GLSLParser.Translation_unitContext root() {
return root;
}
public String source() {
return source;
}
public List<Token> occurrences(String name) {
return identifiers.getOrDefault(name, List.of());
}
public List<GLSLParser.Postfix_expressionContext> callsTo(String name) {
return calls.getOrDefault(name, List.of());
}
public List<GLSLParser.DeclarationContext> globalDeclarations() {
return globalDeclarations;
}
/** Lexes the source on first use. */
public Directives directives() {
if (directives == null) directives = scanDirectives(source);
return directives;
}
private static Directives scanDirectives(String source) {
if (source.indexOf('#') < 0) return Directives.NONE;
final GLSLLexer lexer = GlslTransformUtils.quietLexer(source);
final Set<String> names = new HashSet<>();
boolean conditionalOrMacro = false;
int versionEnd = -1;
boolean inVersion = false;
for (Token token = lexer.nextToken(); token.getType() != Token.EOF; token = lexer.nextToken()) {
final int type = token.getType();
if (type == GLSLLexer.VERSION_DIRECTIVE && versionEnd < 0) {
inVersion = true;
versionEnd = token.getStopIndex() + 1;
continue;
}
if (inVersion && (type == GLSLLexer.NUMBER || type == GLSLLexer.PROFILE)) {
versionEnd = token.getStopIndex() + 1;
continue;
}
inVersion = false;
switch (type) {
case GLSLLexer.PROGRAM_TEXT, GLSLLexer.MACRO_TEXT, GLSLLexer.CONSTANT_EXPRESSION -> collectIdentifiers(token.getText(), names);
case GLSLLexer.IF_DIRECTIVE, GLSLLexer.IFDEF_DIRECTIVE, GLSLLexer.IFNDEF_DIRECTIVE, GLSLLexer.DEFINE_DIRECTIVE -> conditionalOrMacro = true;
default -> { }
}
}
return new Directives(names, conditionalOrMacro, versionEnd);
}
private static void collectIdentifiers(String text, Set<String> out) {
if (text == null) return;
final int len = text.length();
int i = 0;
while (i < len) {
final char c = text.charAt(i);
if (c != '_' && !Character.isLetter(c)) {
i++;
continue;
}
int end = i + 1;
while (end < len) {
final char n = text.charAt(end);
if (n != '_' && !Character.isLetterOrDigit(n)) break;
end++;
}
out.add(text.substring(i, end));
i = end;
}
}
public static String calleeName(GLSLParser.Postfix_expressionContext call) {
final ParseTree callee = call.getChild(0);
return callee instanceof ParserRuleContext ? callee.getText() : "";
}
public static List<GLSLParser.Typeless_declarationContext> declarators(GLSLParser.Init_declarator_listContext list) {
final GLSLParser.Single_declarationContext single = list.single_declaration();
final List<GLSLParser.Typeless_declarationContext> rest = list.typeless_declaration();
if (single == null || single.typeless_declaration() == null) return rest;
final List<GLSLParser.Typeless_declarationContext> all = new ArrayList<>(rest.size() + 1);
all.add(single.typeless_declaration());
all.addAll(rest);
return all;
}
public static boolean hasStorageQualifier(@Nullable GLSLParser.Type_qualifierContext qualifier, String keyword) {
if (qualifier == null) return false;
for (GLSLParser.Single_type_qualifierContext q : qualifier.single_type_qualifier()) {
if (q.storage_qualifier() != null && keyword.equals(q.storage_qualifier().getText())) return true;
}
return false;
}
}
@@ -47,14 +47,6 @@ public final class SpirvShaderTranslator {
private SpirvShaderTranslator() {}
public static @Nullable String glslToGlslEs(String source, int glShaderType, String debugName) {
return glslToGlslEsImpl(source, glShaderType, debugName, false);
}
static @Nullable String glslToGlslEsRawForTest(String source, int glShaderType, String debugName) {
return glslToGlslEsImpl(source, glShaderType, debugName, true);
}
private static @Nullable String glslToGlslEsImpl(String source, int glShaderType, String debugName, boolean rawOutput) {
final int shaderKind = shaderKindForGlType(glShaderType);
if (shaderKind < 0) {
LOGGER.warn("Unknown GL shader type 0x{} for '{}' - skipping SPIR-V translation", Integer.toHexString(glShaderType), debugName);
@@ -74,7 +66,7 @@ public final class SpirvShaderTranslator {
try {
final String emitted = emitGlslEs(cr.spirv(), debugName, glShaderType, pre.explicitVsInputs());
if (emitted == null) return null;
return rawOutput ? emitted : postProcessEsOutput(emitted, pre.boolUniforms());
return postProcessEsOutput(emitted, pre.boolUniforms());
} finally {
memFree(cr.spirv());
}
@@ -163,16 +155,7 @@ public final class SpirvShaderTranslator {
final GLSLParser.Single_declarationContext single = ctx.single_declaration();
if (single == null || single.typeless_declaration() == null) return;
final GLSLParser.Fully_specified_typeContext fst = single.fully_specified_type();
if (fst == null || fst.type_qualifier() == null) return;
boolean isUniform = false;
for (GLSLParser.Single_type_qualifierContext stq : fst.type_qualifier().single_type_qualifier()) {
if (stq.storage_qualifier() != null && "uniform".equals(stq.storage_qualifier().getText())) {
isUniform = true;
break;
}
}
if (!isUniform) return;
if (fst == null || !ShaderIndex.hasStorageQualifier(fst.type_qualifier(), "uniform")) return;
if (fst.type_specifier() == null || fst.type_specifier().type_specifier_nonarray() == null) return;
final GLSLParser.Type_specifier_nonarrayContext ts = fst.type_specifier().type_specifier_nonarray();
+2
View File
@@ -10,6 +10,8 @@
# length - Units: blocks (straight), blocks per leg (circuit), degrees (pan), ticks (static).
# extra - passed through to gradle; -Dangelica.* reaches the client (e.g. -Dangelica.flyby.timeOfDay=1500).
# -Dangelica.flyby.camera=FIRST_PERSON|THIRD_PERSON_BACK|THIRD_PERSON_FRONT (default FIRST_PERSON).
# -Dangelica.flyby.dimension=<id> moves the player there first (1 = End), after world saving is off.
# -PclientJvmVendor=adoptium (macOS) picks the JVM vendor for runClient17/21/25.
#
# FLYBY_SPEED - blocks per tick. (0.5 ~= creative flight).
# FLYBY_PACING - UNCAPPED | CONFIGURED. Passed through when set.
+4
View File
@@ -0,0 +1,4 @@
# End crystals next to the arrival platform. Use with -Dangelica.flyby.dimension=1, route pan.
summon EnderCrystal ~ ~2 ~12 {ForgeData:{flyby:1b}}
summon EnderCrystal ~8 ~2 ~8 {ForgeData:{flyby:1b}}
summon EnderCrystal ~-8 ~4 ~10 {ForgeData:{flyby:1b}}
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.sdlgpu;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.sdlgpu.compute.VertexWriteReplayer;
import com.gtnewhorizons.angelica.sdlgpu.compute.VoxelizationDispatcher;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.CaptureGate;
@@ -10,6 +11,7 @@ import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.backend.BackendOptions;
import com.gtnewhorizons.angelica.glsm.backend.GLDebugMessageListener;
import com.gtnewhorizons.angelica.glsm.backend.MainThreadPump;
import com.gtnewhorizons.angelica.glsm.backend.VertexWriteReplaySetup;
import me.eigenraven.lwjgl3ify.client.MainThreadExec;
import org.lwjglx.Lwjgl3ifyEventLoop;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
@@ -164,6 +166,7 @@ public class SDLGPURenderBackend extends RenderBackend {
@Override public long nextSeq() { return TransferThread.nextSeq(); }
});
private final PipelineApplier pipelineApplier = new PipelineApplier(frameManager, resourceManager, shaderManager, pipelineStore, fboClearTracker, persistentSync, samplerBinder, storageTextureBinder, storageBufferBinder);
private final VertexWriteReplayer vertexWriteReplayer = new VertexWriteReplayer(shaderManager, resourceManager, pipelineApplier, computeBinder, SDLGPURenderBackend::s);
private final AttachmentClear attachmentClear = new AttachmentClear(frameManager, pipelineStore, shaderManager);
private final DepthStencilReadback depthStencilReadback = new DepthStencilReadback(frameManager, resourceManager, pipelineStore, shaderManager, fboClearTracker);
private final DrawDispatch drawDispatch = new DrawDispatch(device, frameManager, resourceManager, pipelineApplier, this::enqueuePreCopied);
@@ -301,6 +304,8 @@ public class SDLGPURenderBackend extends RenderBackend {
}
});
pipelineApplier.setDeferredUploadSink(this::enqueuePreCopied);
frameManager.setRenderPassEndHook(vertexWriteReplayer::flush);
resourceManager.setTextureReplaceHook(this::flushVertexWritesBeforeTextureChange);
frameManager.setPreRenderPassHook(() -> {
final ContextState st = s();
pipelineApplier.flushUniformBlocks(st);
@@ -1207,9 +1212,11 @@ public class SDLGPURenderBackend extends RenderBackend {
if (st.rasterizerDiscard) return;
final FrameState f = frameManager.frame();
if (!f.frameActive) { f.droppedDrawsThisFrame++; return; }
prepareVertexWrites(st);
if (mode == GL11.GL_TRIANGLE_FAN && count >= 3) {
if (!drawDispatch.drawTriangleFanAsTriangleList(st, first, count)) { f.droppedDrawsThisFrame++; return; }
if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "arraysFan", mode, count, 0, first, 0);
captureVertexWrites(st, first, count, 1, 0, 0, 0);
return;
}
drawDispatch.setPrimitiveTypeForDraw(st, FormatMap.mapPrimitiveType(mode));
@@ -1218,6 +1225,7 @@ public class SDLGPURenderBackend extends RenderBackend {
if (!pipelineApplier.applyPipelineAndState(st, f)) { f.droppedDrawsThisFrame++; return; }
if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "arrays", mode, count, 0, first, 0);
SDL_DrawGPUPrimitives(f.renderPass, count, 1, first, 0);
captureVertexWrites(st, first, count, 1, 0, 0, 0);
}
@Override public void drawElements(int mode, int count, int type, long indices) {
@@ -1227,6 +1235,7 @@ public class SDLGPURenderBackend extends RenderBackend {
final int firstIndex = (int) (indices / FormatMap.indexElementSize(type));
if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "elements", mode, count, type, firstIndex, 0);
drawDispatch.issueIndexedDraw(st, rp, st.currentVao.elementBuffer, type, count, 1, firstIndex, 0);
captureVertexWrites(st, 0, count, 1, type, indices, 0);
}
@Override public void drawElementsInstanced(int mode, int count, int type, long indices, int primcount) {
@@ -1236,6 +1245,7 @@ public class SDLGPURenderBackend extends RenderBackend {
final int firstIndex = (int) (indices / FormatMap.indexElementSize(type));
if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "elementsInstanced", mode, count, type, firstIndex, 0);
drawDispatch.issueIndexedDraw(st, rp, st.currentVao.elementBuffer, type, count, primcount, firstIndex, 0);
captureVertexWrites(st, 0, count, primcount, type, indices, 0);
}
@@ -1246,10 +1256,12 @@ public class SDLGPURenderBackend extends RenderBackend {
final int firstIndex = (int) (indices / FormatMap.indexElementSize(type));
if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "elementsBaseVertex", mode, count, type, firstIndex, baseVertex);
drawDispatch.issueIndexedDraw(st, rp, st.currentVao.elementBuffer, type, count, 1, firstIndex, baseVertex);
captureVertexWrites(st, 0, count, 1, type, indices, baseVertex);
}
private long prepareIndexedDrawBind(ContextState st, int mode, int type, String opName) {
if (st.rasterizerDiscard) return 0;
prepareVertexWrites(st);
if (!drawDispatch.prepareIndexedDraw(st, mode, type)) return 0;
final int ebo = st.currentVao.elementBuffer;
final long eboHandle = resourceManager.getBufferHandle(ebo);
@@ -1278,6 +1290,7 @@ public class SDLGPURenderBackend extends RenderBackend {
final int baseVertex = MemoryUtil.memGetInt(pBaseVertex + offset);
if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "multiElementsBaseVertex", mode, count, type, firstIndex, baseVertex);
drawDispatch.issueIndexedDraw(st, rp, ebo, type, count, 1, firstIndex, baseVertex);
captureVertexWrites(st, 0, count, 1, type, indices, baseVertex);
}
}
@@ -1304,6 +1317,7 @@ public class SDLGPURenderBackend extends RenderBackend {
}
if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "elementsIndirect", mode, drawcount, type, (int) indirect, 0);
SDL_DrawGPUIndexedPrimitivesIndirect(rp, indirectHandle, (int) indirect, drawcount);
if (st.boundProgramObj != null && st.boundProgramObj.vertexWriteReplay != null) vertexWriteReplayer.noteIndirectDraw();
} finally {
Tracy.endZone();
}
@@ -1418,6 +1432,11 @@ public class SDLGPURenderBackend extends RenderBackend {
resourceManager.deleteTexture(texture);
}
/** Captured vertex-write replays name textures by GL id, which a delete or reallocation would make mean something else. */
private void flushVertexWritesBeforeTextureChange() {
if (vertexWriteReplayer.hasPending()) frameManager.endRenderPassIfActive(FrameManager.PASS_END_COMPUTE);
}
@Override public void bindTexture(int target, int texture) {
if (isProxyTarget(target)) return;
@@ -4271,6 +4290,10 @@ public class SDLGPURenderBackend extends RenderBackend {
@Override public void memoryBarrier(int barriers) {
// Graphics storage bindings are read-only on SDL_GPU; RW is compute-pass only
frameManager.endCopyPassIfActive();
// Replayed vertex image writes land when the render pass ends, and a barrier promises them to the draws after it
if ((barriers & (GL42.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL42.GL_TEXTURE_FETCH_BARRIER_BIT)) != 0 && vertexWriteReplayer.hasPending()) {
frameManager.endRenderPassIfActive(FrameManager.PASS_END_COMPUTE);
}
}
@Override public void copyImageSubData(int srcName, int srcTarget, int srcLevel, int srcX, int srcY, int srcZ,
@@ -4417,18 +4440,45 @@ public class SDLGPURenderBackend extends RenderBackend {
voxelizationDispatcher.endBatch(pass);
}
@Override public void setVertexWriteReplay(int graphicsProgram, VertexWriteReplaySetup setup) {
vertexWriteReplayer.setReplay(graphicsProgram, setup);
}
@Override public boolean supportsComputeImageAtomics() {
return device.supportsSpirv() || !device.supportsMsl();
}
@Override public boolean supportsComputeImageAtomics(int width, int height, int depth) {
return supportsComputeImageAtomics() || device.supportsImageAtomics(width, height, depth);
}
private void prepareVertexWrites(ContextState st) {
final ShaderManager.ProgramObject prog = st.boundProgramObj;
if (prog != null && prog.vertexWriteReplay != null) vertexWriteReplayer.ensureImageUsage(prog.vertexWriteReplay);
}
private void captureVertexWrites(ContextState st, int first, int count, int instances, int indexType, long indexOffset, int baseVertex) {
final ShaderManager.ProgramObject prog = st.boundProgramObj;
if (prog != null && prog.vertexWriteReplay != null) {
vertexWriteReplayer.capture(st, first, count, instances, indexType, indexOffset, baseVertex);
if (!frameManager.isRenderPassActive()) vertexWriteReplayer.flush();
}
}
@Override
public void drawArraysInstanced(int mode, int first, int count, int primcount) {
if (count <= 0 || primcount <= 0) return;
final ContextState st = s();
if (st.rasterizerDiscard) return;
if (!frameManager.isFrameActive()) return;
prepareVertexWrites(st);
drawDispatch.setPrimitiveTypeForDraw(st, FormatMap.mapPrimitiveType(mode));
pipelineApplier.ensureDrawRenderPass(st);
if (!frameManager.isRenderPassActive()) return;
if (!pipelineApplier.applyPipelineAndState(st)) return;
if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "arraysInstanced", mode, count, 0, first, 0);
SDL_DrawGPUPrimitives(frameManager.getRenderPass(), count, primcount, first, 0);
captureVertexWrites(st, first, count, primcount, 0, 0, 0);
}
@Override
@@ -45,6 +45,27 @@ public final class ComputeBinder implements ComputeDispatchSink {
private final ShaderManager shaderManager;
private final SamplerBinder samplerBinder;
private boolean droppedDispatchWarned;
private ShaderManager.ProgramObject lastUniformProgram;
private long lastUniformCb;
private long[] samplerOverrides;
private long[] rwBufferOverrides;
private ByteBuffer[] uniformBlockOverrides;
public void setSamplerOverrides(long[] byUnit) {
samplerOverrides = byUnit;
}
public void setReadWriteStorageBufferOverrides(long[] byGlSlot) {
rwBufferOverrides = byGlSlot;
}
public void setUniformBlockOverrides(ByteBuffer[] byIndex) {
uniformBlockOverrides = byIndex;
}
public long samplerHandle(int samplerObject, int glTexId) {
return samplerBinder.getSampler(samplerObject, glTexId);
}
public ComputeBinder(FrameManager frameManager, ResourceManager resourceManager, ShaderManager shaderManager, SamplerBinder samplerBinder) {
this.frameManager = frameManager;
@@ -153,6 +174,43 @@ public final class ComputeBinder implements ComputeDispatchSink {
@Override
public long beginBatchedComputeDispatch(ContextState st, boolean cycleRwBuffers) {
return beginComputePass(st, cycleRwBuffers, true);
}
public long beginComputePassWithoutStorageBuffers(ContextState st) {
return beginComputePass(st, false, false);
}
public boolean bindRoStorageBufferHandles(long pass, ContextState st, long[] handlesByGlSlot) {
if (pass == 0) return false;
final ShaderManager.ProgramObject prog = shaderManager.getProgram(st.boundProgram);
if (prog == null || !prog.linked) return false;
final int[] slots = prog.computeBindingMap.roSsboGlSlots();
if (slots.length == 0) return true;
try (MemoryStack stack = MemoryStack.stackPush()) {
final PointerBuffer ptrs = stack.mallocPointer(slots.length);
for (int i = 0; i < slots.length; i++) {
long handle = slots[i] < handlesByGlSlot.length ? handlesByGlSlot[slots[i]] : 0;
if (handle == 0) handle = resourceManager.getOrCreateFallbackStorageBuffer();
if (handle == 0) return false;
ptrs.put(i, handle);
}
SDL_BindGPUComputeStorageBuffers(pass, 0, ptrs);
}
return true;
}
public boolean rebindSamplers(long pass, ContextState st) {
if (pass == 0) return false;
final ShaderManager.ProgramObject prog = shaderManager.getProgram(st.boundProgram);
if (prog == null || !prog.linked) return false;
final ShaderManager.ComputeBindingMap map = prog.computeBindingMap;
try (MemoryStack stack = MemoryStack.stackPush()) {
return bindComputeSamplers(st, prog, pass, map.samplerGlSlots(), map.samplerNames(), stack);
}
}
private long beginComputePass(ContextState st, boolean cycleRwBuffers, boolean bindRoBuffers) {
final int programId = st.boundProgram;
if (programId == 0) return 0;
final ShaderManager.ProgramObject prog = shaderManager.getProgram(programId);
@@ -196,7 +254,7 @@ public final class ComputeBinder implements ComputeDispatchSink {
final boolean ok = bindComputeSamplers(st, prog, pass, samplerSlots, map.samplerNames(), stack)
&& bindRoStorageTextures(st, prog, pass, map, stack)
&& bindRoStorageBuffers(st, pass, roBufSlots, stack);
&& (!bindRoBuffers || bindRoStorageBuffers(st, pass, roBufSlots, stack));
if (!ok) {
SDL_EndGPUComputePass(pass);
frameManager.noteComputePassEnded();
@@ -298,7 +356,7 @@ public final class ComputeBinder implements ComputeDispatchSink {
private SDL_GPUStorageBufferReadWriteBinding.Buffer buildRwBufferBindings(ContextState st, int[] slots, MemoryStack stack, boolean cycle) {
final SDL_GPUStorageBufferReadWriteBinding.Buffer buf = SDL_GPUStorageBufferReadWriteBinding.calloc(slots.length, stack);
for (int i = 0; i < slots.length; i++) {
final long handle = resolveStorageBufHandle(st, slots[i]);
final long handle = rwBufferOverrides != null && slots[i] < rwBufferOverrides.length ? rwBufferOverrides[slots[i]] : resolveStorageBufHandle(st, slots[i]);
if (handle == 0) { warnMissingBinding(st, 1, "RW storage buffer slot", slots[i], true); return null; }
buf.get(i).buffer(handle).cycle(cycle);
@@ -319,7 +377,8 @@ public final class ComputeBinder implements ComputeDispatchSink {
if (texHandle == 0) { warnMissingBinding(st, 2, "sampler texture unit", unit, true); return false; }
warnMissingBinding(st, 2, "sampler texture unit", unit, false);
}
long sampHandle = samplerBinder.getSamplerForUnit(st, unit, texGlId);
final long override = samplerOverrides != null && unit >= 0 && unit < samplerOverrides.length ? samplerOverrides[unit] : 0;
long sampHandle = override != 0 ? override : samplerBinder.getSamplerForUnit(st, unit, texGlId);
if (sampHandle == 0) {
sampHandle = resourceManager.getOrCreateDefaultSampler();
if (sampHandle == 0) { warnMissingBinding(st, 3, "sampler unit", unit, true); return false; }
@@ -374,6 +433,28 @@ public final class ComputeBinder implements ComputeDispatchSink {
return true;
}
public ByteBuffer uniformBlockView(ContextState st, ShaderManager.ProgramObject prog, int i) {
final ShaderManager.ComputeBindingMap map = prog.computeBindingMap;
final int[] uboSlots = map.uboGlSlots();
final int[] declaredSizes = map.uboSizes();
if (i >= uboSlots.length) return null;
final int slot = uboSlots[i];
final int bufGlId = st.boundUboByIndex[slot];
if (bufGlId == 0) return null;
final ByteBuffer shadow = resourceManager.getUboShadow(bufGlId);
if (shadow == null) return null;
final int offset = st.uboRangeOffset[slot];
if (offset < 0 || offset >= shadow.capacity()) return null;
int size = (i < declaredSizes.length && declaredSizes[i] > 0) ? declaredSizes[i] : shadow.capacity() - offset;
final int rangeSize = st.uboRangeSize[slot];
if (rangeSize > 0) size = Math.min(size, rangeSize);
size = Math.min(size, shadow.capacity() - offset);
if (size <= 0) return null;
final ByteBuffer view = frameManager.getUboPushView(bufGlId, shadow);
view.position(offset).limit(offset + size);
return view;
}
private void pushComputeUboData(ContextState st, long cb, ShaderManager.ProgramObject prog, int[] uboSlots) {
if (prog.lastComputeUboHash == null || prog.lastComputeUboHash.length != uboSlots.length) {
prog.lastComputeUboHash = new long[uboSlots.length];
@@ -381,16 +462,18 @@ public final class ComputeBinder implements ComputeDispatchSink {
prog.lastComputeFrame = 0;
}
final long curFrame = frameManager.getFrameNumber();
final boolean cacheValid = cb == prog.lastComputeCb && curFrame == prog.lastComputeFrame;
final boolean cacheValid = cb == prog.lastComputeCb && curFrame == prog.lastComputeFrame
&& lastUniformProgram == prog && lastUniformCb == cb;
final long[] cache = prog.lastComputeUboHash;
if (!cacheValid) {
for (int i = 0; i < cache.length; i++) cache[i] = 0L;
prog.lastComputeCb = cb;
prog.lastComputeFrame = curFrame;
}
lastUniformProgram = prog;
lastUniformCb = cb;
final ShaderManager.ComputeBindingMap map = prog.computeBindingMap;
final boolean[] isDefault = map.uboIsDefaultBlock();
final int[] declaredSizes = map.uboSizes();
final UniformStaging us = st.uniformStaging(prog);
for (int i = 0; i < uboSlots.length; i++) {
final ByteBuffer view;
@@ -398,21 +481,13 @@ public final class ComputeBinder implements ComputeDispatchSink {
if (us.fsUniformBuf == null || prog.fragmentUboSize <= 0) continue;
view = us.fsUniformBuf;
view.position(0).limit(prog.fragmentUboSize);
} else if (uniformBlockOverrides != null && i < uniformBlockOverrides.length && uniformBlockOverrides[i] != null) {
view = uniformBlockOverrides[i];
view.position(0);
if (!view.hasRemaining()) continue;
} else {
final int slot = uboSlots[i];
final int bufGlId = st.boundUboByIndex[slot];
if (bufGlId == 0) continue;
final ByteBuffer shadow = resourceManager.getUboShadow(bufGlId);
if (shadow == null) continue;
final int offset = st.uboRangeOffset[slot];
if (offset < 0 || offset >= shadow.capacity()) continue;
int size = (i < declaredSizes.length && declaredSizes[i] > 0) ? declaredSizes[i] : shadow.capacity() - offset;
final int rangeSize = st.uboRangeSize[slot];
if (rangeSize > 0) size = Math.min(size, rangeSize);
size = Math.min(size, shadow.capacity() - offset);
if (size <= 0) continue;
view = frameManager.getUboPushView(bufGlId, shadow);
view.position(offset).limit(offset + size);
view = uniformBlockView(st, prog, i);
if (view == null) continue;
}
final int pos = view.position();
final int len = view.remaining();
@@ -0,0 +1,152 @@
package com.gtnewhorizons.angelica.sdlgpu.compute;
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileCache;
import com.gtnewhorizons.angelica.sdlgpu.shader.msl.MslCrossCompile;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
import org.lwjgl.opengl.GL43;
import org.lwjgl.sdl.SDLError;
import org.lwjgl.sdl.SDL_GPUComputePipelineCreateInfo;
import org.lwjgl.sdl.SDL_GPUStorageTextureReadWriteBinding;
import org.lwjgl.sdl.SDL_GPUTextureCreateInfo;
import org.lwjgl.sdl.SDL_GPUTextureRegion;
import org.lwjgl.sdl.SDL_GPUTextureTransferInfo;
import org.lwjgl.sdl.SDL_GPUTransferBufferCreateInfo;
import org.lwjgl.system.JNI;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.system.macosx.ObjCRuntime;
import org.lwjgl.util.shaderc.Shaderc;
import static org.lwjgl.sdl.SDLGPU.*;
public final class ImageAtomicsProbe {
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
private static final String SOURCE = "angelica:sdlgpu/atomic_probe.csh";
public static final int MTL_LANGUAGE_VERSION_3_1 = (3 << 16) + 1;
private static final int GROUPS = 64;
private final long device;
private long pipeline;
public ImageAtomicsProbe(long device) {
this.device = device;
final SpirvCompiler.Result compiled = SpirvCompiler.compile(ShaderLoader.getShaderSource(SOURCE), Shaderc.shaderc_compute_shader, "atomic_probe", SpirvCompiler.Options.vulkanForced460Core(), false);
if (compiled.spirv() == null) {
LOG.warn("Metal image atomics: probe pipeline failed: {}", compiled.error());
return;
}
CrossCompileCache.Output msl = null;
try (MemoryStack stack = MemoryStack.stackPush()) {
ShaderManager.remapSpirvForComputeSDLGPU(compiled.spirv());
msl = MslCrossCompile.compileAtVersion(compiled.spirv(), GL43.GL_COMPUTE_SHADER, MslCrossCompile.SPVC_MSL_3_1);
pipeline = SDL_CreateGPUComputePipeline(device, SDL_GPUComputePipelineCreateInfo.calloc(stack)
.code(msl.code())
.entrypoint(stack.ASCII(msl.entrypoint()))
.format(SDL_GPU_SHADERFORMAT_MSL)
.num_readwrite_storage_textures(1)
.threadcount_x(InvocationDispatch.WORKGROUP_SIZE).threadcount_y(1).threadcount_z(1));
if (pipeline == 0) LOG.warn("Metal image atomics: probe pipeline failed: {}", SDLError.SDL_GetError());
} catch (RuntimeException e) {
LOG.warn("Metal image atomics: probe pipeline failed", e);
} finally {
if (msl != null) MemoryUtil.memFree(msl.code());
MemoryUtil.memFree(compiled.spirv());
}
}
public static int defaultMslVersion() {
try {
final long msgSend = ObjCRuntime.getLibrary().getFunctionAddress("objc_msgSend");
final long cls = ObjCRuntime.objc_getClass("MTLCompileOptions");
if (msgSend == 0 || cls == 0) return 0;
final long options = JNI.invokePPP(cls, ObjCRuntime.sel_getUid("new"), msgSend);
if (options == 0) return 0;
final long version = JNI.invokePPP(options, ObjCRuntime.sel_getUid("languageVersion"), msgSend);
JNI.invokePPV(options, ObjCRuntime.sel_getUid("release"), msgSend);
return (int) version;
} catch (RuntimeException | LinkageError e) {
LOG.warn("Could not read the default Metal language version", e);
return 0;
}
}
public boolean run(int width, int height) {
if (pipeline == 0) return false;
long texture = 0, upload = 0, download = 0, cb = 0;
try (MemoryStack stack = MemoryStack.stackPush()) {
texture = SDL_CreateGPUTexture(device, SDL_GPUTextureCreateInfo.calloc(stack)
.type(SDL_GPU_TEXTURETYPE_2D)
.format(SDL_GPU_TEXTUREFORMAT_R32_INT)
.usage(SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COMPUTE_STORAGE_WRITE)
.width(width).height(height).layer_count_or_depth(1).num_levels(1)
.sample_count(SDL_GPU_SAMPLECOUNT_1));
if (texture == 0) return failSdl(width, height, "texture");
upload = SDL_CreateGPUTransferBuffer(device, SDL_GPUTransferBufferCreateInfo.calloc(stack).usage(SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD).size(Integer.BYTES));
download = SDL_CreateGPUTransferBuffer(device, SDL_GPUTransferBufferCreateInfo.calloc(stack).usage(SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD).size(Integer.BYTES));
if (upload == 0 || download == 0) return failSdl(width, height, "transfer buffer");
final long zero = nSDL_MapGPUTransferBuffer(device, upload, false);
if (zero == 0) return failSdl(width, height, "map");
MemoryUtil.memPutInt(zero, 0);
SDL_UnmapGPUTransferBuffer(device, upload);
cb = SDL_AcquireGPUCommandBuffer(device);
if (cb == 0) return failSdl(width, height, "command buffer");
final SDL_GPUTextureRegion region = SDL_GPUTextureRegion.calloc(stack).texture(texture).w(1).h(1).d(1);
long copy = SDL_BeginGPUCopyPass(cb);
SDL_UploadToGPUTexture(copy, SDL_GPUTextureTransferInfo.calloc(stack).transfer_buffer(upload), region, false);
SDL_EndGPUCopyPass(copy);
final SDL_GPUStorageTextureReadWriteBinding.Buffer binding = SDL_GPUStorageTextureReadWriteBinding.calloc(1, stack);
binding.get(0).texture(texture);
final long pass = SDL_BeginGPUComputePass(cb, binding, null);
if (pass == 0) return failSdl(width, height, "compute pass");
SDL_BindGPUComputePipeline(pass, pipeline);
SDL_DispatchGPUCompute(pass, GROUPS, 1, 1);
SDL_EndGPUComputePass(pass);
copy = SDL_BeginGPUCopyPass(cb);
SDL_DownloadFromGPUTexture(copy, region, SDL_GPUTextureTransferInfo.calloc(stack).transfer_buffer(download));
SDL_EndGPUCopyPass(copy);
final boolean submitted = SDL_SubmitGPUCommandBuffer(cb);
cb = 0;
if (!submitted) return failSdl(width, height, "submit");
SDL_WaitForGPUIdle(device);
final long result = nSDL_MapGPUTransferBuffer(device, download, false);
if (result == 0) return failSdl(width, height, "map");
final int adds = MemoryUtil.memGetInt(result);
SDL_UnmapGPUTransferBuffer(device, download);
final int invocations = GROUPS * InvocationDispatch.WORKGROUP_SIZE;
if (adds != invocations) LOG.info("Metal image atomics on {}x{}: {} invocations added {}", width, height, invocations, adds);
return adds == invocations;
} catch (RuntimeException e) {
return fail(width, height, e.toString());
} finally {
if (cb != 0) SDL_CancelGPUCommandBuffer(cb);
if (download != 0) SDL_ReleaseGPUTransferBuffer(device, download);
if (upload != 0) SDL_ReleaseGPUTransferBuffer(device, upload);
if (texture != 0) SDL_ReleaseGPUTexture(device, texture);
}
}
public void release() {
if (pipeline != 0) SDL_ReleaseGPUComputePipeline(device, pipeline);
pipeline = 0;
}
private static boolean failSdl(int width, int height, String what) {
return fail(width, height, what + " failed: " + SDLError.SDL_GetError());
}
private static boolean fail(int width, int height, String reason) {
LOG.warn("Metal image atomics on {}x{}: {}", width, height, reason);
return false;
}
}
@@ -0,0 +1,24 @@
package com.gtnewhorizons.angelica.sdlgpu.compute;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
public final class InvocationDispatch {
static final int MAX_GROUPS_PER_DISPATCH = 65535;
static final int WORKGROUP_SIZE = 64;
@FunctionalInterface
public interface IntUniformWriter {
void write(ContextState st, int location, int value);
}
private InvocationDispatch() {}
static void dispatch(ComputeDispatchSink sink, ContextState st, long pass, long invocations, int invocationBaseLocation, IntUniformWriter writer) {
final long groups = (invocations + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE;
for (long g0 = 0; g0 < groups; g0 += MAX_GROUPS_PER_DISPATCH) {
if (invocationBaseLocation >= 0) writer.write(st, invocationBaseLocation, (int) (g0 * WORKGROUP_SIZE));
sink.pushPendingComputeUniforms(st);
sink.dispatchInBatch(pass, (int) Math.min(MAX_GROUPS_PER_DISPATCH, groups - g0), 1, 1);
}
}
}
@@ -0,0 +1,635 @@
package com.gtnewhorizons.angelica.sdlgpu.compute;
import com.gtnewhorizons.angelica.glsm.backend.VertexWriteReplaySetup;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineApplier;
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import com.gtnewhorizons.angelica.sdlgpu.shader.UniformStaging;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL33;
import org.lwjgl.opengl.GL41;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.IntSupplier;
import java.util.function.Supplier;
/**
* Performs the image writes of a vertex shader that SDL-GPU strips (graphics stages may only read storage images).
* Every draw made with a program that has a {@link Replay} is captured: its vertex and index buffers, attribute layout,
* draw range, the uniform values the replay reads and the textures it samples. When the render pass ends, the replay
* compute shader re-runs the vertex shader for each captured vertex and performs the writes.
*/
public final class VertexWriteReplayer {
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
private static final int MAX_CAPTURES = 1 << 16;
private static final int[] COMPONENT_SIZE = { 4, 2, 1, 1, 2, 2, 4, 4, 4, 4, 4, 4 };
private static final int FIRST_PACKED_TYPE = 9;
private static final int PACKED_10F_11F_11F = 11;
private static final int ENABLED = 0x2000;
private static final int BGRA = 0x4000;
private static final int NO_ELEMENT = 0x8000;
private static final int MAX_DIVISOR = 0xFFFF;
private static final int UNCHANGED = -1;
private static final int TEXTURE_UNITS = 32;
private static final int SAMPLER_OBJECT_OF_DRAW = -1;
public static final class Replay {
final int replayProgram;
final VertexWriteReplaySetup setup;
private boolean resolved;
private boolean broken;
int[] inputLocations;
int[] uniformSources;
int[] uniformTargets;
int[] samplerUnits;
IntSupplier[] samplerSources;
int[] samplerObjects;
int[] packStorageSlots;
int[] packRwStorageSlots;
int[] uniformBlockIndices;
int uniformBlockCount;
int locAttr, locAttrDefault, locDraw, locIndex, locRestart, locInvocationBase;
long[] lastHashes;
boolean lastHashesApplied;
public Replay(VertexWriteReplaySetup setup) {
this.replayProgram = setup.replayProgram();
this.setup = setup;
}
IntSupplier[] images() {
return setup.images();
}
boolean resolve(ShaderManager.ProgramObject graphics, ShaderManager.ProgramObject replay) {
if (resolved) return !broken;
resolved = true;
final String[] inputNames = setup.inputNames();
inputLocations = new int[inputNames.length];
for (int i = 0; i < inputNames.length; i++) {
int loc = graphics.resolvedAttribLocations.getInt(inputNames[i]);
if (loc < 0) loc = graphics.attribLocationBindings.getInt(inputNames[i]);
if (loc < 0) loc = setup.inputLocations()[i];
inputLocations[i] = loc;
}
final IntArrayList sources = new IntArrayList();
final IntArrayList targets = new IntArrayList();
final List<String> samplerNames = new ArrayList<>();
for (int target = 0; target < replay.uniformSlotCount; target++) {
final String name = replay.locationName[target];
if (name == null || name.startsWith("_vg_")) continue;
if (replay.locationKind[target] == ShaderManager.ProgramObject.LOCATION_KIND_SAMPLER) {
samplerNames.add(name);
continue;
}
if (replay.locationKind[target] != ShaderManager.ProgramObject.LOCATION_KIND_PLAIN) continue;
final int source = graphics.nameToLocation.getInt(name);
if (source < 0) continue;
sources.add(source);
targets.add(target);
}
uniformSources = sources.toIntArray();
uniformTargets = targets.toIntArray();
lastHashes = new long[uniformSources.length];
resolveSamplers(graphics, replay, samplerNames);
final IntArrayList storage = new IntArrayList();
for (int slot : replay.computeBindingMap.roSsboGlSlots()) {
final boolean vertexBuffer = slot >= setup.vertexBufferBinding() && slot < setup.vertexBufferBinding() + setup.vertexBufferCount();
if (!vertexBuffer && slot != setup.indexBufferBinding()) storage.add(slot);
}
packStorageSlots = storage.toIntArray();
packRwStorageSlots = replay.computeBindingMap.rwSsboGlSlots().clone();
final IntArrayList blocks = new IntArrayList();
final boolean[] isDefault = replay.computeBindingMap.uboIsDefaultBlock();
for (int i = 0; i < replay.computeBindingMap.uboGlSlots().length; i++) {
if (i >= isDefault.length || !isDefault[i]) blocks.add(i);
}
uniformBlockIndices = blocks.toIntArray();
uniformBlockCount = replay.computeBindingMap.uboGlSlots().length;
locAttr = replay.nameToLocation.getInt("_vg_attr");
locAttrDefault = replay.nameToLocation.getInt("_vg_attrDefault");
locDraw = replay.nameToLocation.getInt("_vg_draw");
locIndex = replay.nameToLocation.getInt("_vg_index");
locRestart = replay.nameToLocation.getInt("_vg_restart");
locInvocationBase = replay.nameToLocation.getInt("_vg_invocationBase");
if (locDraw < 0 || locInvocationBase < 0) {
broken = true;
LOG.warn("Vertex write replay program {} is missing its dispatch uniforms; its writes are dropped", replayProgram);
}
return !broken;
}
private void resolveSamplers(ShaderManager.ProgramObject graphics, ShaderManager.ProgramObject replay, List<String> names) {
final IntArrayList units = new IntArrayList();
final List<IntSupplier> sources = new ArrayList<>();
final IntArrayList objects = new IntArrayList();
final IntArrayList ownTextureIndices = new IntArrayList();
final List<String> ownTextureNames = new ArrayList<>();
final List<String> pipelineNames = Arrays.asList(setup.samplerNames());
for (String name : names) {
int unit = graphics.samplerTextureUnits.getInt(name);
if (unit < 0) {
final int i = pipelineNames.indexOf(name);
if (i < 0) continue;
if (setup.samplerTextures()[i] != null && setup.samplerUnits()[i] < 0) {
ownTextureNames.add(name);
ownTextureIndices.add(i);
continue;
}
unit = setup.samplerUnits()[i];
if (unit < 0) continue;
}
replay.samplerTextureUnits.put(name, unit);
if (!units.contains(unit)) {
units.add(unit);
sources.add(null);
objects.add(SAMPLER_OBJECT_OF_DRAW);
}
}
int free = TEXTURE_UNITS - 1;
for (int k = 0; k < ownTextureNames.size(); k++) {
while (free >= 0 && units.contains(free)) free--;
if (free < 0) break;
final int i = ownTextureIndices.getInt(k);
replay.samplerTextureUnits.put(ownTextureNames.get(k), free);
units.add(free);
sources.add(setup.samplerTextures()[i]);
objects.add(setup.samplerObjects()[i]);
}
samplerUnits = units.toIntArray();
samplerSources = sources.toArray(new IntSupplier[0]);
samplerObjects = objects.toIntArray();
}
}
private static final class Capture {
Replay replay;
long[] buffers = new long[0];
int bufferCount;
long indexBuffer;
float[] attr = new float[0];
float[] attrDefault = new float[0];
int[] samplerTextures = new int[0];
long[] samplerHandles = new long[0];
int[] imageTextures = new int[0];
long[] packStorage = new long[0];
long[] packRwStorage = new long[0];
ByteBuffer[] uniformBlocks = new ByteBuffer[0];
int first, count, instances, baseVertex, indexSize, indexOffset, indexCount;
boolean restart;
int restartIndex;
}
private final ShaderManager shaderManager;
private final ResourceManager resourceManager;
private final PipelineApplier pipelineApplier;
private final ComputeBinder computeBinder;
private final Supplier<ContextState> state;
private final ArrayList<Capture> captures = new ArrayList<>();
private int pending;
private float[] uniformValues = new float[4096];
private int[] uniformLengths = new int[256];
private int uniformValueCount;
private int uniformCount;
private boolean flushing;
private final long[] handlesByGlSlot = new long[ContextState.MAX_INDEXED_BUFFERS];
private final long[] rwHandlesByGlSlot = new long[ContextState.MAX_INDEXED_BUFFERS];
private final long[] samplerOverrides = new long[TEXTURE_UNITS];
private final int[] savedImages = new int[ContextState.MAX_IMAGE_UNITS];
private final int[] savedLevels = new int[ContextState.MAX_IMAGE_UNITS];
private final int[] savedAccess = new int[ContextState.MAX_IMAGE_UNITS];
private final int[] savedTextures = new int[TEXTURE_UNITS];
private int uniformCursor;
private int valueCursor;
private final InvocationDispatch.IntUniformWriter invocationBaseWriter = (st, location, value) -> putIntVector(st, location, 1, value, 0, 0, 0);
private final Set<String> warned = new HashSet<>();
public VertexWriteReplayer(ShaderManager shaderManager, ResourceManager resourceManager, PipelineApplier pipelineApplier, ComputeBinder computeBinder, Supplier<ContextState> state) {
this.shaderManager = shaderManager;
this.resourceManager = resourceManager;
this.pipelineApplier = pipelineApplier;
this.computeBinder = computeBinder;
this.state = state;
}
public void setReplay(int graphicsProgram, VertexWriteReplaySetup setup) {
final ShaderManager.ProgramObject prog = shaderManager.getProgram(graphicsProgram);
if (prog == null) return;
if (setup == null) {
prog.vertexWriteReplay = null;
return;
}
if (setup.images().length > ContextState.MAX_IMAGE_UNITS) {
warnOnce("images:" + setup.replayProgram(), "Vertex write replay program " + setup.replayProgram() + " uses " + setup.images().length + " images, more than the " + ContextState.MAX_IMAGE_UNITS + " image units; its writes are dropped");
prog.vertexWriteReplay = null;
return;
}
prog.vertexWriteReplay = new Replay(setup);
}
public void ensureImageUsage(Replay replay) {
final IntSupplier[] images = replay.images();
final int written = replay.setup.writtenImageCount();
for (int i = 0; i < images.length; i++) {
final int texture = images[i].getAsInt();
if (texture != 0) resourceManager.ensureTextureUsage(texture, SDLGPURenderBackend.imageUsageForAccess(imageAccess(i, written)));
}
}
public void capture(ContextState st, int first, int count, int instances, int indexType, long indexOffset, int baseVertex) {
final ShaderManager.ProgramObject graphics = st.boundProgramObj;
final Replay replay = graphics.vertexWriteReplay;
final ShaderManager.ProgramObject twin = shaderManager.getProgram(replay.replayProgram);
if (twin == null || !twin.linked || twin.sdlComputePipeline == 0) return;
if (!replay.resolve(graphics, twin)) return;
if (count <= 0 || instances <= 0) return;
if (pending == MAX_CAPTURES) {
warnOnce("captures", "More than " + MAX_CAPTURES + " draws to replay vertex image writes for in one render pass; the rest are dropped");
return;
}
final IntSupplier[] images = replay.images();
final Capture c = acquire();
c.replay = replay;
if (c.buffers.length != replay.setup.vertexBufferCount()) c.buffers = new long[replay.setup.vertexBufferCount()];
c.bufferCount = 0;
if (!captureInputs(st, replay, c)) return;
if (indexType != 0) {
c.indexBuffer = resourceManager.getBufferHandle(st.currentVao.elementBuffer);
c.indexSize = indexType == GL11.GL_UNSIGNED_BYTE ? 1 : indexType == GL11.GL_UNSIGNED_SHORT ? 2 : 4;
c.indexOffset = (int) indexOffset;
if (c.indexBuffer == 0) return;
final long indexBytes = resourceManager.getBufferSize(st.currentVao.elementBuffer);
c.indexCount = indexBytes <= 0 ? -1 : (int) Math.min(Math.max(0L, indexBytes - indexOffset) / c.indexSize, Integer.MAX_VALUE);
c.restart = st.primitiveRestartEnabled && indexType != GL11.GL_UNSIGNED_BYTE;
c.restartIndex = indexType == GL11.GL_UNSIGNED_SHORT ? st.primitiveRestartSentinel & 0xFFFF : st.primitiveRestartSentinel;
} else {
c.indexBuffer = 0;
c.indexSize = 0;
c.indexOffset = 0;
c.indexCount = 0;
c.restart = false;
}
c.first = first;
c.count = count;
c.instances = instances;
c.baseVertex = baseVertex;
if (c.samplerTextures.length != replay.samplerUnits.length) {
c.samplerTextures = new int[replay.samplerUnits.length];
c.samplerHandles = new long[replay.samplerUnits.length];
}
for (int i = 0; i < replay.samplerUnits.length; i++) {
final int unit = replay.samplerUnits[i];
final IntSupplier source = replay.samplerSources[i];
c.samplerTextures[i] = source != null ? source.getAsInt() : st.boundTextures[unit];
final int samplerObject = replay.samplerObjects[i] != SAMPLER_OBJECT_OF_DRAW ? replay.samplerObjects[i] : st.boundSamplerObjects[unit];
c.samplerHandles[i] = computeBinder.samplerHandle(samplerObject, c.samplerTextures[i]);
}
if (c.imageTextures.length != images.length) c.imageTextures = new int[images.length];
for (int i = 0; i < images.length; i++) c.imageTextures[i] = images[i].getAsInt();
if (c.packStorage.length != replay.packStorageSlots.length) c.packStorage = new long[replay.packStorageSlots.length];
for (int i = 0; i < replay.packStorageSlots.length; i++) {
c.packStorage[i] = resourceManager.getBufferHandle(st.boundSsboByIndex[replay.packStorageSlots[i]]);
}
if (c.packRwStorage.length != replay.packRwStorageSlots.length) c.packRwStorage = new long[replay.packRwStorageSlots.length];
for (int i = 0; i < replay.packRwStorageSlots.length; i++) {
c.packRwStorage[i] = resourceManager.getBufferHandle(st.boundSsboByIndex[replay.packRwStorageSlots[i]]);
}
captureUniformBlocks(st, twin, replay, c);
captureUniforms(st, graphics, replay);
pending++;
}
private boolean captureInputs(ContextState st, Replay replay, Capture c) {
final int n = replay.inputLocations.length;
if (c.attr.length < n * 4) {
c.attr = new float[n * 4];
c.attrDefault = new float[n * 4];
}
final ContextState.VAOState vao = st.currentVao;
for (int k = 0; k < n; k++) {
final int loc = replay.inputLocations[k];
int x = 0, y = 0, z = 0, w = 0;
if (loc >= 0 && loc < ContextState.MAX_VERTEX_ATTRIBS) {
System.arraycopy(st.attribDefaults, loc * 4, c.attrDefault, k * 4, 4);
if (vao.attribEnabled[loc]) {
final int type = componentType(vao.attribType[loc]);
final int binding = vao.attribBinding[loc];
final int buffer = vao.bindingBuffer[binding];
final long handle = type >= 0 ? resourceManager.getBufferHandle(buffer) : 0;
if (type < 0) warnOnce("type:" + vao.attribType[loc], "Vertex write replay cannot read vertex attribute type 0x" + Integer.toHexString(vao.attribType[loc]) + "; that input reads its default value");
if (handle != 0) {
final int slot = bufferSlot(c, handle);
if (slot < 0) {
warnOnce("buffers", "A draw reads more than " + c.buffers.length + " vertex buffers; its vertex image writes are dropped");
return false;
}
final boolean bgra = vao.attribSize[loc] == GL12.GL_BGRA;
final int size = type == PACKED_10F_11F_11F ? 3 : bgra ? 4 : vao.attribSize[loc];
final int elementBytes = type >= FIRST_PACKED_TYPE ? 4 : size * COMPONENT_SIZE[type];
final int stride = vao.bindingStride[binding] != 0 ? vao.bindingStride[binding] : elementBytes;
final long offset = vao.bindingOffset[binding] + vao.attribRelativeOffset[loc];
final int divisor = Math.min(vao.bindingDivisor[binding], MAX_DIVISOR);
final long bufferSize = resourceManager.getBufferSize(buffer);
final boolean noElement = bufferSize > 0 && bufferSize < offset + elementBytes;
x = slot | type << 4 | size << 8 | (vao.attribNormalized[loc] ? 0x800 : 0) | (vao.attribIsInteger[loc] ? 0x1000 : 0)
| ENABLED | (bgra ? BGRA : 0) | (noElement ? NO_ELEMENT : 0) | divisor << 16;
y = (int) offset;
z = stride;
w = lastElement(bufferSize, offset, elementBytes, stride);
}
}
} else {
c.attrDefault[k * 4] = 0f;
c.attrDefault[k * 4 + 1] = 0f;
c.attrDefault[k * 4 + 2] = 0f;
c.attrDefault[k * 4 + 3] = 1f;
}
c.attr[k * 4] = Float.intBitsToFloat(x);
c.attr[k * 4 + 1] = Float.intBitsToFloat(y);
c.attr[k * 4 + 2] = Float.intBitsToFloat(z);
c.attr[k * 4 + 3] = Float.intBitsToFloat(w);
}
return true;
}
static int lastElement(long bufferSize, long offset, int elementBytes, int stride) {
if (bufferSize <= 0) return -1;
final long room = bufferSize - offset - elementBytes;
if (room < 0 || stride <= 0) return 0;
return (int) Math.min(room / stride, 0xFFFFFFFFL);
}
private static int bufferSlot(Capture c, long handle) {
for (int i = 0; i < c.bufferCount; i++) {
if (c.buffers[i] == handle) return i;
}
if (c.bufferCount == c.buffers.length) return -1;
c.buffers[c.bufferCount] = handle;
return c.bufferCount++;
}
static int componentType(int glType) {
return switch (glType) {
case GL11.GL_FLOAT -> 0;
case GL30.GL_HALF_FLOAT -> 1;
case GL11.GL_BYTE -> 2;
case GL11.GL_UNSIGNED_BYTE -> 3;
case GL11.GL_SHORT -> 4;
case GL11.GL_UNSIGNED_SHORT -> 5;
case GL11.GL_INT -> 6;
case GL11.GL_UNSIGNED_INT -> 7;
case GL41.GL_FIXED -> 8;
case GL33.GL_INT_2_10_10_10_REV -> 9;
case GL12.GL_UNSIGNED_INT_2_10_10_10_REV -> 10;
case GL30.GL_UNSIGNED_INT_10F_11F_11F_REV -> PACKED_10F_11F_11F;
default -> -1;
};
}
private void captureUniformBlocks(ContextState st, ShaderManager.ProgramObject twin, Replay replay, Capture c) {
if (c.uniformBlocks.length != replay.uniformBlockCount) c.uniformBlocks = new ByteBuffer[replay.uniformBlockCount];
for (int index : replay.uniformBlockIndices) {
final ByteBuffer view = computeBinder.uniformBlockView(st, twin, index);
if (view == null) {
if (c.uniformBlocks[index] != null) c.uniformBlocks[index].limit(0);
continue;
}
ByteBuffer copy = c.uniformBlocks[index];
if (copy == null || copy.capacity() < view.remaining()) copy = c.uniformBlocks[index] = ByteBuffer.allocateDirect(view.remaining()).order(ByteOrder.nativeOrder());
copy.clear();
copy.put(view);
copy.flip();
}
}
private void captureUniforms(ContextState st, ShaderManager.ProgramObject graphics, Replay replay) {
final UniformStaging us = st.uniformStaging(graphics);
final long[] hashes = us.uniformValueHashBySlot;
for (int u = 0; u < replay.uniformSources.length; u++) {
final int source = replay.uniformSources[u];
final float[] value = us.uniformDataBySlot != null && source < us.uniformDataBySlot.length ? us.uniformDataBySlot[source] : null;
final long hash = hashes != null && source < hashes.length ? hashes[source] : 0L;
if (uniformCount == uniformLengths.length) uniformLengths = Arrays.copyOf(uniformLengths, uniformLengths.length * 2);
if (value != null && replay.lastHashesApplied && hash == replay.lastHashes[u]) {
uniformLengths[uniformCount++] = UNCHANGED;
continue;
}
replay.lastHashes[u] = hash;
final int len = value == null ? 0 : value.length;
uniformLengths[uniformCount++] = len;
if (len == 0) continue;
if (uniformValueCount + len > uniformValues.length) uniformValues = Arrays.copyOf(uniformValues, Math.max(uniformValues.length * 2, uniformValueCount + len));
System.arraycopy(value, 0, uniformValues, uniformValueCount, len);
uniformValueCount += len;
}
replay.lastHashesApplied = true;
}
public boolean hasPending() {
return pending > 0;
}
public void noteIndirectDraw() {
warnOnce("indirect", "An indirect draw used a program whose vertex shader writes images; those writes are dropped");
}
private Capture acquire() {
if (pending == captures.size()) captures.add(new Capture());
return captures.get(pending);
}
public void flush() {
if (pending == 0 || flushing) return;
flushing = true;
final ContextState st = state.get();
final int savedProgram = st.boundProgram;
final ShaderManager.ProgramObject savedProgramObj = st.boundProgramObj;
try {
uniformCursor = 0;
valueCursor = 0;
int start = 0;
while (start < pending) {
final Capture first = captures.get(start);
final Replay replay = first.replay;
int end = start;
while (end < pending && captures.get(end).replay == replay && Arrays.equals(captures.get(end).imageTextures, first.imageTextures)
&& Arrays.equals(captures.get(end).packRwStorage, first.packRwStorage)) end++;
replayRun(st, replay, start, end);
start = end;
}
} finally {
st.boundProgram = savedProgram;
st.boundProgramObj = savedProgramObj;
for (int i = 0; i < pending; i++) captures.get(i).replay = null;
pending = 0;
uniformCount = 0;
uniformValueCount = 0;
flushing = false;
}
}
private void replayRun(ContextState st, Replay replay, int start, int end) {
final ShaderManager.ProgramObject twin = shaderManager.getProgram(replay.replayProgram);
if (twin == null || !twin.linked || twin.sdlComputePipeline == 0) {
skipUniforms(replay, start, end);
return;
}
final Capture first = captures.get(start);
final int[] imageTextures = first.imageTextures;
final int imageCount = imageTextures.length;
final int written = replay.setup.writtenImageCount();
System.arraycopy(st.boundStorageTextureByUnit, 0, savedImages, 0, imageCount);
System.arraycopy(st.boundStorageTextureLevel, 0, savedLevels, 0, imageCount);
System.arraycopy(st.boundStorageTextureAccess, 0, savedAccess, 0, imageCount);
for (int i = 0; i < replay.samplerUnits.length; i++) savedTextures[i] = st.boundTextures[replay.samplerUnits[i]];
computeBinder.setSamplerOverrides(samplerOverrides);
Arrays.fill(rwHandlesByGlSlot, 0L);
for (int i = 0; i < replay.packRwStorageSlots.length; i++) rwHandlesByGlSlot[replay.packRwStorageSlots[i]] = first.packRwStorage[i];
computeBinder.setReadWriteStorageBufferOverrides(rwHandlesByGlSlot);
try {
for (int i = 0; i < imageCount; i++) {
st.boundStorageTextureByUnit[i] = imageTextures[i];
st.boundStorageTextureLevel[i] = 0;
st.boundStorageTextureAccess[i] = imageAccess(i, written);
}
applySamplerTextures(st, replay, captures.get(start));
st.boundProgram = replay.replayProgram;
st.boundProgramObj = twin;
final long pass = computeBinder.beginComputePassWithoutStorageBuffers(st);
if (pass == 0) {
for (int i = start; i < end; i++) applyUniforms(st, replay);
return;
}
try {
for (int i = start; i < end; i++) {
final Capture c = captures.get(i);
if (i > start && applySamplerTextures(st, replay, c)) computeBinder.rebindSamplers(pass, st);
dispatch(st, pass, replay, c);
}
} finally {
computeBinder.endBatchedComputeDispatch(pass);
}
} finally {
computeBinder.setReadWriteStorageBufferOverrides(null);
computeBinder.setUniformBlockOverrides(null);
System.arraycopy(savedImages, 0, st.boundStorageTextureByUnit, 0, imageCount);
System.arraycopy(savedLevels, 0, st.boundStorageTextureLevel, 0, imageCount);
System.arraycopy(savedAccess, 0, st.boundStorageTextureAccess, 0, imageCount);
for (int i = 0; i < replay.samplerUnits.length; i++) {
st.boundTextures[replay.samplerUnits[i]] = savedTextures[i];
samplerOverrides[replay.samplerUnits[i]] = 0L;
}
computeBinder.setSamplerOverrides(null);
}
}
private boolean applySamplerTextures(ContextState st, Replay replay, Capture c) {
boolean changed = false;
for (int i = 0; i < replay.samplerUnits.length; i++) {
final int unit = replay.samplerUnits[i];
if (st.boundTextures[unit] != c.samplerTextures[i] || samplerOverrides[unit] != c.samplerHandles[i]) {
st.boundTextures[unit] = c.samplerTextures[i];
samplerOverrides[unit] = c.samplerHandles[i];
changed = true;
}
}
return changed;
}
private void skipUniforms(Replay replay, int start, int end) {
replay.lastHashesApplied = false;
for (int i = start; i < end; i++) {
for (int u = 0; u < replay.uniformSources.length; u++) valueCursor += Math.max(0, uniformLengths[uniformCursor++]);
}
}
private void dispatch(ContextState st, long pass, Replay replay, Capture c) {
Arrays.fill(handlesByGlSlot, 0L);
for (int i = 0; i < replay.packStorageSlots.length; i++) handlesByGlSlot[replay.packStorageSlots[i]] = c.packStorage[i];
System.arraycopy(c.buffers, 0, handlesByGlSlot, replay.setup.vertexBufferBinding(), c.bufferCount);
handlesByGlSlot[replay.setup.indexBufferBinding()] = c.indexBuffer;
final boolean bound = computeBinder.bindRoStorageBufferHandles(pass, st, handlesByGlSlot);
applyUniforms(st, replay);
if (!bound) return;
final int inputs = replay.inputLocations.length;
putFloats(st, replay.locAttr, c.attr, inputs * 4);
putFloats(st, replay.locAttrDefault, c.attrDefault, inputs * 4);
putIntVector(st, replay.locDraw, 4, c.first, c.count, c.instances, c.baseVertex);
putIntVector(st, replay.locIndex, 3, c.indexSize, c.indexOffset, c.indexCount, 0);
putIntVector(st, replay.locRestart, 2, c.restart ? 1 : 0, c.restartIndex, 0, 0);
computeBinder.setUniformBlockOverrides(c.uniformBlocks);
InvocationDispatch.dispatch(computeBinder, st, pass, (long) c.count * c.instances, replay.locInvocationBase, invocationBaseWriter);
}
private void putFloats(ContextState st, int location, float[] source, int len) {
if (location < 0 || len == 0) return;
final float[] value = pipelineApplier.reuseOrAlloc(st, location, len);
if (value == null) return;
System.arraycopy(source, 0, value, 0, len);
pipelineApplier.putUniform(st, location, value);
}
private void applyUniforms(ContextState st, Replay replay) {
for (int u = 0; u < replay.uniformSources.length; u++) {
final int len = uniformLengths[uniformCursor++];
if (len <= 0) continue;
final float[] value = pipelineApplier.reuseOrAlloc(st, replay.uniformTargets[u], len);
if (value != null) {
System.arraycopy(uniformValues, valueCursor, value, 0, len);
pipelineApplier.putUniform(st, replay.uniformTargets[u], value);
}
valueCursor += len;
}
}
private static int imageAccess(int index, int writtenImageCount) {
return index < writtenImageCount ? GL15.GL_READ_WRITE : GL15.GL_READ_ONLY;
}
private void putIntVector(ContextState st, int location, int count, int x, int y, int z, int w) {
if (location < 0) return;
final float[] value = pipelineApplier.reuseOrAlloc(st, location, count);
if (value == null) return;
value[0] = Float.intBitsToFloat(x);
if (count > 1) value[1] = Float.intBitsToFloat(y);
if (count > 2) value[2] = Float.intBitsToFloat(z);
if (count > 3) value[3] = Float.intBitsToFloat(w);
pipelineApplier.putUniform(st, location, value);
}
private void warnOnce(String key, String message) {
if (warned.add(key)) LOG.warn(message);
}
}
@@ -4,8 +4,7 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
public final class VoxelizationDispatcher {
static final int MAX_GROUPS_PER_DISPATCH = 65535;
private static final int WORKGROUP_SIZE = 64;
private static final InvocationDispatch.IntUniformWriter UNIFORM_1I = (st, location, value) -> GLStateManager.glUniform1i(location, value);
private final ComputeDispatchSink computeBinder;
@@ -25,12 +24,7 @@ public final class VoxelizationDispatcher {
if (locBase >= 0) GLStateManager.glUniform1i(locBase, rangeBase);
if (locCount >= 0) GLStateManager.glUniform1i(locCount, rangeCount);
if (locTotal >= 0) GLStateManager.glUniform1i(locTotal, vertexTotal);
final int groups = (vertexTotal + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE;
for (int g0 = 0; g0 < groups; g0 += MAX_GROUPS_PER_DISPATCH) {
if (locInvBase >= 0) GLStateManager.glUniform1i(locInvBase, g0 * WORKGROUP_SIZE);
computeBinder.pushPendingComputeUniforms(st);
computeBinder.dispatchInBatch(pass, Math.min(MAX_GROUPS_PER_DISPATCH, groups - g0), 1, 1);
}
InvocationDispatch.dispatch(computeBinder, st, pass, vertexTotal, locInvBase, UNIFORM_1I);
}
public void endBatch(long pass) {
@@ -5,7 +5,9 @@ import com.gtnewhorizons.angelica.glsm.backend.BackendOptions;
import com.gtnewhorizons.angelica.glsm.backend.MoltenVK;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.sdlgpu.compute.ImageAtomicsProbe;
import com.gtnewhorizons.angelica.sdlgpu.util.DebugMessageRelay;
import it.unimi.dsi.fastutil.longs.Long2BooleanOpenHashMap;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.sdl.SDLError;
@@ -49,6 +51,9 @@ public final class Device {
private String driverName;
private boolean fencePollEnabled;
private boolean fenceQueryInverted;
private int defaultMslVersion;
private ImageAtomicsProbe imageAtomicsProbe;
private final Long2BooleanOpenHashMap imageAtomicsBySize = new Long2BooleanOpenHashMap();
private final FenceReleaser fenceReleaser = new FenceReleaser(fence -> device == 0 || FenceWait.isSignaled(this, fence), fence -> { if (device != 0) SDL_ReleaseGPUFence(device, fence); });
private String deviceName;
private String driverVersion;
@@ -153,10 +158,36 @@ public final class Device {
}
probeFences(ver);
readMetalLanguageVersion();
return true;
}
private void readMetalLanguageVersion() {
if (!isMetal(driverName)) return;
defaultMslVersion = ImageAtomicsProbe.defaultMslVersion();
if (metalTextureAtomics()) {
LOG.info("Metal image atomics: MSL 3.1 available, checked per image size");
} else {
LOG.info("Metal image atomics: unavailable, this Java runtime compiles Metal shaders as MSL {}.{} and they need 3.1. A runtime built with the macOS 14 SDK or newer enables them.", defaultMslVersion >>> 16, defaultMslVersion & 0xFFFF);
}
}
public boolean metalTextureAtomics() {
return defaultMslVersion >= ImageAtomicsProbe.MTL_LANGUAGE_VERSION_3_1;
}
public boolean supportsImageAtomics(int width, int height, int depth) {
if (!metalTextureAtomics() || width <= 0 || height <= 0 || depth > 1) return false;
final long key = (long) width << 32 | height;
if (imageAtomicsBySize.containsKey(key)) return imageAtomicsBySize.get(key);
if (imageAtomicsProbe == null) imageAtomicsProbe = new ImageAtomicsProbe(device);
final boolean ok = imageAtomicsProbe.run(width, height);
if (ok) LOG.info("Metal image atomics on {}x{}: ok", width, height);
imageAtomicsBySize.put(key, ok);
return ok;
}
private void probeFences(int ver) {
fencePollEnabled = FenceWait.pollEnabled(driverName, ver);
final boolean expectInverted = isMetal(driverName) && ver == SDLVersion.SDL_VERSIONNUM(3, 4, 14);
@@ -256,11 +287,15 @@ public final class Device {
claimed = false;
}
fenceReleaser.releaseAll();
if (imageAtomicsProbe != null) imageAtomicsProbe.release();
imageAtomicsProbe = null;
SDL_DestroyGPUDevice(device);
device = 0;
driverName = null;
fencePollEnabled = false;
fenceQueryInverted = false;
defaultMslVersion = 0;
imageAtomicsBySize.clear();
deviceName = null;
driverVersion = null;
driverInfo = null;
@@ -381,11 +381,14 @@ public final class FrameManager {
private volatile Runnable preRenderPassHook;
private volatile Runnable renderPassEndHook;
private volatile Runnable beforeSubmit;
private volatile Runnable afterPresent;
public void setPreRenderPassHook(Runnable hook) { this.preRenderPassHook = hook; }
public void setRenderPassEndHook(Runnable hook) { this.renderPassEndHook = hook; }
public void noteComputePassBegun() {
final FrameState f = frame();
f.computePassesThisFrame++;
@@ -472,6 +475,8 @@ public final class FrameManager {
f.currentColorTarget = 0;
f.currentDepthTarget = 0;
f.activeLayoutHash = 0;
final Runnable hook = renderPassEndHook;
if (hook != null) hook.run();
}
}
@@ -248,6 +248,7 @@ public final class ResourceManager {
}
private long createTextureCore(int glId, int glTarget, int sdlFormat, int glFormat, int width, int height, int depth, int levels, int usage) {
if (getTextureHandle(glId) != 0) beforeTextureReplaced();
usage = reconcileUsageAndWarn(glId, usage);
final int sdlType = mapTextureType(glTarget);
if (!SDL_GPUTextureSupportsFormat(device.getDevice(), sdlFormat, sdlType, usage)) {
@@ -309,7 +310,19 @@ public final class ResourceManager {
return sdlFormat;
}
private Runnable textureReplaceHook;
public void setTextureReplaceHook(Runnable hook) {
textureReplaceHook = hook;
}
private void beforeTextureReplaced() {
final Runnable hook = textureReplaceHook;
if (hook != null) hook.run();
}
public void deleteTexture(int glId) {
beforeTextureReplaced();
final long handle;
wLock.lock();
try {
@@ -340,6 +353,7 @@ public final class ResourceManager {
}
public void releaseTextureHandleForRealloc(int glId) {
beforeTextureReplaced();
final long handle;
wLock.lock();
try {
@@ -43,8 +43,13 @@ public final class SamplerBinder {
}
public long getSamplerForUnit(ContextState st, int glUnit, int glTexId) {
if (glUnit >= 0 && glUnit < st.boundSamplerObjects.length && st.boundSamplerObjects[glUnit] != 0) {
final TextureSamplerState ss = resourceManager.getSamplerObject(st.boundSamplerObjects[glUnit]);
final int samplerObject = glUnit >= 0 && glUnit < st.boundSamplerObjects.length ? st.boundSamplerObjects[glUnit] : 0;
return getSampler(samplerObject, glTexId);
}
public long getSampler(int samplerObject, int glTexId) {
if (samplerObject != 0) {
final TextureSamplerState ss = resourceManager.getSamplerObject(samplerObject);
if (ss != null) return getOrCreateSdlSampler(ss);
}
if (glTexId != 0) return getSamplerForTexture(glTexId);
@@ -1,9 +1,7 @@
package com.gtnewhorizons.angelica.sdlgpu.shader;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.taumc.glsl.grammar.GLSLParser;
import org.taumc.glsl.grammar.GLSLParserBaseListener;
import java.util.List;
@@ -14,22 +12,15 @@ public final class ClipZRemap {
private ClipZRemap() {}
public static void collectEdits(GLSLParser.Translation_unitContext root, List<Edit> edits) {
final int[] mainCloseBraceIdx = { -1 };
ParseTreeWalker.DEFAULT.walk(new GLSLParserBaseListener() {
@Override
public void enterFunction_definition(GLSLParser.Function_definitionContext ctx) {
if (mainCloseBraceIdx[0] >= 0) return;
final GLSLParser.Function_prototypeContext proto = ctx.function_prototype();
if (proto == null || proto.IDENTIFIER() == null) return;
if (!"main".equals(proto.IDENTIFIER().getText())) return;
final GLSLParser.Compound_statement_no_new_scopeContext body = ctx.compound_statement_no_new_scope();
if (body == null || body.RIGHT_BRACE() == null) return;
mainCloseBraceIdx[0] = body.RIGHT_BRACE().getSymbol().getStartIndex();
}
}, root);
if (mainCloseBraceIdx[0] < 0) return;
final int idx = mainCloseBraceIdx[0];
edits.add(new Edit(idx, idx - 1, INJECTION_STD));
for (GLSLParser.External_declarationContext external : root.external_declaration()) {
final GLSLParser.Function_definitionContext function = external.function_definition();
if (function == null || function.function_prototype().IDENTIFIER() == null) continue;
if (!"main".equals(function.function_prototype().IDENTIFIER().getText())) continue;
final GLSLParser.Compound_statement_no_new_scopeContext body = function.compound_statement_no_new_scope();
if (body == null || body.RIGHT_BRACE() == null) return;
final int idx = body.RIGHT_BRACE().getSymbol().getStartIndex();
edits.add(new Edit(idx, idx - 1, INJECTION_STD));
return;
}
}
}
@@ -0,0 +1,398 @@
package com.gtnewhorizons.angelica.sdlgpu.shader;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
import com.gtnewhorizons.angelica.glsm.shader.ShaderIndex;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ParseTree;
import org.jetbrains.annotations.Nullable;
import org.taumc.glsl.grammar.GLSLLexer;
import org.taumc.glsl.grammar.GLSLParser;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.startIdx;
import static com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.stopIdx;
/**
* Moves {@code dFdx}/{@code dFdy}/{@code fwidth} calls in front of the {@code if} blocks around them, so every section of
* the 2x2 quad computes them. Packs take derivatives inside branches that only part of a quad enters, which GLSL
* leaves undefined. ACO writes {@code src - src[lane0]} in the active lanes only, then reads lane 1/2 with
* fetch-inactive, so a neighbor that skipped the branch hands back a stale register, often NaN!
* This was observed on Euphoria Patches and this aims to fix this wherever possible on Vulkan.
*/
public final class DerivativeHoisting {
private static final String PREFIX = "angelica_derivative_";
private static final Set<String> DERIVATIVES = Set.of(
"dFdx", "dFdy", "fwidth",
"dFdxFine", "dFdyFine", "fwidthFine",
"dFdxCoarse", "dFdyCoarse", "fwidthCoarse");
private static final Map<String, Set<Integer>> BUILTIN_OUT_PARAMS = Map.of(
"modf", Set.of(1),
"frexp", Set.of(1),
"uaddCarry", Set.of(2),
"usubBorrow", Set.of(2),
"umulExtended", Set.of(2, 3),
"imulExtended", Set.of(2, 3));
private static final String UNKNOWN = "*";
private DerivativeHoisting() {}
private record Declared(String type, boolean global) {}
private record Operand(String name, @Nullable String swizzle) {}
private record DerivativeCall(GLSLParser.Postfix_expressionContext call, String callee) {}
private record Effects(Set<String> declared, Set<String> written, Set<String> called) {
Effects() {
this(new HashSet<>(), new HashSet<>(), new HashSet<>());
}
}
public static void collectEdits(ShaderIndex index, List<Edit> edits) {
final List<DerivativeCall> calls = new ArrayList<>();
for (String callee : DERIVATIVES) {
for (GLSLParser.Postfix_expressionContext call : index.callsTo(callee)) calls.add(new DerivativeCall(call, callee));
}
if (calls.isEmpty()) return;
calls.sort(Comparator.comparingInt(c -> startIdx(c.call())));
final String source = index.source();
final GLSLParser.Translation_unitContext root = index.root();
final Functions functions = new Functions(index);
final int earlierEditCount = edits.size();
final Map<GLSLParser.Selection_statementContext, Effects> ifEffects = new HashMap<>();
final Map<GLSLParser.Selection_statementContext, StringBuilder> declarations = new LinkedHashMap<>();
final Map<String, String> hoisted = new HashMap<>();
boolean checkedDirectives = false;
for (DerivativeCall derivative : calls) {
final GLSLParser.Postfix_expressionContext call = derivative.call();
final GLSLParser.Function_call_parametersContext params = call.function_call_parameters();
if (params == null || params.assignment_expression().size() != 1) continue;
final GLSLParser.Assignment_expressionContext arg = params.assignment_expression(0);
final Operand operand = operand(arg);
if (operand == null) continue;
final Declared declared = declaredType(call, operand.name(), root);
if (declared == null) continue;
final String type = resultType(declared.type(), operand.swizzle());
if (type == null) continue;
final GLSLParser.Selection_statementContext target = outermostSafeIf(call, operand.name(), declared.global(), functions, ifEffects);
if (target == null) continue;
if (!checkedDirectives) {
if (index.directives().hasConditionalOrMacro()) return;
checkedDirectives = true;
}
final String callee = derivative.callee();
final String argText = sliceWithEdits(source, startIdx(arg), stopIdx(arg), edits.subList(0, earlierEditCount));
final String key = startIdx(target) + "|" + callee + "|" + argText;
String variable = hoisted.get(key);
if (variable == null) {
variable = PREFIX + hoisted.size();
hoisted.put(key, variable);
declarations.computeIfAbsent(target, k -> new StringBuilder())
.append(type).append(' ').append(variable).append(" = ").append(callee).append('(').append(argText).append(");\n")
.append(indentBefore(source, startIdx(target)));
}
edits.add(new Edit(startIdx(call), stopIdx(call), variable));
}
for (Map.Entry<GLSLParser.Selection_statementContext, StringBuilder> e : declarations.entrySet()) {
final int at = startIdx(e.getKey());
edits.add(new Edit(at, at - 1, e.getValue().toString()));
}
}
private static final class Functions {
final Map<String, Set<Integer>> outParams = new HashMap<>();
final Set<String> names = new HashSet<>();
final Map<GLSLParser.Postfix_expressionContext, String> calleeNames = new HashMap<>();
private final List<GLSLParser.Function_definitionContext> definitions = new ArrayList<>();
private @Nullable Map<String, Set<String>> globalWrites;
Functions(ShaderIndex index) {
for (GLSLParser.External_declarationContext external : index.root().external_declaration()) {
final GLSLParser.Function_prototypeContext prototype;
if (external.function_definition() != null) {
definitions.add(external.function_definition());
prototype = external.function_definition().function_prototype();
} else if (external.declaration() != null) {
prototype = external.declaration().function_prototype();
} else {
prototype = null;
}
if (prototype == null || prototype.IDENTIFIER() == null) continue;
final String name = prototype.IDENTIFIER().getText();
names.add(name);
final List<GLSLParser.Parameter_declarationContext> params = parameters(prototype);
for (int i = 0; i < params.size(); i++) {
final GLSLParser.Type_qualifierContext q = params.get(i).type_qualifier();
if (ShaderIndex.hasStorageQualifier(q, "out") || ShaderIndex.hasStorageQualifier(q, "inout")) {
outParams.computeIfAbsent(name, k -> new HashSet<>()).add(i);
}
}
}
for (String name : names) addCalls(index, name);
for (String name : BUILTIN_OUT_PARAMS.keySet()) addCalls(index, name);
}
private void addCalls(ShaderIndex index, String name) {
for (GLSLParser.Postfix_expressionContext call : index.callsTo(name)) calleeNames.put(call, name);
}
Set<String> globalWrites(String function) {
if (globalWrites == null) globalWrites = summarize();
return globalWrites.getOrDefault(function, Set.of());
}
private Map<String, Set<String>> summarize() {
final Map<String, Set<String>> writes = new HashMap<>();
final Map<String, Set<String>> callees = new HashMap<>();
for (GLSLParser.Function_definitionContext definition : definitions) {
final String name = definition.function_prototype().IDENTIFIER().getText();
final Effects effects = new Effects();
collectEffects(definition.compound_statement_no_new_scope(), this, effects);
for (GLSLParser.Parameter_declarationContext param : parameters(definition.function_prototype())) {
final String parameter = parameterName(param);
if (parameter != null) effects.written().remove(parameter);
}
writes.computeIfAbsent(name, k -> new HashSet<>()).addAll(effects.written());
callees.computeIfAbsent(name, k -> new HashSet<>()).addAll(effects.called());
}
boolean changed = true;
while (changed) {
changed = false;
for (Map.Entry<String, Set<String>> e : callees.entrySet()) {
final Set<String> into = writes.get(e.getKey());
for (String callee : e.getValue()) {
final Set<String> from = writes.get(callee);
if (from != null) changed |= into.addAll(from);
}
}
}
return writes;
}
}
private static @Nullable Operand operand(GLSLParser.Assignment_expressionContext arg) {
if (arg.assignment_operator() != null) return null;
final GLSLParser.Constant_expressionContext c = arg.constant_expression();
if (c == null || c.QUESTION() != null) return null;
final GLSLParser.Unary_expressionContext u = c.binary_expression().unary_expression();
if (u == null || u.postfix_expression() == null) return null;
final GLSLParser.Postfix_expressionContext p = u.postfix_expression();
final String name = variableName(p);
if (name != null) return new Operand(name, null);
if (p.DOT() == null || p.field_selection() == null || p.field_selection().variable_identifier() == null) return null;
final String base = variableName(p.postfix_expression());
return base == null ? null : new Operand(base, p.field_selection().variable_identifier().getText());
}
private static @Nullable String variableName(@Nullable GLSLParser.Postfix_expressionContext p) {
if (p == null || p.primary_expression() == null || p.primary_expression().variable_identifier() == null) return null;
return p.primary_expression().variable_identifier().getText();
}
private static @Nullable String resultType(String declaredType, @Nullable String swizzle) {
final int size = switch (declaredType) {
case "float" -> 1;
case "vec2" -> 2;
case "vec3" -> 3;
case "vec4" -> 4;
default -> 0;
};
if (size == 0) return null;
if (swizzle == null) return declaredType;
if (size == 1 || swizzle.isEmpty() || swizzle.length() > 4) return null;
for (String set : new String[] { "xyzw", "rgba", "stpq" }) {
boolean all = true;
for (int i = 0; i < swizzle.length(); i++) {
final int component = set.indexOf(swizzle.charAt(i));
if (component < 0 || component >= size) { all = false; break; }
}
if (all) return swizzle.length() == 1 ? "float" : "vec" + swizzle.length();
}
return null;
}
private static @Nullable Declared declaredType(ParserRuleContext from, String name, GLSLParser.Translation_unitContext root) {
ParseTree child = from;
for (ParserRuleContext p = from.getParent(); p != null; child = p, p = p.getParent()) {
if (p instanceof GLSLParser.Statement_listContext list) {
for (int i = list.children.indexOf(child) - 1; i >= 0; i--) {
final String type = typeOf(list.getChild(i), name);
if (type != null) return new Declared(type, false);
}
} else if (p instanceof GLSLParser.Iteration_statementContext loop && loop.for_init_statement() != null && child != loop.for_init_statement()) {
final String type = typeOf(loop.for_init_statement(), name);
if (type != null) return new Declared(type, false);
} else if (p instanceof GLSLParser.Function_definitionContext function) {
for (GLSLParser.Parameter_declarationContext param : parameters(function.function_prototype())) {
if (name.equals(parameterName(param))) {
final GLSLParser.Parameter_declaratorContext d = param.parameter_declarator();
return new Declared(d.array_specifier() == null ? baseType(d.type_specifier()) : "", false);
}
}
break;
}
}
for (GLSLParser.External_declarationContext external : root.external_declaration()) {
if (external.declaration() == null) continue;
final String type = typeOf(external.declaration(), name);
if (type != null) return new Declared(type, true);
}
return null;
}
private static @Nullable String typeOf(ParseTree node, String name) {
final GLSLParser.Init_declarator_listContext list = firstDeclaratorList(node);
if (list == null || list.single_declaration() == null) return null;
for (GLSLParser.Typeless_declarationContext d : ShaderIndex.declarators(list)) {
if (d.IDENTIFIER() == null || !name.equals(d.IDENTIFIER().getText())) continue;
if (d.array_specifier() != null) return "";
return baseType(list.single_declaration().fully_specified_type().type_specifier());
}
return null;
}
private static GLSLParser.@Nullable Init_declarator_listContext firstDeclaratorList(ParseTree node) {
ParseTree n = node;
while (n != null) {
if (n instanceof GLSLParser.Init_declarator_listContext list) return list;
if (n instanceof GLSLParser.Simple_statementContext s) n = s.declaration_statement();
else if (n instanceof GLSLParser.StatementContext s) n = s.simple_statement();
else if (n instanceof GLSLParser.For_init_statementContext f) n = f.declaration_statement();
else if (n instanceof GLSLParser.Declaration_statementContext d) n = d.declaration();
else if (n instanceof GLSLParser.DeclarationContext d) n = d.init_declarator_list();
else return null;
}
return null;
}
private static String baseType(GLSLParser.Type_specifierContext type) {
if (type == null || type.array_specifier() != null || type.type_specifier_nonarray() == null) return "";
return type.type_specifier_nonarray().getText();
}
private static List<GLSLParser.Parameter_declarationContext> parameters(GLSLParser.Function_prototypeContext prototype) {
final GLSLParser.Function_parametersContext params = prototype.function_parameters();
return params == null ? List.of() : params.parameter_declaration();
}
private static @Nullable String parameterName(GLSLParser.Parameter_declarationContext param) {
final GLSLParser.Parameter_declaratorContext d = param.parameter_declarator();
return d == null || d.IDENTIFIER() == null ? null : d.IDENTIFIER().getText();
}
private static GLSLParser.@Nullable Selection_statementContext outermostSafeIf(ParserRuleContext call, String name, boolean global, Functions functions,
Map<GLSLParser.Selection_statementContext, Effects> ifEffects) {
final List<GLSLParser.Selection_statementContext> enclosing = new ArrayList<>();
for (ParserRuleContext p = call.getParent(); p != null; p = p.getParent()) {
if (p instanceof GLSLParser.Selection_rest_statementContext rest) {
enclosing.add((GLSLParser.Selection_statementContext) rest.getParent());
} else if (p instanceof GLSLParser.Iteration_statementContext || p instanceof GLSLParser.Switch_statementContext
|| p instanceof GLSLParser.Function_definitionContext) {
break;
}
}
for (int i = enclosing.size() - 1; i >= 0; i--) {
final GLSLParser.Selection_statementContext candidate = enclosing.get(i);
if (!inStatementList(candidate)) continue;
Effects effects = ifEffects.get(candidate);
if (effects == null) {
effects = new Effects();
collectEffects(candidate, functions, effects);
ifEffects.put(candidate, effects);
}
if (effects.declared().contains(name) || writes(effects, name, global, functions)) continue;
return candidate;
}
return null;
}
private static boolean inStatementList(GLSLParser.Selection_statementContext s) {
return s.getParent() instanceof GLSLParser.Simple_statementContext simple
&& simple.getParent() instanceof GLSLParser.StatementContext statement
&& statement.getParent() instanceof GLSLParser.Statement_listContext list
&& !(list.getParent() instanceof GLSLParser.Switch_statementContext);
}
private static boolean writes(Effects effects, String name, boolean global, Functions functions) {
if (effects.written().contains(name) || effects.written().contains(UNKNOWN)) return true;
if (!global) return false;
for (String callee : effects.called()) {
final Set<String> w = functions.globalWrites(callee);
if (w.contains(name) || w.contains(UNKNOWN)) return true;
}
return false;
}
private static void collectEffects(ParseTree node, Functions functions, Effects effects) {
final Set<String> written = effects.written();
if (node instanceof GLSLParser.Typeless_declarationContext d && d.IDENTIFIER() != null) {
effects.declared().add(d.IDENTIFIER().getText());
} else if (node instanceof GLSLParser.ConditionContext c && c.IDENTIFIER() != null) {
effects.declared().add(c.IDENTIFIER().getText());
} else if (node instanceof GLSLParser.Assignment_expressionContext a && a.assignment_operator() != null) {
written.add(lvalueName(a.unary_expression()));
} else if (node instanceof GLSLParser.Unary_expressionContext u && (u.INC_OP() != null || u.DEC_OP() != null)) {
written.add(lvalueName(u.unary_expression()));
} else if (node instanceof GLSLParser.Postfix_expressionContext p) {
if (p.INC_OP() != null || p.DEC_OP() != null) written.add(lvalueName(p.postfix_expression()));
final String callee = functions.calleeNames.get(p);
if (callee != null) {
if (functions.names.contains(callee)) effects.called().add(callee);
Set<Integer> positions = functions.outParams.get(callee);
if (positions == null) positions = BUILTIN_OUT_PARAMS.get(callee);
if (positions != null && p.function_call_parameters() != null) {
final List<GLSLParser.Assignment_expressionContext> args = p.function_call_parameters().assignment_expression();
for (int i : positions) {
if (i < args.size()) written.add(lvalueName(args.get(i)));
}
}
}
}
for (int i = 0; i < node.getChildCount(); i++) {
collectEffects(node.getChild(i), functions, effects);
}
}
private static String lvalueName(@Nullable ParserRuleContext lvalue) {
if (lvalue == null) return UNKNOWN;
final Token start = lvalue.getStart();
return start != null && start.getType() == GLSLLexer.IDENTIFIER ? start.getText() : UNKNOWN;
}
private static String indentBefore(String source, int at) {
int lineStart = at;
while (lineStart > 0 && (source.charAt(lineStart - 1) == ' ' || source.charAt(lineStart - 1) == '\t')) lineStart--;
return source.substring(lineStart, at);
}
private static String sliceWithEdits(String source, int start, int stop, List<Edit> edits) {
final List<Edit> inside = new ArrayList<>();
for (Edit e : edits) {
if (e.startIdx() >= start && e.startIdx() <= stop && e.stopIdx() <= stop) {
inside.add(new Edit(e.startIdx() - start, e.stopIdx() - start, e.replacement()));
}
}
return GlslVulkanPreprocess.applyEdits(source.substring(start, stop + 1), inside);
}
}
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.sdlgpu.shader;
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
import com.gtnewhorizons.angelica.glsm.shader.ShaderIndex;
import org.antlr.v4.runtime.Token;
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
import org.taumc.glsl.grammar.GLSLParser;
@@ -112,11 +113,8 @@ public final class LogicOpLowering {
final int vecSize = floatVecSize(ts.type_specifier_nonarray().getText());
final int typeArrayLen = ts.array_specifier() != null ? arrayLen(ts.array_specifier(), program) : 0;
final List<GLSLParser.Typeless_declarationContext> names = new ArrayList<>();
if (single.typeless_declaration() != null) names.add(single.typeless_declaration());
names.addAll(idl.typeless_declaration());
int nextLocation = location;
for (GLSLParser.Typeless_declarationContext td : names) {
for (GLSLParser.Typeless_declarationContext td : ShaderIndex.declarators(idl)) {
final int len = td.array_specifier() != null ? arrayLen(td.array_specifier(), program) : typeArrayLen;
outputs.add(new Output(td.IDENTIFIER().getText(), nextLocation, len, vecSize));
if (nextLocation >= 0) nextLocation += Math.max(len, 1);
@@ -3,13 +3,9 @@ package com.gtnewhorizons.angelica.sdlgpu.shader;
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock.Member;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
import com.gtnewhorizons.angelica.glsm.shader.ShaderIndex;
import com.gtnewhorizons.angelica.glsm.shader.UniformType;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.taumc.glsl.grammar.GLSLLexer;
import org.taumc.glsl.grammar.GLSLParser;
import org.taumc.glsl.grammar.GLSLParserBaseListener;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
@@ -28,13 +24,13 @@ public final class PerFrameBlockInjector {
private PerFrameBlockInjector() {}
public static void collectEdits(GLSLParser.Translation_unitContext root, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass, List<Edit> out) {
public static void collectEdits(ShaderIndex index, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass, List<Edit> out) {
final PerFrameUniformBlock[] blocks = new PerFrameUniformBlock[ShaderManager.BLOCK_COUNT];
blocks[ShaderManager.BLOCK_PER_FRAME] = normalize(perFrame);
blocks[ShaderManager.BLOCK_PER_PASS] = normalize(perPass);
if (blocks[0] == null && blocks[1] == null) return;
final int insertAt = firstDeclarationStart(root);
final int insertAt = firstDeclarationStart(index.root());
if (insertAt < 0) return;
final Map<String, String> memberTypes = new LinkedHashMap<>();
@@ -51,9 +47,12 @@ public final class PerFrameBlockInjector {
final List<Edit> stripEdits = new ArrayList<>();
final IntArrayList stripBlocks = new IntArrayList();
final Set<String> shadowed = new HashSet<>();
classifyDeclarations(root, memberTypes, memberBlock, stripEdits, stripBlocks, shadowed);
classifyDeclarations(index, memberTypes, memberBlock, stripEdits, stripBlocks, shadowed);
final Set<String> referenced = referencedMembers(root, memberTypes.keySet(), shadowed);
final Set<String> referenced = new HashSet<>();
for (String name : memberTypes.keySet()) {
if (!shadowed.contains(name) && !index.occurrences(name).isEmpty()) referenced.add(name);
}
final boolean[] injected = new boolean[blocks.length];
boolean anyInjected = false;
for (int b = 0; b < blocks.length; b++) {
@@ -108,11 +107,8 @@ public final class PerFrameBlockInjector {
return (decls == null || decls.isEmpty()) ? -1 : decls.get(0).getStart().getStartIndex();
}
private static void classifyDeclarations(GLSLParser.Translation_unitContext root, Map<String, String> memberTypes, Object2IntOpenHashMap<String> memberBlock, List<Edit> stripEdits, IntArrayList stripBlocks, Set<String> shadowed) {
for (GLSLParser.External_declarationContext ed : root.external_declaration()) {
final GLSLParser.DeclarationContext ctx = ed.declaration();
if (ctx == null) continue;
private static void classifyDeclarations(ShaderIndex index, Map<String, String> memberTypes, Object2IntOpenHashMap<String> memberBlock, List<Edit> stripEdits, IntArrayList stripBlocks, Set<String> shadowed) {
for (GLSLParser.DeclarationContext ctx : index.globalDeclarations()) {
final List<String> declared = declaredNames(ctx);
if (declared.isEmpty()) continue;
@@ -141,16 +137,7 @@ public final class PerFrameBlockInjector {
final GLSLParser.Single_declarationContext single = idl.single_declaration();
if (single == null || single.fully_specified_type() == null) return false;
final GLSLParser.Fully_specified_typeContext fst = single.fully_specified_type();
if (fst.type_qualifier() == null) return false;
boolean hasUniform = false;
for (GLSLParser.Single_type_qualifierContext stq : fst.type_qualifier().single_type_qualifier()) {
if (stq.storage_qualifier() != null && "uniform".equals(stq.storage_qualifier().getText())) {
hasUniform = true;
break;
}
}
if (!hasUniform) return false;
if (!ShaderIndex.hasStorageQualifier(fst.type_qualifier(), "uniform")) return false;
if (fst.type_specifier() == null || fst.type_specifier().type_specifier_nonarray() == null) return false;
final String declaredType = fst.type_specifier().type_specifier_nonarray().getText();
@@ -160,30 +147,11 @@ public final class PerFrameBlockInjector {
return true;
}
private static Set<String> referencedMembers(GLSLParser.Translation_unitContext root, Set<String> memberNames, Set<String> shadowed) {
final Set<String> referenced = new HashSet<>();
ParseTreeWalker.DEFAULT.walk(new GLSLParserBaseListener() {
@Override
public void visitTerminal(TerminalNode node) {
final Token tok = node.getSymbol();
if (tok.getType() != GLSLLexer.IDENTIFIER) return;
final String name = tok.getText();
if (memberNames.contains(name) && !shadowed.contains(name)) referenced.add(name);
}
}, root);
return referenced;
}
private static List<String> declaredNames(GLSLParser.DeclarationContext ctx) {
final GLSLParser.Init_declarator_listContext idl = ctx.init_declarator_list();
if (idl == null) return List.of();
final GLSLParser.Single_declarationContext single = idl.single_declaration();
if (single == null) return List.of();
if (idl == null || idl.single_declaration() == null) return List.of();
final List<String> out = new ArrayList<>(2);
if (single.typeless_declaration() != null && single.typeless_declaration().IDENTIFIER() != null) {
out.add(single.typeless_declaration().IDENTIFIER().getText());
}
for (GLSLParser.Typeless_declarationContext td : idl.typeless_declaration()) {
for (GLSLParser.Typeless_declarationContext td : ShaderIndex.declarators(idl)) {
if (td.IDENTIFIER() != null) out.add(td.IDENTIFIER().getText());
}
return out;
@@ -1,20 +1,15 @@
package com.gtnewhorizons.angelica.sdlgpu.shader;
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
import com.gtnewhorizons.angelica.glsm.shader.ShaderIndex;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.taumc.glsl.grammar.GLSLLexer;
import org.jetbrains.annotations.Nullable;
import org.taumc.glsl.grammar.GLSLParser;
import org.taumc.glsl.grammar.GLSLParserBaseListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -24,79 +19,68 @@ public final class SamplerStripper {
private SamplerStripper() {}
public static void collectEdits(GLSLParser.Translation_unitContext root, String source, List<Edit> edits) {
record SamplerDecl(String name, int start, int stop) {}
private record SamplerDecl(List<String> names, int start, int stop) {}
public static void collectEdits(ShaderIndex index, List<Edit> edits) {
final List<SamplerDecl> samplers = new ArrayList<>();
ParseTreeWalker.DEFAULT.walk(new GLSLParserBaseListener() {
@Override
public void enterDeclaration(GLSLParser.DeclarationContext ctx) {
final GLSLParser.Init_declarator_listContext idl = ctx.init_declarator_list();
if (idl == null) return;
final GLSLParser.Single_declarationContext single = idl.single_declaration();
if (single == null || single.fully_specified_type() == null) return;
final GLSLParser.Fully_specified_typeContext fst = single.fully_specified_type();
if (fst.type_qualifier() == null) return;
boolean hasUniform = false;
for (GLSLParser.Single_type_qualifierContext stq : fst.type_qualifier().single_type_qualifier()) {
if (stq.storage_qualifier() != null && "uniform".equals(stq.storage_qualifier().getText())) {
hasUniform = true;
break;
}
}
if (!hasUniform) return;
if (fst.type_specifier() == null || fst.type_specifier().type_specifier_nonarray() == null) return;
final String typeText = fst.type_specifier().type_specifier_nonarray().getText();
if (!isSamplerType(typeText)) return;
final int start = ctx.getStart().getStartIndex();
final int stop = ctx.getStop().getStopIndex();
if (single.typeless_declaration() != null && single.typeless_declaration().IDENTIFIER() != null) {
samplers.add(new SamplerDecl(single.typeless_declaration().IDENTIFIER().getText(), start, stop));
}
for (GLSLParser.Typeless_declarationContext td : idl.typeless_declaration()) {
if (td.IDENTIFIER() != null) {
samplers.add(new SamplerDecl(td.IDENTIFIER().getText(), start, stop));
}
}
}
}, root);
for (GLSLParser.DeclarationContext ctx : index.globalDeclarations()) {
final SamplerDecl decl = samplerDeclaration(ctx);
if (decl != null) samplers.add(decl);
}
if (samplers.isEmpty()) return;
final Map<String, Integer> refCounts = new HashMap<>();
for (SamplerDecl s : samplers) refCounts.put(s.name(), 0);
ParseTreeWalker.DEFAULT.walk(new GLSLParserBaseListener() {
@Override
public void visitTerminal(TerminalNode node) {
final Token tok = node.getSymbol();
if (tok.getType() != GLSLLexer.IDENTIFIER) return;
final String name = tok.getText();
final Integer prior = refCounts.get(name);
if (prior == null) return;
final int pos = tok.getStartIndex();
for (SamplerDecl s : samplers) {
if (pos >= s.start() && pos <= s.stop()) return;
}
refCounts.put(name, prior + 1);
}
}, root);
final Set<String> directiveRefs = GlslTransformUtils.identifiersInDirectiveText(source);
Set<String> directiveRefs = null;
for (SamplerDecl s : samplers) {
if (refCounts.getOrDefault(s.name(), 0) != 0) continue;
if (directiveRefs.contains(s.name())) {
LOGGER.debug("Keeping sampler {}: referenced only inside a preprocessor block", s.name());
if (!allUnreferenced(s, samplers, index)) continue;
if (directiveRefs == null) directiveRefs = index.directives().identifiers();
if (anyIn(s, directiveRefs)) {
LOGGER.debug("Keeping sampler {}: referenced only inside a preprocessor block", s.names());
continue;
}
edits.add(new Edit(s.start(), s.stop(), ""));
}
}
private static @Nullable SamplerDecl samplerDeclaration(GLSLParser.DeclarationContext ctx) {
final GLSLParser.Init_declarator_listContext idl = ctx.init_declarator_list();
if (idl == null) return null;
final GLSLParser.Single_declarationContext single = idl.single_declaration();
if (single == null || single.fully_specified_type() == null) return null;
final GLSLParser.Fully_specified_typeContext fst = single.fully_specified_type();
if (!ShaderIndex.hasStorageQualifier(fst.type_qualifier(), "uniform")) return null;
if (fst.type_specifier() == null || fst.type_specifier().type_specifier_nonarray() == null) return null;
if (!isSamplerType(fst.type_specifier().type_specifier_nonarray().getText())) return null;
final List<String> names = new ArrayList<>(1);
for (GLSLParser.Typeless_declarationContext td : ShaderIndex.declarators(idl)) {
if (td.IDENTIFIER() != null) names.add(td.IDENTIFIER().getText());
}
return names.isEmpty() ? null : new SamplerDecl(names, ctx.getStart().getStartIndex(), ctx.getStop().getStopIndex());
}
private static boolean insideAny(List<SamplerDecl> samplers, int pos) {
for (SamplerDecl s : samplers) {
if (pos >= s.start() && pos <= s.stop()) return true;
}
return false;
}
private static boolean allUnreferenced(SamplerDecl s, List<SamplerDecl> samplers, ShaderIndex index) {
for (String name : s.names()) {
for (Token tok : index.occurrences(name)) {
if (!insideAny(samplers, tok.getStartIndex())) return false;
}
}
return true;
}
private static boolean anyIn(SamplerDecl s, Set<String> names) {
for (String name : s.names()) {
if (names.contains(name)) return true;
}
return false;
}
private static boolean isSamplerType(String t) {
return t.startsWith("sampler") || t.startsWith("isampler") || t.startsWith("usampler");
}
@@ -9,8 +9,10 @@ import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
import com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO;
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
import com.gtnewhorizons.angelica.glsm.shader.ShaderIndex;
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
import com.gtnewhorizons.angelica.glsm.threading.AngelicaWorkers;
import com.gtnewhorizons.angelica.sdlgpu.compute.VertexWriteReplayer;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileCache;
@@ -315,40 +317,40 @@ public final class ShaderManager {
LOG.warn("glsl-transformation-lib parse failed for '{}': {}", debugName, e.getMessage());
return null;
}
final List<Edit> edits = new ArrayList<>();
if (isGraphics(glShaderType)) {
PerFrameBlockInjector.collectEdits(root, perFrame, perPass, edits);
}
final GlslVulkanPreprocess.Metadata meta = GlslVulkanPreprocess.collectEdits(transformedSource, root, glShaderType, debugName, true, edits);
if (glShaderType == GL20.GL_VERTEX_SHADER) {
ClipZRemap.collectEdits(root, edits);
}
SamplerStripper.collectEdits(root, transformedSource, edits);
final String s = edits.isEmpty() ? transformedSource : GlslVulkanPreprocess.applyEdits(transformedSource, edits);
return new PrewarmTransformResult(s, meta.boolUniforms());
return transform(transformedSource, 0, root, glShaderType, debugName, perFrame, perPass);
}
public static PrewarmTransformResult applyPrewarmTransformsFull(String finalSource, GLSLParser.Translation_unitContext bodyTree, int headerLen, int glShaderType, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
final String header = finalSource.substring(0, headerLen);
final String body = finalSource.substring(headerLen);
return transform(finalSource, headerLen, bodyTree, glShaderType, "prewarm", perFrame, perPass);
}
private static PrewarmTransformResult transform(String source, int headerLen, GLSLParser.Translation_unitContext tree, int glShaderType, String debugName, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
final String body = source.substring(headerLen);
final ShaderIndex index = new ShaderIndex(tree, body);
final List<Edit> edits = new ArrayList<>();
if (isGraphics(glShaderType)) {
PerFrameBlockInjector.collectEdits(bodyTree, perFrame, perPass, edits);
PerFrameBlockInjector.collectEdits(index, perFrame, perPass, edits);
}
final GlslVulkanPreprocess.Metadata meta = GlslVulkanPreprocess.collectEdits(null, bodyTree, glShaderType, "prewarm", true, edits);
final GlslVulkanPreprocess.Metadata meta = GlslVulkanPreprocess.collectEdits(index, glShaderType, debugName, true, edits);
if (glShaderType == GL20.GL_VERTEX_SHADER) {
ClipZRemap.collectEdits(bodyTree, edits);
ClipZRemap.collectEdits(tree, edits);
}
SamplerStripper.collectEdits(bodyTree, body, edits);
String outHeader = header;
SamplerStripper.collectEdits(index, edits);
if (glShaderType == GL20.GL_FRAGMENT_SHADER) {
DerivativeHoisting.collectEdits(index, edits);
}
if (headerLen == 0) {
return new PrewarmTransformResult(GlslVulkanPreprocess.applyEdits(source, edits), meta.boolUniforms());
}
if (edits.isEmpty() && !meta.needsSamplerless()) {
return new PrewarmTransformResult(source, meta.boolUniforms());
}
String header = source.substring(0, headerLen);
if (meta.needsSamplerless()) {
final int nl = header.indexOf('\n');
outHeader = header.substring(0, nl) + "\n" + GlslVulkanPreprocess.SAMPLERLESS_EXTENSION + header.substring(nl);
header = header.substring(0, nl) + "\n" + GlslVulkanPreprocess.SAMPLERLESS_EXTENSION + header.substring(nl);
}
if (edits.isEmpty() && outHeader == header) {
return new PrewarmTransformResult(finalSource, meta.boolUniforms());
}
return new PrewarmTransformResult(outHeader + GlslVulkanPreprocess.applyEdits(body, edits), meta.boolUniforms());
return new PrewarmTransformResult(header + GlslVulkanPreprocess.applyEdits(body, edits), meta.boolUniforms());
}
public static void prewarmSpirv(String transformedSource, int glShaderType) {
@@ -1161,7 +1163,7 @@ public final class ShaderManager {
return new CrossCompiled(SDL_GPU_SHADERFORMAT_SPIRV, spirv, "main", false);
}
if (device.supportsMsl()) {
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, glShaderType);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, glShaderType, device.metalTextureAtomics());
return new CrossCompiled(SDL_GPU_SHADERFORMAT_MSL, out.code(), out.entrypoint(), true);
}
if (device.supportsDxbc()) {
@@ -2078,6 +2080,7 @@ public final class ShaderManager {
public int vertexShader;
public int vertexUboSize;
public long sdlComputePipeline;
public VertexWriteReplayer.Replay vertexWriteReplay;
public long sdlFragmentShader;
public long sdlVertexShader;
public long lastComputeCb;
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.sdlgpu.shader;
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
import com.gtnewhorizons.angelica.glsm.shader.ShaderIndex;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
@@ -60,16 +61,7 @@ public final class UscaledRetype {
final GLSLParser.Single_declarationContext single = idl.single_declaration();
if (single == null || single.fully_specified_type() == null) return;
final GLSLParser.Fully_specified_typeContext fst = single.fully_specified_type();
if (fst.type_qualifier() == null) return;
boolean isIn = false;
for (GLSLParser.Single_type_qualifierContext stq : fst.type_qualifier().single_type_qualifier()) {
if (stq.storage_qualifier() != null && "in".equals(stq.storage_qualifier().getText())) {
isIn = true;
break;
}
}
if (!isIn) return;
if (!ShaderIndex.hasStorageQualifier(fst.type_qualifier(), "in")) return;
if (fst.type_specifier() == null || fst.type_specifier().type_specifier_nonarray() == null) return;
if (single.typeless_declaration() == null || single.typeless_declaration().IDENTIFIER() == null) return;
@@ -150,18 +150,30 @@ public final class CrossCompileUtil {
return false;
}
public static boolean usesImageAtomics(ByteBuffer spirv) {
final IntBuffer w = spirv.asIntBuffer();
for (int i = SPIRV_HEADER_WORDS; i < w.limit(); i = nextInstruction(w, i)) {
if ((w.get(i) & 0xFFFF) == Spv.SpvOpImageTexelPointer) return true;
}
return false;
}
private static final int SPIRV_HEADER_WORDS = 5; // magic, version, generator, bound, schema
private static int nextInstruction(IntBuffer w, int i) {
final int len = w.get(i) >>> 16;
return len == 0 ? w.limit() : i + len;
}
public static int[] parseLocalSize(ByteBuffer spirv) {
final IntBuffer w = spirv.asIntBuffer();
final int wordCount = w.limit();
int i = 5; // 5-word SPIR-V header (magic, version, generator, bound, schema)
int lx = 1, ly = 1, lz = 1;
boolean foundLiteral = false;
boolean sawExecutionModeId = false;
while (i < wordCount) {
for (int i = SPIRV_HEADER_WORDS; i < w.limit(); i = nextInstruction(w, i)) {
final int word0 = w.get(i);
final int opcode = word0 & 0xFFFF;
final int instrLen = (word0 >>> 16) & 0xFFFF;
if (instrLen == 0) break;
final int instrLen = word0 >>> 16;
if (opcode == 16 && instrLen >= 6) { // SpvOpExecutionMode
final int mode = w.get(i + 2);
if (mode == 17) { // SpvExecutionModeLocalSize
@@ -173,7 +185,6 @@ public final class CrossCompileUtil {
} else if (opcode == 331) { // SpvOpExecutionModeId
sawExecutionModeId = true;
}
i += instrLen;
}
if (!foundLiteral && sawExecutionModeId) {
LOG.warn("SPIR-V uses OpExecutionModeId (spec-constant LocalSizeId); falling back to {{1,1,1}} - not supported by parseLocalSize");
@@ -26,7 +26,11 @@ public final class MslCrossCompile {
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
private static final String BACKEND = "MSL";
private static final CrossCompileCache CACHE = new CrossCompileCache("msl", CrossCompileUtil::spvcId, MslCrossCompile::crossCompileUncached);
private static final int SPVC_MSL_2_2 = mslVersion(2, 2, 0);
public static final int SPVC_MSL_3_1 = mslVersion(3, 1, 0);
private static final CrossCompileCache CACHE = new CrossCompileCache("msl", CrossCompileUtil::spvcId, (spirv, glShaderType) -> compileAtVersion(spirv, glShaderType, SPVC_MSL_2_2));
private static final CrossCompileCache CACHE_3_1 = new CrossCompileCache("msl31", CrossCompileUtil::spvcId, (spirv, glShaderType) -> compileAtVersion(spirv, glShaderType, SPVC_MSL_3_1));
@FunctionalInterface
private interface BufferSlotFn { public int slot(int set, int binding); }
@@ -36,8 +40,9 @@ public final class MslCrossCompile {
private MslCrossCompile() {}
public static Output compile(ByteBuffer spirv, int glShaderType) {
return CACHE.compile(spirv, glShaderType);
public static Output compile(ByteBuffer spirv, int glShaderType, boolean textureAtomics) {
final boolean atomics = textureAtomics && glShaderType == GL43.GL_COMPUTE_SHADER && CrossCompileUtil.usesImageAtomics(spirv);
return (atomics ? CACHE_3_1 : CACHE).compile(spirv, glShaderType);
}
private static int executionModelFor(int glShaderType) {
@@ -47,7 +52,7 @@ public final class MslCrossCompile {
throw new UnsupportedOperationException("Unsupported shader type for MSL cross-compile: 0x" + Integer.toHexString(glShaderType));
}
private static Output crossCompileUncached(ByteBuffer spirv, int glShaderType) {
public static Output compileAtVersion(ByteBuffer spirv, int glShaderType, int version) {
final int executionModel = executionModelFor(glShaderType);
final int dumpId = SystemProperties.dumpShaders() ? CrossCompileUtil.SHADER_DUMP_COUNTER.getAndIncrement() : -1;
if (dumpId >= 0) CrossCompileUtil.dumpSpirv(spirv, dumpId, glShaderType);
@@ -77,7 +82,7 @@ public final class MslCrossCompile {
throw CrossCompileUtil.spvcError(ctx, BACKEND, "create_compiler_options");
}
final long opts = pOpts.get(0);
Spvc.spvc_compiler_options_set_uint(opts, Spvc.SPVC_COMPILER_OPTION_MSL_VERSION, mslVersion(2, 2, 0));
Spvc.spvc_compiler_options_set_uint(opts, Spvc.SPVC_COMPILER_OPTION_MSL_VERSION, version);
Spvc.spvc_compiler_options_set_uint(opts, Spvc.SPVC_COMPILER_OPTION_MSL_PLATFORM, Spvc.SPVC_MSL_PLATFORM_MACOS);
if (Spvc.spvc_compiler_install_compiler_options(compiler, opts) != Spvc.SPVC_SUCCESS) {
throw CrossCompileUtil.spvcError(ctx, BACKEND, "install_compiler_options");
@@ -0,0 +1,7 @@
#version 460 core
layout(local_size_x = 64) in;
layout(binding = 0, r32i) uniform iimage2D counter;
void main() {
imageAtomicAdd(counter, ivec2(0), 1);
}
@@ -0,0 +1,35 @@
package com.gtnewhorizons.angelica.sdlgpu.compute;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.lwjgl.sdl.SDLGPU.SDL_GetGPUDeviceDriver;
class ImageAtomicsGpuTest {
private static SdlTestRig rig;
private static ImageAtomicsProbe probe;
@BeforeAll
static void setUp() throws Exception {
rig = SdlTestRig.acquireRealDevice();
assumeTrue("metal".equalsIgnoreCase(SDL_GetGPUDeviceDriver(rig.sdlHandle)), "native Metal only");
assumeTrue(ImageAtomicsProbe.defaultMslVersion() >= ImageAtomicsProbe.MTL_LANGUAGE_VERSION_3_1, "this process compiles Metal source below MSL 3.1");
probe = new ImageAtomicsProbe(rig.sdlHandle);
}
@AfterAll
static void tearDown() {
if (probe != null) probe.release();
SdlTestRig.releaseRealDevice();
}
@Test
void atomicsCountEveryInvocationOnAnImageTheSizeOfEuphoriasEndCrystalMap() {
assertTrue(probe.run(40, 10));
}
}
@@ -77,9 +77,9 @@ class VoxelizationDispatcherTest {
void dispatchRegion_splitsAboveMaxGroups() {
final RecordingSink sink = new RecordingSink();
final VoxelizationDispatcher d = new VoxelizationDispatcher(sink);
region(d, 1, VoxelizationDispatcher.MAX_GROUPS_PER_DISPATCH * 64 + 1, new ContextState());
region(d, 1, InvocationDispatch.MAX_GROUPS_PER_DISPATCH * 64 + 1, new ContextState());
assertEquals(2, sink.dispatches.size());
assertEquals(VoxelizationDispatcher.MAX_GROUPS_PER_DISPATCH, sink.dispatches.get(0)[0]);
assertEquals(InvocationDispatch.MAX_GROUPS_PER_DISPATCH, sink.dispatches.get(0)[0]);
assertEquals(1, sink.dispatches.get(1)[0]);
assertEquals(2, sink.uniformPushCount);
}
@@ -0,0 +1,62 @@
package com.gtnewhorizons.angelica.sdlgpu.device;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import it.unimi.dsi.fastutil.longs.Long2BooleanOpenHashMap;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
class DeviceImageAtomicsGpuTest {
private Device device;
@BeforeAll
static void initSdl() {
SdlTestRig.initSdlVideo();
}
@AfterEach
void tearDown() {
if (device != null) device.destroyDevice();
}
private Long2BooleanOpenHashMap verdicts() {
return Reflect.get(device, "imageAtomicsBySize");
}
@Test
void verdictsAreCachedPerSizeAndDroppedWithTheDevice() {
device = new Device();
assumeTrue(device.createDevice(), "no SDL GPU device on this machine");
assumeTrue(device.metalTextureAtomics(), "needs native Metal compiling at MSL 3.1 or newer");
assertFalse(device.supportsImageAtomics(40, 10, 2));
assertFalse(device.supportsImageAtomics(0, 10, 1));
assertFalse(device.supportsImageAtomics(40, -1, 1));
assertEquals(0, verdicts().size());
assertNull(Reflect.get(device, "imageAtomicsProbe"));
assertTrue(device.supportsImageAtomics(40, 10, 1));
assertFalse(device.supportsImageAtomics(64, 64, 1));
assertEquals(2, verdicts().size());
final Object probe = Reflect.get(device, "imageAtomicsProbe");
assertTrue(device.supportsImageAtomics(40, 10, 1));
assertFalse(device.supportsImageAtomics(64, 64, 1));
assertEquals(2, verdicts().size());
assertEquals(probe, Reflect.get(device, "imageAtomicsProbe"));
device.destroyDevice();
assertFalse(device.metalTextureAtomics());
assertFalse(device.supportsImageAtomics(40, 10, 1));
assertEquals(0, verdicts().size());
assertNull(Reflect.get(device, "imageAtomicsProbe"));
}
}
@@ -0,0 +1,179 @@
package com.gtnewhorizons.angelica.sdlgpu.shader;
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
import com.gtnewhorizons.angelica.glsm.shader.ShaderIndex;
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
import org.junit.jupiter.api.Test;
import org.lwjgl.util.shaderc.Shaderc;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.system.MemoryUtil.memFree;
class DerivativeHoistingTest {
private static String hoist(String source) {
final List<Edit> edits = new ArrayList<>();
DerivativeHoisting.collectEdits(new ShaderIndex(GlslTransformUtils.parseFullQuiet(source), source), edits);
return GlslVulkanPreprocess.applyEdits(source, edits);
}
private static void assertCompiles(String source) {
final SpirvCompiler.Result r = SpirvCompiler.compile(source, Shaderc.shaderc_fragment_shader, "derivatives.frag", SpirvCompiler.Options.vulkanForced460Core(), false);
assertNotNull(r.spirv(), () -> "compile failed: " + r.error() + "\n" + source);
memFree(r.spirv());
}
@Test
void directionalBlocklightDerivativesLeaveTheLightmapBranch() {
final String out = hoist("""
#version 420 core
in vec2 lmCoord;
in vec3 viewIn;
out vec4 color;
float light(vec3 viewPos) {
vec2 oldLightmap = lmCoord;
float result = oldLightmap.x;
if (oldLightmap.x > 0.035) {
vec2 dFdBlock = vec2(dFdx(oldLightmap.x), dFdy(oldLightmap.x));
vec3 dir = vec3(0.0);
if (length(dFdBlock) < 1e-6) {
dir = vec3(1.0);
} else {
dir = dFdx(viewPos) * dFdBlock.x + dFdy(viewPos) * dFdBlock.y;
}
result *= dot(normalize(dir), vec3(0.0, 1.0, 0.0));
}
return result;
}
void main() {
color = vec4(light(viewIn));
}
""");
final int branch = out.indexOf("if (oldLightmap.x > 0.035)");
assertTrue(branch > 0, out);
assertFalse(out.substring(branch).contains("dFdx("), out);
assertFalse(out.substring(branch).contains("dFdy("), out);
final String hoisted = out.substring(out.indexOf("float result"), branch);
assertTrue(hoisted.contains("float angelica_derivative_0 = dFdx(oldLightmap.x);"), out);
assertTrue(hoisted.contains("float angelica_derivative_1 = dFdy(oldLightmap.x);"), out);
assertTrue(hoisted.contains("vec3 angelica_derivative_2 = dFdx(viewPos);"), out);
assertTrue(hoisted.contains("vec3 angelica_derivative_3 = dFdy(viewPos);"), out);
assertCompiles(out);
}
@Test
void variableWrittenInsideTheBranchStaysPut() {
final String source = """
#version 420 core
in vec2 uv;
out vec4 color;
void bump(inout vec2 p) { p += 1.0; }
void main() {
vec2 a = uv;
vec2 b = uv;
if (uv.x > 0.5) {
a *= 2.0;
bump(b);
color = vec4(dFdx(a), dFdy(b));
}
}
""";
assertEquals(source, hoist(source));
}
@Test
void writesHiddenInsideDirectivesBlockTheMove() {
final String source = """
#version 420 core
in vec2 uv;
out vec4 color;
void main() {
vec2 p = uv;
if (true) {
#if 1
p *= 2.0;
#endif
color = vec4(dFdx(p), 0.0, 1.0);
}
}
""";
assertEquals(source, hoist(source));
}
@Test
void parenthesizedBuiltinOutArgumentCountsAsAWrite() {
final String source = """
#version 420 core
in vec2 uv;
out vec4 color;
void main() {
vec2 p = uv;
if (true) {
modf(uv * 2.0, (p));
color = vec4(dFdx(p), 0.0, 1.0);
}
}
""";
assertEquals(source, hoist(source));
}
@Test
void globalsStayOnlyWhenACalleeWritesThem() {
final String out = hoist("""
#version 420 core
in vec4 colorIn;
out vec4 color;
vec4 glColor = colorIn;
vec2 nudged = vec2(0.0);
float square(float x) { return x * x; }
void nudge() { nudged += 1.0; }
void touch() { nudge(); }
void main() {
if (glColor.a > 0.5) {
touch();
color = vec4(dFdx(glColor.a) + square(1.0), dFdy(nudged), 1.0);
}
}
""");
final int branch = out.indexOf("if (glColor.a > 0.5)");
assertTrue(out.substring(0, branch).contains("float angelica_derivative_0 = dFdx(glColor.a);"), out);
assertTrue(out.substring(branch).contains("dFdy(nudged)"), out);
assertCompiles(out);
}
@Test
void loopsAreNotCrossed() {
final String out = hoist("""
#version 420 core
in vec2 uv;
out vec4 color;
void main() {
color = vec4(0.0);
if (uv.y > 0.5) {
for (int i = 0; i < 4; i++) {
vec2 p = uv * float(i);
if (p.x > 1.0) {
color.xy += dFdx(p);
}
}
}
}
""");
final int loop = out.indexOf("for (");
final int inner = out.indexOf("if (p.x > 1.0)");
final int hoisted = out.indexOf("vec2 angelica_derivative_0 = dFdx(p);");
assertTrue(loop < hoisted && hoisted < inner, out);
assertCompiles(out);
}
}
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
import com.gtnewhorizons.angelica.glsm.shader.ShaderIndex;
import org.lwjgl.opengl.GL20;
import org.taumc.glsl.grammar.GLSLParser;
@@ -30,19 +31,19 @@ final class ShaderTransformChain {
}
static String clipZ(String source) {
return edit(source, ClipZRemap::collectEdits);
return edit(source, (index, edits) -> ClipZRemap.collectEdits(index.root(), edits));
}
static String stripUnused(String source) {
if (!source.contains("sampler")) return source;
return edit(source, (root, edits) -> SamplerStripper.collectEdits(root, source, edits));
return edit(source, SamplerStripper::collectEdits);
}
static String inject(String source, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
return edit(source, (root, edits) -> PerFrameBlockInjector.collectEdits(root, perFrame, perPass, edits));
return edit(source, (index, edits) -> PerFrameBlockInjector.collectEdits(index, perFrame, perPass, edits));
}
private static String edit(String source, BiConsumer<GLSLParser.Translation_unitContext, List<Edit>> collect) {
private static String edit(String source, BiConsumer<ShaderIndex, List<Edit>> collect) {
final GLSLParser.Translation_unitContext root;
try {
root = GlslTransformUtils.parseFullQuiet(source);
@@ -50,7 +51,7 @@ final class ShaderTransformChain {
return source;
}
final List<Edit> edits = new ArrayList<>();
collect.accept(root, edits);
collect.accept(new ShaderIndex(root, source), edits);
return edits.isEmpty() ? source : GlslVulkanPreprocess.applyEdits(source, edits);
}
}
@@ -57,7 +57,7 @@ class FragmentStorageTextureSlotTest {
final ByteBuffer spirv = r.spirv();
try {
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_FRAGMENT_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER, false);
try {
final String msl = StandardCharsets.UTF_8.decode(out.code().duplicate()).toString();
final boolean[] seen = new boolean[samplerCount];
@@ -136,7 +136,7 @@ class FragmentStorageTextureSlotTest {
final ByteBuffer spirv = Reflect.get(obj, "spirv");
final ShaderManager.StageReflection refl = Reflect.get(obj, "reflection");
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER, false);
try {
final String msl = StandardCharsets.UTF_8.decode(out.code().duplicate()).toString();
final int samplerCount = refl.counts().numSamplers();
@@ -8,6 +8,7 @@ import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import com.gtnewhorizons.angelica.sdlgpu.shader.SpirvTestShaders;
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileCache;
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -52,7 +53,7 @@ class MslCrossCompileTest {
final ByteBuffer spirv = compile(SpirvTestShaders.VERTEX_GLSL, Shaderc.shaderc_vertex_shader);
try {
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_VERTEX_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER, false);
try {
final String msl = decode(out.code());
assertTrue(msl.length() > 0, "MSL output non-empty");
@@ -75,7 +76,7 @@ class MslCrossCompileTest {
final ByteBuffer spirv = compile(SpirvTestShaders.FRAGMENT_GLSL, Shaderc.shaderc_fragment_shader);
try {
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_FRAGMENT_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER, false);
try {
final String msl = decode(out.code());
assertTrue(msl.contains("fragment "), "MSL should contain fragment stage qualifier:\n" + msl);
@@ -114,7 +115,7 @@ class MslCrossCompileTest {
final ByteBuffer spirv = compile(SpirvTestShaders.LDS_MULTI_MIP_GLSL, Shaderc.shaderc_compute_shader);
try {
ShaderManager.remapSpirvForComputeSDLGPU(spirv);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER, false);
try {
final String msl = decode(out.code());
assertTrue(msl.contains("kernel "), "Compute MSL should contain kernel stage qualifier:\n" + msl);
@@ -146,7 +147,7 @@ class MslCrossCompileTest {
final ByteBuffer spirv = compile(glsl, Shaderc.shaderc_compute_shader);
try {
ShaderManager.remapSpirvForComputeSDLGPU(spirv);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER, false);
try {
final String msl = decode(out.code());
assertTrue(msl.contains("kernel "), "compute MSL should contain kernel stage qualifier:\n" + msl);
@@ -167,7 +168,7 @@ class MslCrossCompileTest {
final ByteBuffer spirv = compile(CHUNK_CULL_GLSL, Shaderc.shaderc_compute_shader);
try {
ShaderManager.remapSpirvForComputeSDLGPU(spirv);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER, false);
try {
final String msl = decode(out.code());
assertTrue(msl.contains("kernel "), "Compute MSL should contain kernel stage qualifier:\n" + msl);
@@ -202,7 +203,7 @@ class MslCrossCompileTest {
final ByteBuffer spirv = compile(VERTEX_WRITES_IMAGE_GLSL, Shaderc.shaderc_vertex_shader);
try {
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_VERTEX_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER, false);
try {
final String msl = decode(out.code());
assertFalse(msl.contains("vertex void main0"),
@@ -236,7 +237,7 @@ class MslCrossCompileTest {
final ByteBuffer spirv = compile(pre.rewrittenSource(), Shaderc.shaderc_fragment_shader);
try {
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_FRAGMENT_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER, false);
try {
final String msl = decode(out.code());
assertTrue(msl.contains("angelica_renamed_try"), "the local must keep a renamed identifier:\n" + msl);
@@ -255,7 +256,7 @@ class MslCrossCompileTest {
final ByteBuffer spirv = compile(SpirvTestShaders.VERTEX_GLSL, Shaderc.shaderc_vertex_shader);
try {
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_VERTEX_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER, false);
MemoryUtil.memFree(out.code());
} finally {
MemoryUtil.memFree(spirv);
@@ -381,7 +382,7 @@ class MslCrossCompileTest {
final ByteBuffer spirv = compile(glsl, Shaderc.shaderc_compute_shader);
try {
ShaderManager.remapSpirvForComputeSDLGPU(spirv);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER, false);
try {
final String msl = decode(out.code());
assertUniqueBufferSlots(msl);
@@ -436,7 +437,7 @@ class MslCrossCompileTest {
final ByteBuffer spirv = compile(glsl, Shaderc.shaderc_compute_shader);
try {
ShaderManager.remapSpirvForComputeSDLGPU(spirv);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER, false);
try {
assertTrue(decode(out.code()).contains("kernel "), name + " should produce compute MSL");
} finally {
@@ -456,10 +457,10 @@ class MslCrossCompileTest {
try {
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_VERTEX_SHADER);
final byte[] heap = ShaderCacheIO.toHeap(spirv);
MemoryUtil.memFree(MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER).code());
MemoryUtil.memFree(MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER, false).code());
Reflect.<Map<?, ?>>get(cache(), "cache").clear();
ShaderDiskCache.putBlob(cache().diskKey(heap, GL20.GL_VERTEX_SHADER), "poison_entry", "poison".getBytes(StandardCharsets.UTF_8));
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER, false);
try {
assertEquals("poison_entry", out.entrypoint());
assertEquals("poison", new String(ShaderCacheIO.toHeap(out.code()), StandardCharsets.UTF_8));
@@ -471,6 +472,95 @@ class MslCrossCompileTest {
}
}
private static final String IMAGE_ATOMIC_GLSL = """
#version 460 core
layout(local_size_x = 64) in;
layout(binding = 0, r32i) uniform iimage2D counter;
void main() {
imageAtomicAdd(counter, ivec2(0, 0), 1);
}
""";
@Test
void imageAtomicsUseTheAuxBufferWithoutTextureAtomics() {
assertTrue(computeMsl(IMAGE_ATOMIC_GLSL, false).contains("counter_atomic"));
}
@Test
void imageAtomicsCompileToTextureAtomicsWhenAvailable() {
final String msl = computeMsl(IMAGE_ATOMIC_GLSL, true);
assertFalse(msl.contains("counter_atomic"), msl);
assertTrue(msl.contains("counter.atomic_fetch_add("), msl);
}
@Test
void textureAtomicsDoNotChangeShadersWithoutImageAtomics() {
final String base = computeMsl(CHUNK_CULL_GLSL, false);
Reflect.<Map<?, ?>>get(cache(), "cache").clear();
assertEquals(base, computeMsl(CHUNK_CULL_GLSL, true));
}
private static final String FRAGMENT_IMAGE_ATOMIC_GLSL = """
#version 460 core
layout(binding = 0, r32i) uniform iimage2D counter;
layout(location = 0) out vec4 fragColor;
void main() {
fragColor = vec4(imageAtomicAdd(counter, ivec2(0, 0), 1));
}
""";
@Test
void textureAtomicsLeaveRasterStagesAtMsl22() {
final ByteBuffer spirv = compile(FRAGMENT_IMAGE_ATOMIC_GLSL, Shaderc.shaderc_fragment_shader);
try {
assertTrue(CrossCompileUtil.usesImageAtomics(spirv));
final String base = fragmentMsl(spirv, false);
Reflect.<Map<?, ?>>get(cache(), "cache").clear();
final String atomics = fragmentMsl(spirv, true);
assertEquals(base, atomics);
assertFalse(atomics.contains("counter.atomic_fetch_add("), atomics);
} finally {
MemoryUtil.memFree(spirv);
}
}
private static String fragmentMsl(ByteBuffer spirv, boolean textureAtomics) {
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER, textureAtomics);
try {
return decode(out.code());
} finally {
MemoryUtil.memFree(out.code());
}
}
private static String computeMsl(String glsl, boolean textureAtomics) {
final ByteBuffer spirv = compile(glsl, Shaderc.shaderc_compute_shader);
try {
ShaderManager.remapSpirvForComputeSDLGPU(spirv);
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER, textureAtomics);
try {
return decode(out.code());
} finally {
MemoryUtil.memFree(out.code());
}
} finally {
MemoryUtil.memFree(spirv);
}
}
@Test
void onlyShadersWithImageAtomicsAreDetected() {
final ByteBuffer atomic = compile(IMAGE_ATOMIC_GLSL, Shaderc.shaderc_compute_shader);
final ByteBuffer plain = compile(CHUNK_CULL_GLSL, Shaderc.shaderc_compute_shader);
try {
assertTrue(CrossCompileUtil.usesImageAtomics(atomic));
assertFalse(CrossCompileUtil.usesImageAtomics(plain));
} finally {
MemoryUtil.memFree(atomic);
MemoryUtil.memFree(plain);
}
}
private static String decode(ByteBuffer buf) {
final ByteBuffer slice = buf.duplicate();
int len = slice.remaining();
@@ -0,0 +1,39 @@
package com.gtnewhorizons.angelica.api;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import net.coderbot.iris.gbuffer_overrides.matching.SpecialCondition;
import net.coderbot.iris.layer.GbufferPrograms;
import net.coderbot.iris.shaderpack.materialmap.NamespacedId;
import net.coderbot.iris.uniforms.CapturedRenderingState;
public final class EnderDragonBeams {
private static final NamespacedId CRYSTAL_BEAM = new NamespacedId("minecraft", "end_crystal_beam");
private static final NamespacedId DEATH_RAYS = new NamespacedId("minecraft", "dragon_death_rays");
private EnderDragonBeams() {}
public static void beginCrystalBeam() {
if (!AngelicaConfig.enableIris) return;
CapturedRenderingState.INSTANCE.pushCurrentEntityAndItem();
CapturedRenderingState.INSTANCE.setCurrentNamedEntity(CRYSTAL_BEAM);
}
public static void endCrystalBeam() {
if (!AngelicaConfig.enableIris) return;
CapturedRenderingState.INSTANCE.popCurrentEntityAndItem();
}
public static void beginDeathRays() {
if (!AngelicaConfig.enableIris) return;
GbufferPrograms.setupSpecialRenderCondition(SpecialCondition.LIGHTNING);
CapturedRenderingState.INSTANCE.pushCurrentEntityAndItem();
CapturedRenderingState.INSTANCE.setCurrentNamedEntity(DEATH_RAYS);
}
public static void endDeathRays() {
if (!AngelicaConfig.enableIris) return;
CapturedRenderingState.INSTANCE.popCurrentEntityAndItem();
GbufferPrograms.teardownSpecialRenderCondition();
}
}
@@ -0,0 +1,4 @@
package com.gtnewhorizons.angelica.api;
public interface EyePassRenderer {
}
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.client.font;
import com.google.common.collect.ImmutableSet;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IndexBuffer;
import com.gtnewhorizon.gtnhlib.util.font.FontRendering;
import com.gtnewhorizon.gtnhlib.util.font.GlyphReplacements;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.config.FontConfig;
import com.gtnewhorizons.angelica.glsm.GLContextState;
@@ -393,14 +392,33 @@ public class BatchingFontRenderer {
pushQuadIdx();
}
private static final float[] MAX_AA_REACH = {0f, 6f, 8f};
private void pushTexRect(float x, float y, float w, float h, float itOff, int rgba, float uStart, float vStart, float uSz, float vSz, boolean flipV) {
ensureCapacity();
final float tBu0 = uStart;
final float tBu1 = uStart + uSz;
final float tBv0 = vStart;
final float tBv1 = vStart + vSz;
if (FontConfig.fontAAMode > 0 && w > 0 && h > 0) {
final float margin = MAX_AA_REACH[FontConfig.fontAAMode] * preprocessAAStrength() + 0.5f;
final float marginU = margin * (uSz / w);
final float marginV = margin * (vSz / h);
itOff *= (h + 2 * margin) / h;
x -= margin; w += 2 * margin;
y -= margin; h += 2 * margin;
uStart -= marginU; uSz += 2 * marginU;
vStart -= marginV; vSz += 2 * marginV;
}
float vTop = flipV ? vStart + vSz : vStart;
float vBot = flipV ? vStart : vStart + vSz;
pushVtx(x + itOff, y, rgba, uStart, vTop, uStart, uStart + uSz, vStart, vStart + vSz);
pushVtx(x - itOff, y + h, rgba, uStart, vBot, uStart, uStart + uSz, vStart, vStart + vSz);
pushVtx(x + itOff + w, y, rgba, uStart + uSz, vTop, uStart, uStart + uSz, vStart, vStart + vSz);
pushVtx(x - itOff + w, y + h, rgba, uStart + uSz, vBot, uStart, uStart + uSz, vStart, vStart + vSz);
pushVtx(x + itOff, y, rgba, uStart, vTop, tBu0, tBu1, tBv0, tBv1);
pushVtx(x - itOff, y + h, rgba, uStart, vBot, tBu0, tBu1, tBv0, tBv1);
pushVtx(x + itOff + w, y, rgba, uStart + uSz, vTop, tBu0, tBu1, tBv0, tBv1);
pushVtx(x - itOff + w, y + h, rgba, uStart + uSz, vBot, tBu0, tBu1, tBv0, tBv1);
pushQuadIdx();
}
@@ -859,6 +877,10 @@ public class BatchingFontRenderer {
truncateBatchToWatermark();
}
private static float preprocessAAStrength() {
return FontConfig.fontAAStrength / 120.f;
}
private void setupFontDrawState() {
GLStateManager.enableTexture();
GLStateManager.enableAlphaTest();
@@ -873,7 +895,7 @@ public class BatchingFontRenderer {
}
if (FontConfig.fontAAStrength != fontAAStrengthLast) {
fontAAStrengthLast = FontConfig.fontAAStrength;
GLStateManager.glUniform1f(AAStrength, FontConfig.fontAAStrength / 120.f);
GLStateManager.glUniform1f(AAStrength, preprocessAAStrength());
}
if (FontConfig.fontBrightness != fontBrightnessLast) {
fontBrightnessLast = FontConfig.fontBrightness;
@@ -1429,16 +1451,7 @@ public class BatchingFontRenderer {
if (chr == ColorCodeUtils.ESCAPED_AMPERSAND) { chr = '&'; }
if (FontConfig.enableCustomFont && FontConfig.enableGlyphReplacements) {
final char replacement = GlyphReplacements.getReplacementGlyph(chr);
if (replacement != 0) {
if (FontProviderCustom.getPrimary().isGlyphAvailable(replacement)
|| FontProviderCustom.getFallback().isGlyphAvailable(replacement)
) {
chr = replacement;
}
}
}
chr = FontStrategist.replaceCustomGlyph(this, chr, unicodeFlag);
// ASCII space, NBSP, NNBSP, decided before obfuscation as vanilla does.
final boolean whitespace = chr == ' ' || chr == '\u00A0' || chr == '\u202F';
@@ -1451,7 +1464,8 @@ public class BatchingFontRenderer {
chr = fontProvider.getRandomReplacement(chr);
}
heightNorth = anchorY + (underlying.FONT_HEIGHT - 1.0f) * (0.5f - glyphScaleY * fontProvider.getYScaleMultiplier() / 2);
heightNorth = anchorY + (underlying.FONT_HEIGHT - 1.0f) * (0.5f - glyphScaleY * fontProvider.getYScaleMultiplier() / 2)
+ fontProvider.getBaselineShift() * glyphScaleY;
float heightSouth = (underlying.FONT_HEIGHT - 1.0f) * glyphScaleY * fontProvider.getYScaleMultiplier();
visibleCharIndex++;
@@ -1623,7 +1637,9 @@ public class BatchingFontRenderer {
return 4 * this.getWhitespaceScale();
}
FontProvider fp = FontStrategist.getFontProvider(this, chr, FontConfig.enableCustomFont, underlying.getUnicodeFlag());
final boolean unicodeFlag = underlying.getUnicodeFlag();
chr = FontStrategist.replaceCustomGlyph(this, chr, unicodeFlag);
FontProvider fp = FontStrategist.getFontProvider(this, chr, FontConfig.enableCustomFont, unicodeFlag);
return fp.getXAdvance(chr) * this.getGlyphScaleX();
}
@@ -20,4 +20,7 @@ public interface FontProvider {
float getShadowOffset();
int getTexture(char chr);
float getYScaleMultiplier();
/** Vertical offset in font pixels, applied after the glyph is centered on the line. */
default float getBaselineShift() { return 0.0f; }
}
@@ -0,0 +1,244 @@
package com.gtnewhorizons.angelica.client.font;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.gtnewhorizons.angelica.config.FontConfig;
import it.unimi.dsi.fastutil.chars.CharArrayList;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import jss.util.RandomXoshiro256StarStar;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.IReloadableResourceManager;
import net.minecraft.client.resources.IResource;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.util.ResourceLocation;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* For modern Minecraft's hand-drawn font sheets.
*/
public final class FontProviderBitmap implements FontProvider {
private static final ResourceLocation DEFINITION = new ResourceLocation("angelica", "font/modern.json");
private static final int DEFAULT_HEIGHT = 8;
private static final FontProviderBitmap[] NONE = new FontProviderBitmap[0];
private static volatile FontProviderBitmap[] loaded;
public static void registerReloadListener() {
((IReloadableResourceManager) Minecraft.getMinecraft().getResourceManager()).registerReloadListener(manager -> loaded = null);
}
private final ResourceLocation sheet;
private final int columns;
private final float uSize;
private final float vSize;
private final float glyphW;
private final float yScale;
private final float baselineShift;
private final char[][] slotPages = new char[256][];
private final int[] advances;
private final Int2ObjectOpenHashMap<char[]> charsByAdvance = new Int2ObjectOpenHashMap<>();
private final RandomXoshiro256StarStar fontRandom = new RandomXoshiro256StarStar();
public static FontProviderBitmap find(char chr) {
for (FontProviderBitmap provider : providers()) {
if (provider.slot(chr) >= 0) {
return provider;
}
}
return null;
}
private static FontProviderBitmap[] providers() {
FontProviderBitmap[] local = loaded;
if (local == null) {
final IResourceManager resources = Minecraft.getMinecraft().getResourceManager();
if (!resources.getResourceDomains().contains(DEFINITION.getResourceDomain())) {
return NONE;
}
synchronized (FontProviderBitmap.class) {
local = loaded;
if (local == null) {
local = load(resources);
loaded = local;
}
}
}
return local;
}
private static FontProviderBitmap[] load(IResourceManager resources) {
final List<FontProviderBitmap> providers = new ArrayList<>();
try (Reader reader = new InputStreamReader(resources.getResource(DEFINITION).getInputStream(), StandardCharsets.UTF_8)) {
final JsonArray definitions = new JsonParser().parse(reader).getAsJsonObject().getAsJsonArray("providers");
for (JsonElement element : definitions) {
final JsonObject definition = element.getAsJsonObject();
if (!"bitmap".equals(definition.get("type").getAsString())) {
continue;
}
try {
providers.add(new FontProviderBitmap(definition, resources));
} catch (IOException | RuntimeException e) {
FontStrategist.LOGGER.warn("Skipping modern font sheet {}", definition.get("file"), e);
}
}
} catch (IOException | RuntimeException e) {
FontStrategist.LOGGER.error("Could not read modern font definition {}", DEFINITION, e);
}
return providers.toArray(NONE);
}
private static ResourceLocation resolveSheet(IResourceManager resources, ResourceLocation sheet) {
final ResourceLocation bundled = new ResourceLocation("angelica", sheet.getResourcePath());
try {
final int vanillaCopies = Minecraft.getMinecraft().mcDefaultResourcePack.resourceExists(sheet) ? 1 : 0;
final List<IResource> copies = resources.getAllResources(sheet);
for (IResource copy : copies) {
copy.getInputStream().close();
}
return copies.size() > vanillaCopies ? sheet : bundled;
} catch (IOException e) {
return bundled;
}
}
private FontProviderBitmap(JsonObject definition, IResourceManager resources) throws IOException {
final ResourceLocation file = new ResourceLocation(definition.get("file").getAsString());
this.sheet = resolveSheet(resources, new ResourceLocation(file.getResourceDomain(), "textures/" + file.getResourcePath()));
final int height = definition.has("height") ? definition.get("height").getAsInt() : DEFAULT_HEIGHT;
final int ascent = definition.get("ascent").getAsInt();
final JsonArray rows = definition.getAsJsonArray("chars");
final BufferedImage image;
try (InputStream in = resources.getResource(this.sheet).getInputStream()) {
image = ImageIO.read(in);
}
final String firstRow = rows.get(0).getAsString();
this.columns = firstRow.codePointCount(0, firstRow.length());
final int cellWidth = image.getWidth() / this.columns;
final int cellHeight = image.getHeight() / rows.size();
final float scale = (float) height / cellHeight;
this.uSize = (float) cellWidth / image.getWidth();
this.vSize = (float) cellHeight / image.getHeight();
this.glyphW = cellWidth * scale + 1.0f;
this.yScale = (float) height / DEFAULT_HEIGHT;
this.baselineShift = 3.0f + height / 2.0f - ascent;
this.advances = new int[this.columns * rows.size()];
final Int2ObjectOpenHashMap<CharArrayList> byAdvance = new Int2ObjectOpenHashMap<>();
for (int row = 0; row < rows.size(); row++) {
final int[] codepoints = rows.get(row).getAsString().codePoints().toArray();
for (int column = 0; column < codepoints.length; column++) {
final int codepoint = codepoints[column];
if (codepoint == 0 || codepoint > Character.MAX_VALUE) {
continue;
}
final char chr = (char) codepoint;
final int slot = row * this.columns + column;
final int ink = inkWidth(image, column * cellWidth, row * cellHeight, cellWidth, cellHeight);
this.advances[slot] = (int) (0.5f + ink * scale) + 1;
char[] page = this.slotPages[chr >>> 8];
if (page == null) {
page = this.slotPages[chr >>> 8] = new char[256];
}
page[chr & 0xFF] = (char) (slot + 1);
byAdvance.computeIfAbsent(this.advances[slot], k -> new CharArrayList()).add(chr);
}
}
byAdvance.int2ObjectEntrySet().forEach(e -> this.charsByAdvance.put(e.getIntKey(), e.getValue().toCharArray()));
}
private static int inkWidth(BufferedImage image, int cellX, int cellY, int cellWidth, int cellHeight) {
for (int x = cellWidth - 1; x >= 0; x--) {
for (int y = 0; y < cellHeight; y++) {
if ((image.getRGB(cellX + x, cellY + y) >>> 24) != 0) {
return x + 1;
}
}
}
return 0;
}
private int slot(char chr) {
final char[] page = this.slotPages[chr >>> 8];
return page == null ? -1 : page[chr & 0xFF] - 1;
}
@Override
public boolean isGlyphAvailable(char chr) {
return slot(chr) >= 0;
}
@Override
public char getRandomReplacement(char chr) {
final int slot = slot(chr);
if (slot < 0) {
return chr;
}
final char[] sameWidth = this.charsByAdvance.get(this.advances[slot]);
return sameWidth[this.fontRandom.nextInt(sameWidth.length)];
}
@Override
public float getUStart(char chr) {
return (slot(chr) % this.columns) * this.uSize;
}
@Override
public float getVStart(char chr) {
return (float) (slot(chr) / this.columns) * this.vSize;
}
@Override
public float getXAdvance(char chr) {
return this.advances[slot(chr)];
}
@Override
public float getGlyphW(char chr) {
return this.glyphW;
}
@Override
public float getUSize(char chr) {
return this.uSize;
}
@Override
public float getVSize(char chr) {
return this.vSize;
}
@Override
public float getShadowOffset() {
return FontConfig.fontShadowOffset;
}
@Override
public int getTexture(char chr) {
return FontStrategist.getIntFromResourceLocation(this.sheet);
}
@Override
public float getYScaleMultiplier() {
return this.yScale;
}
@Override
public float getBaselineShift() {
return this.baselineShift;
}
}
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.client.font;
import com.google.common.collect.HashMultiset;
import com.gtnewhorizon.gtnhlib.util.font.GlyphReplacements;
import com.gtnewhorizons.angelica.config.FontConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor;
@@ -166,8 +167,12 @@ public class FontStrategist {
if (fp.isGlyphAvailable(chr)) { return fp; }
fp = FontProviderCustom.getFallback();
if (fp.isGlyphAvailable(chr)) { return fp; }
fp = findModernGlyph(me, chr, forceUnicode);
if (fp != null) { return fp; }
return FontProviderUnicode.get();
} else {
final FontProvider modern = findModernGlyph(me, chr, forceUnicode);
if (modern != null) { return modern; }
if (!forceUnicode && FontProviderMC.get(false).isGlyphAvailable(chr)) {
return FontProviderMC.get(false);
} else {
@@ -176,6 +181,23 @@ public class FontStrategist {
}
}
public static char replaceCustomGlyph(BatchingFontRenderer me, char chr, boolean forceUnicode) {
if (!FontConfig.enableGlyphReplacements || !(FontConfig.enableCustomFont || FontConfig.enableModernFont)) { return chr; }
final char original = GlyphReplacements.getReplacementGlyph(chr);
if (original == 0) { return chr; }
if (FontConfig.enableCustomFont
&& (FontProviderCustom.getPrimary().isGlyphAvailable(original) || FontProviderCustom.getFallback().isGlyphAvailable(original))) {
return original;
}
return findModernGlyph(me, original, forceUnicode) != null ? original : chr;
}
// The splash font renderer can only bind its own ascii texture.
private static FontProvider findModernGlyph(BatchingFontRenderer me, char chr, boolean forceUnicode) {
if (!FontConfig.enableModernFont || forceUnicode || me.isSplash) { return null; }
return FontProviderBitmap.find(chr);
}
public static void reloadCustomFontProviders() {
FontProviderCustom.getPrimary().setFont(null);
FontProviderCustom.getFallback().setFont(null);
@@ -2,30 +2,49 @@ package com.gtnewhorizons.angelica.client.rendering;
import net.minecraft.block.Block;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Used to store the block bounds fields in a thread-safe manner, as instance fields don't work correctly
* on multiple threads.
*/
public class ThreadedBlockData {
public static final Thread MAIN_THREAD = Thread.currentThread();
public static Thread serverThread;
public final class ThreadedBlockData {
public static final Thread MAIN_THREAD = Thread.currentThread();
private static Thread SERVER_THREAD;
private static final AtomicInteger CURRENT_SERVER_THREAD_ID = new AtomicInteger();
private final int serverThreadId = Thread.currentThread() == SERVER_THREAD ? CURRENT_SERVER_THREAD_ID.get() : -1;
public double minX, minY, minZ, maxX, maxY, maxZ;
public final Thread owner = Thread.currentThread();
public ThreadedBlockData() {}
public ThreadedBlockData(ThreadedBlockData other) {
this.minX = other.minX;
this.minY = other.minY;
this.minY = other.minY;
this.minZ = other.minZ;
this.maxX = other.maxX;
this.maxY = other.maxY;
this.maxZ = other.maxZ;
}
public boolean isCurrent() {
return this.serverThreadId == CURRENT_SERVER_THREAD_ID.get();
}
public static ThreadedBlockData get(Block block) {
return ((Getter)block).angelica$getThreadData();
return ((Getter) block).angelica$getThreadData();
}
public static void setServerThread(Thread t) {
ThreadedBlockData.SERVER_THREAD = t;
if (t != null) {
CURRENT_SERVER_THREAD_ID.incrementAndGet();
}
}
public static Thread getServerThread() {
return SERVER_THREAD;
}
public interface Getter {
@@ -8,6 +8,10 @@ public class FontConfig {
@Config.DefaultBoolean(false)
public static boolean enableCustomFont;
@Config.Comment({"Draw text with modern Minecraft's font sheets"})
@Config.DefaultBoolean(true)
public static boolean enableModernFont;
@Config.Comment("Name of the primary custom font. Best not to set it from here.")
@Config.DefaultString("(none)")
public static String customFontNamePrimary;
@@ -32,6 +32,7 @@ import net.minecraft.world.World;
import net.minecraft.world.WorldServer;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.IChunkProvider;
import net.minecraftforge.common.DimensionManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.Display;
@@ -75,6 +76,9 @@ public final class FlybyRunner {
private boolean jfr;
private long recordingId;
private final AtomicReference<FlybyRequest> pendingRequest = new AtomicReference<>();
private final AtomicReference<String> pendingDimensionPlayer = new AtomicReference<>();
private boolean dimensionRequested;
private volatile boolean dimensionAbandoned;
private FlybyRequest activeRequest;
private String[] sceneCommands = NO_COMMANDS;
private volatile boolean sceneClearRequested;
@@ -197,6 +201,8 @@ public final class FlybyRunner {
this.waitForTracy = false;
this.waitForFocus = false;
this.exitWhenDone = false;
this.dimensionRequested = false;
this.dimensionAbandoned = false;
this.jfr = false;
this.armed = false;
this.fixedOrigin = null;
@@ -263,6 +269,7 @@ public final class FlybyRunner {
if (this.waitTicks++ % 100 == 0) LOGGER.info("Flyby waiting for window focus");
return;
}
if (this.awaitingDimension(mc, player)) return;
this.begin(mc, player);
}
case PREPARING -> {
@@ -331,6 +338,22 @@ public final class FlybyRunner {
}
}
private boolean awaitingDimension(Minecraft mc, EntityClientPlayerMP player) {
final Integer dimension = SystemProperties.FLYBY_DIMENSION;
if (!this.startedFromProperties || dimension == null || player.dimension == dimension || this.dimensionAbandoned) return false;
if (mc.getIntegratedServer() == null || !DimensionManager.isDimensionRegistered(dimension)) {
LOGGER.warn("Flyby: {}, staying in dimension {}", mc.getIntegratedServer() == null ? "not singleplayer" : "dimension " + dimension + " is not registered", player.dimension);
this.dimensionAbandoned = true;
return false;
}
if (!this.dimensionRequested) {
this.dimensionRequested = true;
LOGGER.info("Flyby: moving from dimension {} to {}", player.dimension, dimension);
this.pendingDimensionPlayer.set(player.getCommandSenderName());
}
return true;
}
private void overridePauseOnLostFocus(Minecraft mc) {
if (this.pauseOnLostFocusOverridden) return;
this.pauseOnLostFocusSaved = mc.gameSettings.pauseOnLostFocus;
@@ -600,11 +623,23 @@ public final class FlybyRunner {
public void onServerTick(TickEvent.ServerTickEvent event) {
if (event.phase != TickEvent.Phase.END) return;
final boolean clear = this.sceneClearRequested;
if (!clear && this.pendingRequest.get() == null) return;
if (!clear && this.pendingRequest.get() == null && this.pendingDimensionPlayer.get() == null) return;
final MinecraftServer server = MinecraftServer.getServer();
if (server == null) return;
final String traveller = this.pendingDimensionPlayer.getAndSet(null);
if (traveller != null) {
this.discardWorldChanges(server);
final EntityPlayerMP player = server.getConfigurationManager().func_152612_a(traveller);
if (player == null) {
LOGGER.warn("Flyby: no server player named '{}', staying in the current dimension", traveller);
this.dimensionAbandoned = true;
} else {
server.getConfigurationManager().transferPlayerToDimension(player, SystemProperties.FLYBY_DIMENSION);
}
}
if (clear) {
this.sceneClearRequested = false;
this.clearScene(server);
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.helpers;
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.FORMATTING_CHAR;
import com.gtnewhorizons.angelica.api.EyePassRenderer;
import net.minecraft.client.renderer.entity.RenderDragon;
import net.minecraft.client.renderer.entity.RenderEnderman;
import net.minecraft.client.renderer.entity.RenderSpider;
@@ -15,7 +16,8 @@ public final class RendererLivingEntityHelper {
private RendererLivingEntityHelper() {}
public static boolean hasEyePass(Object renderer) {
return renderer instanceof RenderSpider || renderer instanceof RenderEnderman || renderer instanceof RenderDragon;
return renderer instanceof RenderSpider || renderer instanceof RenderEnderman || renderer instanceof RenderDragon
|| renderer instanceof EyePassRenderer;
}
public static String getUpsideDownName(EntityLivingBase entity) {
@@ -165,11 +165,19 @@ public class DarkModeFontTransform {
RecolorTarget.includeMethodCall(
new MethodInfo("micdoodle8.mods.galacticraft.core.client.gui.element.GuiElementCheckbox func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_85187_a(Ljava/lang/String;IIIZ)I # drawString")
),
RecolorTarget.includeMethodCall(
new MethodInfo("micdoodle8.mods.galacticraft.core.client.gui.container.GuiAirLockController func_73863_a(IIF)V # drawScreen"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_78276_b(Ljava/lang/String;III)I # drawString")
),
// Steve's Factory Manager
RecolorTarget.includeMethodCall(
new MethodInfo("vswe.stevesfactory.interfaces.GuiBase drawString(Ljava/lang/String;IIFI)V"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_78276_b(Ljava/lang/String;III)I # drawString")
),
RecolorTarget.includeMethodCall(
new MethodInfo("vswe.stevesfactory.interfaces.GuiBase drawSplitString(Ljava/lang/String;IIIFI)V"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_78279_b(Ljava/lang/String;IIII)V # drawSplitString")
),
// Steve's Carts 2
RecolorTarget.includeMethodCall(
@@ -252,6 +260,11 @@ public class DarkModeFontTransform {
new MethodInfo("com.cleanroommc.modularui.widgets.RichTextWidget draw(Lcom/cleanroommc/modularui/screen/viewport/ModularGuiContext;Lcom/cleanroommc/modularui/theme/WidgetThemeEntry;)V"),
new MethodInfo("com.cleanroommc.modularui.drawable.text.RichText drawAtZeroPadded(Lcom/cleanroommc/modularui/screen/viewport/GuiContext;Lcom/cleanroommc/modularui/widget/sizer/Area;Lcom/cleanroommc/modularui/theme/WidgetTheme;)V")
),
// Hardcore Ender Expansion
RecolorTarget.includeMethodCall(
new MethodInfo("chylex.hee.gui.GuiTransportBeacon func_73863_a(IIF)V # drawScreen"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_78276_b(Ljava/lang/String;III)I # drawString")
),
//endregion
//region Exclude Methods
// Applied Energistics 2
@@ -38,6 +38,7 @@ import com.google.common.base.Objects;
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager;
import com.gtnewhorizons.angelica.client.font.DarkModeUtils;
import com.gtnewhorizons.angelica.client.font.FontProviderBitmap;
import com.gtnewhorizons.angelica.commands.AngelicaCommand;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.common.BlockError;
@@ -206,6 +207,7 @@ public final class ClientProxy extends CommonProxy {
ItemRenderListManager.registerReloadListener();
BlockRenderListManager.registerReloadListener();
}
FontProviderBitmap.registerReloadListener();
// Debug tooling
if (SystemProperties.debugTooling() || Tracy.ENABLED) {
@@ -98,6 +98,10 @@ public class CloudRenderer implements IResourceManagerReloadListener {
return GalaxySpaceClouds.supports(world, handler);
}
public void invalidateWorld() {
this.world = null;
}
public void checkSettings() {
if (world != mc.theWorld) {
clearLayers();
@@ -6,6 +6,7 @@ import java.util.Comparator;
import java.util.List;
import com.gtnewhorizons.angelica.compat.bop.FogBiomeCache;
import com.gtnewhorizons.angelica.render.CloudRenderer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.WorldClient;
import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher;
@@ -175,6 +176,7 @@ public class CeleritasWorldRenderer extends SimpleWorldRenderer<WorldClient, Ang
GpuCulling.onWorldUnload();
FfpExtendedAttribs.reset();
FogBiomeCache.invalidate();
CloudRenderer.getCloudRenderer().invalidateWorld();
super.unloadWorld();
}
@@ -181,6 +181,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
// Custom images and SSBOs
private final Set<GlImage> customImages;
private final VertexWriteReplays vertexWriteReplays = new VertexWriteReplays();
private final GlImage[] imagesToClear;
@Nullable
private final ShaderStorageBufferHolder ssboHolder;
@@ -1181,6 +1182,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
ProgramBuilder builder = beginBuilder(source.getName(), transformed);
wireGbufferProgram(builder, availability, shadow);
final Program program = builder.build();
vertexWriteReplays.attach(source.getName(), program, transformed, (samplers, images) -> wireGbufferResources(samplers, images, availability, shadow));
final Pass pass = createPassInner(program, source.getDirectives(), shadow, id);
this.customUniforms.mapholderToPass(builder, pass);
@@ -1194,17 +1196,20 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
private void wireGbufferProgram(ProgramBuilder builder, InputAvailability availability, boolean shadow) {
CommonUniforms.addDynamicUniforms(builder, FogMode.PER_VERTEX);
this.customUniforms.assignTo(builder);
wireGbufferResources(builder, builder, availability, shadow);
}
private void wireGbufferResources(SamplerHolder samplers, ImageHolder images, InputAvailability availability, boolean shadow) {
final Supplier<ImmutableSet<Integer>> flipped = shadow ? flippedShadowGbuffers : flippedGbuffers;
TextureStage textureStage = TextureStage.GBUFFERS_AND_SHADOW;
ProgramSamplers.CustomTextureSamplerInterceptor customTextureSamplerInterceptor =
ProgramSamplers.customTextureSamplerInterceptor(builder,
ProgramSamplers.customTextureSamplerInterceptor(samplers,
customTextureManager.getCustomTextureIdMap(textureStage));
IrisSamplers.addRenderTargetSamplers(customTextureSamplerInterceptor, flipped, renderTargets, false, this);
IrisImages.addRenderTargetImages(builder, flipped, renderTargets);
IrisImages.addRenderTargetImages(images, flipped, renderTargets);
if (!shouldBindPBR) {
shouldBindPBR = IrisSamplers.hasPBRSamplers(customTextureSamplerInterceptor);
@@ -1220,7 +1225,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
IrisSamplers.addCustomImages(customTextureSamplerInterceptor, customImages);
IrisSamplers.addCustomTextures(customTextureSamplerInterceptor, customTextureManager.getIrisCustomTextures());
IrisImages.addCustomImages(builder, customImages);
IrisImages.addCustomImages(images, customImages);
if (IrisSamplers.hasShadowSamplers(customTextureSamplerInterceptor)) {
if (!shadow) {
@@ -1229,7 +1234,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
if (shadowRenderTargets != null) {
recordSamplerUsage(IrisSamplers.addShadowSamplers(customTextureSamplerInterceptor, shadowRenderTargets, null, true));
IrisImages.addShadowColorImages(builder, shadowRenderTargets, null);
IrisImages.addShadowColorImages(images, shadowRenderTargets, null);
}
}
}
@@ -1527,6 +1532,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
final ProgramBuilder builder = beginBuilder(sourceName + kind.variantSuffix, transformed);
wireGbufferProgram(builder, availability, shadow);
final Program variant = builder.build();
vertexWriteReplays.attach(sourceName + kind.variantSuffix, variant, transformed, (samplers, images) -> wireGbufferResources(samplers, images, availability, shadow));
this.customUniforms.mapholderToPass(builder, variant);
return variant;
} catch (Exception e) {
@@ -1606,6 +1612,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
}
}
}
vertexWriteReplays.destroy();
if (shadowVoxelizationCompute != null) {
shadowVoxelizationCompute.destroy();
shadowVoxelizationCompute = null;
@@ -0,0 +1,161 @@
package net.coderbot.iris.pipeline;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.VertexWriteReplaySetup;
import com.gtnewhorizons.angelica.glsm.texture.InternalTextureFormat;
import com.gtnewhorizons.angelica.glsm.texture.TextureType;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gl.image.ImageHolder;
import net.coderbot.iris.gl.program.ComputeProgram;
import net.coderbot.iris.gl.program.Program;
import net.coderbot.iris.gl.program.ProgramBuilder;
import net.coderbot.iris.gl.sampler.GlSampler;
import net.coderbot.iris.gl.sampler.SamplerHolder;
import net.coderbot.iris.gl.state.ValueUpdateNotifier;
import net.coderbot.iris.pipeline.transform.PatchShaderType;
import net.coderbot.iris.pipeline.transform.RwImageStoreExtractor;
import net.coderbot.iris.pipeline.transform.RwImageStoreExtractor.VertexInput;
import net.coderbot.iris.samplers.IrisSamplers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.IntSupplier;
/**
* SDL-GPU only allows image writes from compute, so a gbuffers/shadow vertex shader that writes images gets a compute
* twin that the backend runs over every draw made with the program.
*/
final class VertexWriteReplays {
private final List<ComputeProgram> twins = new ArrayList<>();
private final IntArrayList attachedPrograms = new IntArrayList();
void attach(String name, Program program, Map<PatchShaderType, String> transformed, BiConsumer<SamplerHolder, ImageHolder> wireResources) {
BackendManager.RENDER_BACKEND.setVertexWriteReplay(program.getProgramId(), null);
final String source = transformed.get(PatchShaderType.COMPUTE);
if (RwImageStoreExtractor.parseSentinel(source) != RwImageStoreExtractor.RwExtractMode.VERTEX_REPLAY) return;
final ComputeProgram twin;
try {
twin = ProgramBuilder.beginCompute(name + "_vertex_replay", source, IrisSamplers.WORLD_RESERVED_TEXTURE_UNITS).buildCompute();
} catch (RuntimeException e) {
Iris.logger.error("Vertex image write replay for {} failed to build; its image writes are dropped", name, e);
return;
}
final List<String> writtenImages = RwImageStoreExtractor.parseVertexReplayImages(source);
final List<String> images = new ArrayList<>(writtenImages);
images.addAll(RwImageStoreExtractor.parseVertexReplayReadImages(source));
final List<String> samplers = new ArrayList<>();
for (String sampler : RwImageStoreExtractor.parseVertexReplaySamplers(source)) {
if (RenderSystem.getUniformLocation(twin.getProgramId(), sampler) != -1) samplers.add(sampler);
}
final ResourceCollector resources = new ResourceCollector(images, samplers);
wireResources.accept(resources, resources);
for (int i = 0; i < resources.images.length; i++) {
if (resources.images[i] == null) {
Iris.logger.warn("Vertex image writes of {} use image {}, which the pipeline does not provide; they are dropped", name, resources.imageNames.get(i));
twin.destroy();
return;
}
}
final List<VertexInput> inputs = RwImageStoreExtractor.parseVertexReplayInputs(source);
final String[] inputNames = new String[inputs.size()];
final int[] inputLocations = new int[inputs.size()];
for (int i = 0; i < inputs.size(); i++) {
inputNames[i] = inputs.get(i).name();
inputLocations[i] = inputs.get(i).location();
}
BackendManager.RENDER_BACKEND.setVertexWriteReplay(program.getProgramId(), new VertexWriteReplaySetup(twin.getProgramId(),
inputNames, inputLocations, resources.images, writtenImages.size(),
resources.samplerNames.toArray(new String[0]), resources.samplerUnits, resources.samplerTextures, resources.samplerObjects,
RwImageStoreExtractor.VG_REPLAY_VERTEX_BUFFER_BINDING, RwImageStoreExtractor.VG_REPLAY_VERTEX_BUFFER_COUNT, RwImageStoreExtractor.VG_REPLAY_INDEX_BUFFER_BINDING));
attachedPrograms.add(program.getProgramId());
twins.add(twin);
}
void destroy() {
for (int i = 0; i < attachedPrograms.size(); i++) BackendManager.RENDER_BACKEND.setVertexWriteReplay(attachedPrograms.getInt(i), null);
attachedPrograms.clear();
for (ComputeProgram twin : twins) twin.destroy();
twins.clear();
}
private static final class ResourceCollector implements SamplerHolder, ImageHolder {
final List<String> imageNames;
final IntSupplier[] images;
final List<String> samplerNames;
final int[] samplerUnits;
final IntSupplier[] samplerTextures;
final int[] samplerObjects;
ResourceCollector(List<String> imageNames, List<String> samplerNames) {
this.imageNames = imageNames;
this.images = new IntSupplier[imageNames.size()];
this.samplerNames = samplerNames;
this.samplerUnits = new int[samplerNames.size()];
Arrays.fill(samplerUnits, -1);
this.samplerTextures = new IntSupplier[samplerNames.size()];
this.samplerObjects = new int[samplerNames.size()];
}
@Override
public boolean hasImage(String name) {
return imageNames.contains(name);
}
@Override
public void addTextureImage(IntSupplier textureID, InternalTextureFormat internalFormat, String name) {
final int index = imageNames.indexOf(name);
if (index >= 0 && images[index] == null) images[index] = textureID;
}
@Override
public boolean hasSampler(String name) {
return samplerNames.contains(name) || imageNames.contains(name);
}
@Override
public void addExternalSampler(int textureUnit, String... names) {
for (String name : names) {
final int index = samplerNames.indexOf(name);
if (index >= 0 && unassigned(index)) samplerUnits[index] = textureUnit;
}
}
@Override
public boolean addDefaultSampler(TextureType type, IntSupplier texture, ValueUpdateNotifier notifier, GlSampler sampler, String... names) {
return addDynamicSampler(type, texture, notifier, sampler, names);
}
@Override
public boolean addDynamicSampler(TextureType type, IntSupplier texture, GlSampler sampler, String... names) {
return addDynamicSampler(type, texture, null, sampler, names);
}
@Override
public boolean addDynamicSampler(TextureType type, IntSupplier texture, ValueUpdateNotifier notifier, GlSampler sampler, String... names) {
boolean used = false;
for (String name : names) {
final int index = samplerNames.indexOf(name);
if (index < 0) continue;
used = true;
if (unassigned(index)) {
samplerTextures[index] = texture;
samplerObjects[index] = sampler != null ? sampler.getId() : 0;
}
}
return used;
}
private boolean unassigned(int index) {
return samplerUnits[index] < 0 && samplerTextures[index] == null;
}
}
}
@@ -3,6 +3,7 @@ package net.coderbot.iris.pipeline.transform;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import net.coderbot.iris.gl.program.ComputeProgram;
import net.coderbot.iris.postprocess.FullScreenQuadRenderer;
import net.coderbot.iris.uniforms.custom.CustomUniforms;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL42;
@@ -16,6 +17,7 @@ public final class PreRasterComputeDispatcher {
final int prevProgram = GLStateManager.glGetInteger(GL20.GL_CURRENT_PROGRAM);
compute.use();
uniforms.push(compute);
if (mode == RwImageStoreExtractor.RwExtractMode.COMPOSITE_VSH) FullScreenQuadRenderer.uploadCompositeMatrices();
int targetSizeLoc = cachedTargetSizeLoc;
if (mode == RwImageStoreExtractor.RwExtractMode.COMPOSITE_FSH) {
if (targetSizeLoc == LOC_UNRESOLVED) {
@@ -1,5 +1,6 @@
package net.coderbot.iris.pipeline.transform;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
import net.coderbot.iris.gl.image.ImageInformation;
import com.gtnewhorizons.angelica.glsm.texture.InternalTextureFormat;
@@ -7,6 +8,7 @@ import org.antlr.v4.runtime.CommonToken;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
import org.taumc.glsl.ShaderParser;
import org.taumc.glsl.Transformer;
import org.taumc.glsl.grammar.GLSLLexer;
@@ -22,26 +24,30 @@ import java.util.Locale;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
public final class RwImageStoreExtractor {
public enum RwExtractMode { CHUNK, COMPOSITE_VSH, COMPOSITE_FSH }
public enum RwExtractMode { CHUNK, COMPOSITE_VSH, COMPOSITE_FSH, VERTEX_REPLAY }
public static RwExtractMode parseSentinel(String computeSource) {
if (computeSource == null) return null;
if (computeSource.startsWith("// _vg_mode: chunk")) return RwExtractMode.CHUNK;
if (computeSource.startsWith("// _vg_mode: composite-vsh")) return RwExtractMode.COMPOSITE_VSH;
if (computeSource.startsWith("// _vg_mode: composite-fsh")) return RwExtractMode.COMPOSITE_FSH;
if (computeSource.startsWith("// _vg_mode: vertex-replay")) return RwExtractMode.VERTEX_REPLAY;
return null;
}
public record VertexInput(String name, String glslType, int location) {}
public record AttributeSlot(String name, String glslType, int byteOffset, String unpackExpr, boolean liveBranch) {
public AttributeSlot(String name, String glslType, int byteOffset, String unpackExpr) {
this(name, glslType, byteOffset, unpackExpr, true);
}
}
public record Result(String strippedSource, String computeSource, Set<String> writtenImages, RwExtractMode mode) {}
public record Result(String strippedSource, String computeSource, Set<String> writtenImages, RwExtractMode mode, Set<String> atomicImages) {}
public record ImageDecl(String name, String glslType, String layoutFormat) {}
@@ -53,6 +59,9 @@ public final class RwImageStoreExtractor {
public static final int VG_VBUF_SSBO_BINDING = 9;
public static final int VG_RANGES_SSBO_BINDING = 10;
public static final int VG_REPLAY_VERTEX_BUFFER_BINDING = 9;
public static final int VG_REPLAY_VERTEX_BUFFER_COUNT = 4;
public static final int VG_REPLAY_INDEX_BUFFER_BINDING = VG_REPLAY_VERTEX_BUFFER_BINDING + VG_REPLAY_VERTEX_BUFFER_COUNT;
private static final int STRIDE_BYTES = 48;
private static final int STRIDE_UINTS = STRIDE_BYTES / 4;
@@ -62,8 +71,30 @@ public final class RwImageStoreExtractor {
return activeCustomImages;
}
private static volatile Set<String> atomicCustomImages = Set.of();
public static void setActiveCustomImages(Map<String, ImageInformation> images) {
activeCustomImages = (images == null) ? Map.of() : Map.copyOf(images);
final TreeSet<String> atomic = new TreeSet<>();
if (!activeCustomImages.isEmpty() && !BackendManager.RENDER_BACKEND.supportsComputeImageAtomics()) {
for (ImageInformation image : activeCustomImages.values()) {
if (image.isRelative()) continue;
if (BackendManager.RENDER_BACKEND.supportsComputeImageAtomics(image.width(), image.height(), image.depth())) atomic.add(image.name());
}
}
if (!atomic.equals(atomicCustomImages)) {
atomicCustomImages = atomic;
TransformPatcher.clearCache();
ShaderTransformer.clearCache();
}
}
public static boolean supportsAtomics(String image) {
return atomicCustomImages.contains(image) || BackendManager.RENDER_BACKEND.supportsComputeImageAtomics();
}
static String atomicCustomImagesKey() {
return String.join(",", atomicCustomImages);
}
private static final Set<String> RW_CALLS = Set.of(
@@ -73,7 +104,11 @@ public final class RwImageStoreExtractor {
);
public static Result tryExtract(String source, PatchShaderType stage, String programName) {
if (source == null) return null;
return tryExtract(source, stage, programName, null);
}
public static Result tryExtract(String source, PatchShaderType stage, String programName, RwExtractMode vertexMode) {
if (source == null || !mayWriteImages(source)) return null;
if (stage != PatchShaderType.VERTEX && stage != PatchShaderType.FRAGMENT) return null;
final ShaderParser.ParsedShader parsed = ShaderParser.parseShader(source);
@@ -94,17 +129,188 @@ public final class RwImageStoreExtractor {
final Map<String, String> chunkAttrAstTypes = (stage == PatchShaderType.VERTEX) ? collectChunkAttrAstTypes(decls) : Map.of();
final Set<String> chunkAttrsUsed = (stage == PatchShaderType.VERTEX) ? collectChunkAttrsUsed(decls, root, source) : Set.of();
final RwExtractMode mode = (stage == PatchShaderType.VERTEX) ? (chunkAttrsUsed.isEmpty() ? RwExtractMode.COMPOSITE_VSH : RwExtractMode.CHUNK) : RwExtractMode.COMPOSITE_FSH;
final RwExtractMode mode = (stage == PatchShaderType.VERTEX)
? (vertexMode != null ? vertexMode : chunkAttrsUsed.isEmpty() ? RwExtractMode.COMPOSITE_VSH : RwExtractMode.CHUNK)
: RwExtractMode.COMPOSITE_FSH;
final List<AttributeSlot> attrs = (mode == RwExtractMode.CHUNK) ? collectVertexAttributes(chunkAttrsUsed, chunkAttrAstTypes) : List.of();
final Set<String> quadInputs = (mode == RwExtractMode.COMPOSITE_VSH) ? declaredQuadInputs(decls) : Set.of();
final boolean definesFtransform = definesFunction(root, "iris_ftransform");
final String stripped = buildRasterOutputViaSourceSlicing(source, decls, exprs, writtenImages, declared, mode);
final Transformer tc = new Transformer(root);
applyComputeMutations(tc, writtenImages, declared, chunkAttrsUsed, mode, nonWriteonlyImages);
final String compute = buildComputeOutput(root, mode, writtenImages, nonWriteonlyImages, declared, attrs);
applyComputeMutations(tc, writtenImages, declared, mode == RwExtractMode.COMPOSITE_VSH ? quadInputs : chunkAttrsUsed, mode, nonWriteonlyImages);
final String compute = buildComputeOutput(root, mode, writtenImages, nonWriteonlyImages, declared, attrs, quadInputs, definesFtransform, null);
return new Result(stripped, compute, writtenImages, mode);
return new Result(stripped, compute, writtenImages, mode, atomicImages(root, writtenImages));
}
private static boolean mayWriteImages(String source) {
return source.contains("imageStore") || source.contains("imageAtomic");
}
public static Result tryExtractVertexReplay(String source) {
if (source == null || !mayWriteImages(source)) return null;
final ShaderParser.ParsedShader parsed = ShaderParser.parseShader(source);
final GLSLParser.Translation_unitContext root = parsed.full();
final List<GLSLParser.Single_declarationContext> decls = new ArrayList<>();
final List<GLSLParser.Postfix_expressionContext> exprs = new ArrayList<>();
GlslAstHelpers.collectInto(root,
new List[] { decls, exprs },
new Class<?>[] { GLSLParser.Single_declarationContext.class, GLSLParser.Postfix_expressionContext.class });
final Map<String, ImageDecl> declared = collectImageDecls(decls);
final Set<String> writtenImages = new LinkedHashSet<>();
final Set<String> nonWriteonlyImages = new LinkedHashSet<>();
classifyImageCalls(exprs, declared, writtenImages, nonWriteonlyImages);
if (writtenImages.isEmpty()) return null;
final String stripped = buildRasterOutputViaSourceSlicing(source, decls, exprs, writtenImages, declared, RwExtractMode.VERTEX_REPLAY);
final List<VertexInput> inputs = collectVertexInputs(decls);
if (inputs == null) return new Result(stripped, null, writtenImages, RwExtractMode.VERTEX_REPLAY, Set.of());
final List<String> readImages = readOnlyImages(exprs, declared, writtenImages);
final Set<String> inputNames = new LinkedHashSet<>();
for (VertexInput input : inputs) inputNames.add(input.name());
final Transformer tc = new Transformer(root);
applyComputeMutations(tc, writtenImages, declared, inputNames, RwExtractMode.VERTEX_REPLAY, nonWriteonlyImages);
for (String name : readImages) tc.removeVariable(name);
final ReplayLayout layout = new ReplayLayout(inputs, readImages, replayHeader(inputs, writtenImages, readImages, collectSamplerNames(decls)));
final String compute = buildComputeOutput(root, RwExtractMode.VERTEX_REPLAY, writtenImages, nonWriteonlyImages, declared, List.of(), Set.of(), false, layout);
return new Result(stripped, compute, writtenImages, RwExtractMode.VERTEX_REPLAY, atomicImages(root, writtenImages));
}
private record ReplayLayout(List<VertexInput> inputs, List<String> readImages, String header) {}
private static final String REPLAY_INPUTS_HEADER = "// _vg_inputs:";
private static final String REPLAY_IMAGES_HEADER = "// _vg_images:";
private static final String REPLAY_READ_IMAGES_HEADER = "// _vg_read_images:";
private static final String REPLAY_SAMPLERS_HEADER = "// _vg_samplers:";
private static String replayHeader(List<VertexInput> inputs, Set<String> writtenImages, List<String> readImages, List<String> samplers) {
final StringBuilder out = new StringBuilder(REPLAY_INPUTS_HEADER);
for (VertexInput input : inputs) out.append(' ').append(input.name()).append(':').append(input.glslType()).append(':').append(input.location());
out.append('\n').append(REPLAY_IMAGES_HEADER);
for (String image : sortedImages(writtenImages)) out.append(' ').append(image);
out.append('\n').append(REPLAY_READ_IMAGES_HEADER);
for (String image : readImages) out.append(' ').append(image);
out.append('\n').append(REPLAY_SAMPLERS_HEADER);
for (String sampler : samplers) out.append(' ').append(sampler);
return out.append('\n').toString();
}
/** Images the shader reads but never writes, sorted; the replay binds them after the written ones. */
private static List<String> readOnlyImages(List<GLSLParser.Postfix_expressionContext> exprs, Map<String, ImageDecl> declared, Set<String> writtenImages) {
final Set<String> read = new LinkedHashSet<>();
for (var expr : exprs) {
if (GlslAstHelpers.extractCallName(expr) == null) continue;
final String firstArg = GlslAstHelpers.firstArgIdentifier(expr);
if (declared.containsKey(firstArg) && !writtenImages.contains(firstArg)) read.add(firstArg);
}
final List<String> sorted = new ArrayList<>(read);
Collections.sort(sorted);
return sorted;
}
/** The images a vertex-replay compute source only reads, in the order of their bindings after the written ones. */
public static List<String> parseVertexReplayReadImages(String computeSource) {
return headerTokens(computeSource, REPLAY_READ_IMAGES_HEADER);
}
/** The samplers a vertex-replay compute source declares. */
public static List<String> parseVertexReplaySamplers(String computeSource) {
return headerTokens(computeSource, REPLAY_SAMPLERS_HEADER);
}
/** The inputs of a vertex-replay compute source, in the order of its {@code _vg_attr} slots. */
public static List<VertexInput> parseVertexReplayInputs(String computeSource) {
final List<VertexInput> out = new ArrayList<>();
for (String token : headerTokens(computeSource, REPLAY_INPUTS_HEADER)) {
final String[] parts = token.split(":");
if (parts.length != 3) continue;
out.add(new VertexInput(parts[0], parts[1], Integer.parseInt(parts[2])));
}
return out;
}
/** The images a vertex-replay compute source writes, in the order of their bindings. */
public static List<String> parseVertexReplayImages(String computeSource) {
return headerTokens(computeSource, REPLAY_IMAGES_HEADER);
}
private static List<String> headerTokens(String computeSource, String header) {
if (computeSource == null) return List.of();
final int start = computeSource.indexOf(header);
if (start < 0) return List.of();
final int end = computeSource.indexOf('\n', start);
final String line = computeSource.substring(start + header.length(), end < 0 ? computeSource.length() : end).trim();
if (line.isEmpty()) return List.of();
return List.of(line.split(" "));
}
private static final Set<String> REPLAYABLE_INPUT_TYPES = Set.of(
"float", "vec2", "vec3", "vec4", "int", "ivec2", "ivec3", "ivec4", "uint", "uvec2", "uvec3", "uvec4");
/** Global {@code in}/{@code attribute} declarations, or null if one of them cannot be fetched per component. */
private static List<VertexInput> collectVertexInputs(List<GLSLParser.Single_declarationContext> decls) {
final List<VertexInput> out = new ArrayList<>();
for (var decl : decls) {
final var fst = decl.fully_specified_type();
if (fst == null || fst.type_qualifier() == null) continue;
if (GlslAstHelpers.enclosingOfType(decl, GLSLParser.Function_definitionContext.class) != null) continue;
boolean isInput = false;
int location = -1;
for (var sq : fst.type_qualifier().single_type_qualifier()) {
if (sq.storage_qualifier() != null) {
final String storage = sq.storage_qualifier().getText();
if (storage.equals("in") || storage.equals("attribute")) isInput = true;
}
final var lq = sq.layout_qualifier();
if (lq == null || lq.layout_qualifier_id_list() == null) continue;
for (var id : lq.layout_qualifier_id_list().layout_qualifier_id()) {
if (id.IDENTIFIER() == null || !"location".equals(id.IDENTIFIER().getText())) continue;
try {
location = Integer.parseInt(id.getText().substring(id.getText().indexOf('=') + 1).trim());
} catch (NumberFormatException e) {
return null;
}
}
}
if (!isInput) continue;
final String type = fst.type_specifier().getText();
if (!REPLAYABLE_INPUT_TYPES.contains(type)) return null;
final List<GLSLParser.Typeless_declarationContext> declarators = new ArrayList<>();
declarators.add(decl.typeless_declaration());
if (decl.getParent() instanceof GLSLParser.Init_declarator_listContext list) declarators.addAll(list.typeless_declaration());
for (var typeless : declarators) {
if (typeless == null || typeless.IDENTIFIER() == null || typeless.array_specifier() != null) return null;
out.add(new VertexInput(typeless.IDENTIFIER().getText(), type, location));
}
}
return out;
}
/** Global sampler uniforms, which the replay may read even after the graphics program stops reading them. */
private static List<String> collectSamplerNames(List<GLSLParser.Single_declarationContext> decls) {
final List<String> out = new ArrayList<>();
for (var decl : decls) {
final var fst = decl.fully_specified_type();
if (fst == null || fst.type_specifier() == null) continue;
if (GlslAstHelpers.enclosingOfType(decl, GLSLParser.Function_definitionContext.class) != null) continue;
final String type = fst.type_specifier().getText();
if (!type.startsWith("sampler") && !type.startsWith("isampler") && !type.startsWith("usampler")) continue;
final List<GLSLParser.Typeless_declarationContext> declarators = new ArrayList<>();
declarators.add(decl.typeless_declaration());
if (decl.getParent() instanceof GLSLParser.Init_declarator_listContext list) declarators.addAll(list.typeless_declaration());
for (var typeless : declarators) {
if (typeless != null && typeless.IDENTIFIER() != null) out.add(typeless.IDENTIFIER().getText());
}
}
return out;
}
private static Map<String, ImageDecl> collectImageDecls(List<GLSLParser.Single_declarationContext> decls) {
@@ -270,13 +476,22 @@ public final class RwImageStoreExtractor {
private static void applyComputeMutations(Transformer tc, Set<String> writtenImages, Map<String, ImageDecl> declared, Set<String> chunkAttrsUsed, RwExtractMode mode, Set<String> nonWriteonlyImages) {
hoistNonConstGlobalInitializers(tc);
renameMainToVgBody(tc);
rewriteImageAtomicsToNonAtomic(tc, declared, writtenImages, nonWriteonlyImages);
if (mode != RwExtractMode.VERTEX_REPLAY) rewriteImageAtomicsToNonAtomic(tc, declared, writtenImages, nonWriteonlyImages);
for (String name : writtenImages) tc.removeVariable(name);
if (mode == RwExtractMode.CHUNK) {
if (mode == RwExtractMode.CHUNK || mode == RwExtractMode.VERTEX_REPLAY) {
for (String name : chunkAttrsUsed) tc.removeVariable(name);
convertInterfaceBlocks(tc);
if (mode == RwExtractMode.VERTEX_REPLAY) {
stripInterfaceLayoutQualifiers(tc);
removeQualifierOnlyDeclarations(tc);
}
stripStorageQualifiers(tc, Set.of("out", "varying"));
} else if (mode == RwExtractMode.COMPOSITE_VSH) {
for (String name : chunkAttrsUsed) tc.removeVariable(name);
tc.renameFunctionCall("ftransform", "iris_ftransform");
convertInterfaceBlocks(tc);
stripInterfaceLayoutQualifiers(tc);
removeQualifierOnlyDeclarations(tc);
stripStorageQualifiers(tc, Set.of("in", "attribute", "out", "varying"));
} else if (mode == RwExtractMode.COMPOSITE_FSH) {
stripFragOutputs(tc);
@@ -311,6 +526,17 @@ public final class RwImageStoreExtractor {
});
}
private static Set<String> atomicImages(GLSLParser.Translation_unitContext computeRoot, Set<String> writtenImages) {
final Set<String> out = new LinkedHashSet<>();
for (var expr : GlslAstHelpers.collectAll(computeRoot, GLSLParser.Postfix_expressionContext.class)) {
final String fname = GlslAstHelpers.extractCallName(expr);
if (fname == null || !ATOMIC_CALLS.contains(fname)) continue;
final String image = GlslAstHelpers.firstArgIdentifier(expr);
if (writtenImages.contains(image)) out.add(image);
}
return out;
}
private static boolean isCallTopLevelStatement(GLSLParser.Postfix_expressionContext expr) {
final GLSLParser.StatementContext stmt = GlslAstHelpers.enclosingOfType(expr, GLSLParser.StatementContext.class);
if (stmt == null) return false;
@@ -409,6 +635,7 @@ public final class RwImageStoreExtractor {
t.mutateTree(tree -> {
for (var sq : GlslAstHelpers.collectAll(tree, GLSLParser.Storage_qualifierContext.class)) {
if (!kill.contains(sq.getText())) continue;
if (GlslAstHelpers.enclosingOfType(sq, GLSLParser.Parameter_declarationContext.class) != null) continue;
blankLeadingToken(sq);
}
});
@@ -428,6 +655,91 @@ public final class RwImageStoreExtractor {
private static final Set<String> AUXILIARY_STORAGE_QUALIFIERS = Set.of("centroid", "sample", "patch");
private static final Set<String> INTERFACE_STORAGE_QUALIFIERS = Set.of("in", "out", "attribute", "varying");
private static void removeQualifierOnlyDeclarations(Transformer t) {
t.mutateTree(tree -> {
if (tree.children == null) return;
tree.children.removeIf(child -> child instanceof GLSLParser.External_declarationContext ed
&& ed.declaration() != null && isQualifierOnly(ed.declaration()));
});
}
private static boolean isQualifierOnly(GLSLParser.DeclarationContext decl) {
if (decl.LEFT_BRACE() != null) return false;
if (decl.init_declarator_list() == null) return decl.type_qualifier() != null && qualifiesStageInterface(decl.type_qualifier());
final var single = decl.init_declarator_list().single_declaration();
return single != null && single.typeless_declaration() == null && decl.init_declarator_list().COMMA().isEmpty()
&& single.fully_specified_type() != null && single.fully_specified_type().type_qualifier() != null
&& qualifiesStageInterface(single.fully_specified_type().type_qualifier());
}
private static boolean qualifiesStageInterface(GLSLParser.Type_qualifierContext tq) {
for (var sq : tq.single_type_qualifier()) {
if (sq.invariant_qualifier() != null || sq.precise_qualifier() != null) return true;
if (sq.storage_qualifier() != null && INTERFACE_STORAGE_QUALIFIERS.contains(sq.storage_qualifier().getText())) return true;
}
return false;
}
private static void convertInterfaceBlocks(Transformer t) {
t.mutateTree(tree -> {
if (tree.children == null) return;
tree.children.removeIf(child -> child instanceof GLSLParser.External_declarationContext ed
&& isInterfaceBlock(ed.declaration()) && "gl_PerVertex".equals(ed.declaration().IDENTIFIER(0).getText()));
for (var child : tree.children) {
if (!(child instanceof GLSLParser.External_declarationContext ed) || !isInterfaceBlock(ed.declaration())) continue;
final GLSLParser.DeclarationContext decl = ed.declaration();
final String blockName = decl.IDENTIFIER(0).getText();
blankAllTerminals(decl.type_qualifier());
if (decl.IDENTIFIER().size() > 1) {
setFirstTerminal(decl.type_qualifier(), "struct");
setText(decl.RIGHT_BRACE(), "}; " + blockName);
} else {
setText(decl.IDENTIFIER(0), "");
setText(decl.LEFT_BRACE(), "");
setText(decl.RIGHT_BRACE(), "");
setText(decl.SEMICOLON(), "");
}
}
});
}
private static boolean isInterfaceBlock(GLSLParser.DeclarationContext decl) {
return decl != null && decl.LEFT_BRACE() != null && decl.type_qualifier() != null && !decl.IDENTIFIER().isEmpty()
&& qualifiesStageInterface(decl.type_qualifier());
}
private static void setText(TerminalNode node, String text) {
if (node != null && node.getSymbol() instanceof CommonToken ct) ct.setText(text);
}
private static void setFirstTerminal(ParseTree node, String text) {
if (node instanceof TerminalNode tn) {
setText(tn, text);
return;
}
if (node.getChildCount() > 0) setFirstTerminal(node.getChild(0), text);
}
private static void stripInterfaceLayoutQualifiers(Transformer t) {
t.mutateTree(tree -> {
for (var tq : GlslAstHelpers.collectAll(tree, GLSLParser.Type_qualifierContext.class)) {
boolean isInterface = false;
for (var sq : tq.single_type_qualifier()) {
if (sq.storage_qualifier() != null && INTERFACE_STORAGE_QUALIFIERS.contains(sq.storage_qualifier().getText())) {
isInterface = true;
break;
}
}
if (!isInterface) continue;
for (var sq : tq.single_type_qualifier()) {
if (sq.layout_qualifier() != null) blankAllTerminals(sq.layout_qualifier());
}
}
});
}
private static void blankLeadingToken(ParserRuleContext ctx) {
if (ctx.getChildCount() == 0) return;
if (!(ctx.getChild(0) instanceof TerminalNode tn)) return;
@@ -537,19 +849,42 @@ public final class RwImageStoreExtractor {
});
}
private static String buildComputeOutput(GLSLParser.Translation_unitContext computeRoot, RwExtractMode mode, Set<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared, List<AttributeSlot> attrs) {
private static final Set<String> QUAD_INPUTS = Set.of("iris_Vertex", "iris_MultiTexCoord0");
private static Set<String> declaredQuadInputs(List<GLSLParser.Single_declarationContext> decls) {
final Set<String> out = new LinkedHashSet<>();
for (var decl : decls) {
if (decl.typeless_declaration() == null || decl.typeless_declaration().IDENTIFIER() == null) continue;
if (GlslAstHelpers.enclosingOfType(decl, GLSLParser.Function_definitionContext.class) != null) continue;
final String name = decl.typeless_declaration().IDENTIFIER().getText();
if (QUAD_INPUTS.contains(name)) out.add(name);
}
return out;
}
private static boolean definesFunction(GLSLParser.Translation_unitContext root, String name) {
for (var fdef : GlslAstHelpers.collectAll(root, GLSLParser.Function_definitionContext.class)) {
final var id = fdef.function_prototype().IDENTIFIER();
if (id != null && name.equals(id.getText())) return true;
}
return false;
}
private static String buildComputeOutput(GLSLParser.Translation_unitContext computeRoot, RwExtractMode mode, Set<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared, List<AttributeSlot> attrs, Set<String> quadInputs, boolean definesFtransform, ReplayLayout replay) {
final StringBuilder out = new StringBuilder(8192);
out.append("// _vg_mode: ").append(modeTag(mode)).append('\n');
if (replay != null) out.append(replay.header());
out.append("#version 460 core\n");
switch (mode) {
case CHUNK -> emitChunkPrelude(out, writtenImages, nonWriteonlyImages, declared, attrs);
case COMPOSITE_VSH -> emitCompositeVshPrelude(out, writtenImages, nonWriteonlyImages, declared);
case COMPOSITE_VSH -> emitCompositeVshPrelude(out, writtenImages, nonWriteonlyImages, declared, quadInputs, definesFtransform);
case COMPOSITE_FSH -> emitCompositeFshPrelude(out, writtenImages, nonWriteonlyImages, declared);
case VERTEX_REPLAY -> emitVertexReplayPrelude(out, writtenImages, nonWriteonlyImages, declared, replay);
}
out.append('\n');
out.append(GlslTransformUtils.getFormattedShader(computeRoot, ""));
out.append('\n');
emitDispatchMain(out, mode, attrs);
emitDispatchMain(out, mode, attrs, replay != null ? replay.inputs() : List.of(), quadInputs);
return out.toString();
}
@@ -558,19 +893,26 @@ public final class RwImageStoreExtractor {
case CHUNK -> "chunk";
case COMPOSITE_VSH -> "composite-vsh";
case COMPOSITE_FSH -> "composite-fsh";
case VERTEX_REPLAY -> "vertex-replay";
};
}
private static void emitImageDecls(StringBuilder out, Set<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared) {
private static List<String> sortedImages(Set<String> writtenImages) {
final List<String> sorted = new ArrayList<>(writtenImages);
Collections.sort(sorted);
return sorted;
}
private static void emitImageDecls(StringBuilder out, Set<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared) {
final List<String> sorted = sortedImages(writtenImages);
final Map<String, ImageInformation> infos = activeCustomImages;
for (int i = 0; i < sorted.size(); i++) {
final String name = sorted.get(i);
final ImageDecl d = declared.get(name);
if (d == null) continue;
final InternalTextureFormat fmt = (infos != null && infos.get(name) != null) ? infos.get(name).internalTextureFormat() : null;
final String layoutInner = (fmt != null) ? "binding = " + i + ", " + fmt.name().toLowerCase(Locale.ROOT) : "binding = " + i;
final String format = fmt != null ? fmt.name().toLowerCase(Locale.ROOT) : d.layoutFormat();
final String layoutInner = (format != null) ? "binding = " + i + ", " + format : "binding = " + i;
final String qualifier = nonWriteonlyImages.contains(name) ? "" : "writeonly ";
out.append("layout(").append(layoutInner).append(") ").append(qualifier).append("uniform ").append(d.glslType()).append(' ').append(name).append(";\n");
}
@@ -604,7 +946,7 @@ public final class RwImageStoreExtractor {
out.append("void _vert_init() {}\n");
}
private static void emitCompositeVshPrelude(StringBuilder out, Set<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared) {
private static void emitCompositeVshPrelude(StringBuilder out, Set<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared, Set<String> quadInputs, boolean definesFtransform) {
out.append("layout(local_size_x = 64) in;\n\n");
emitImageDecls(out, writtenImages, nonWriteonlyImages, declared);
out.append("\nconst vec4 _vg_quad[4] = vec4[4](\n");
@@ -614,10 +956,67 @@ public final class RwImageStoreExtractor {
out.append(" vec4(0.0, 0.0, 0.0, 1.0));\n");
out.append("vec4 _vg_gl_vertex;\n");
out.append("int _vg_gl_vertex_id;\n");
out.append("vec4 _vg_sink_pos;\n");
out.append("float _vg_sink_psize;\n");
out.append("float _vg_sink_clip[8];\n");
out.append("#define gl_Vertex _vg_gl_vertex\n");
out.append("#define gl_VertexID _vg_gl_vertex_id\n");
out.append("uniform mat4 gl_ModelViewProjectionMatrix;\n");
out.append("vec4 iris_ftransform() { return gl_ModelViewProjectionMatrix * gl_Vertex; }\n");
out.append("#define gl_Position _vg_sink_pos\n");
out.append("#define gl_PointSize _vg_sink_psize\n");
out.append("#define gl_ClipDistance _vg_sink_clip\n");
for (String input : quadInputs) out.append("vec4 ").append(input).append(";\n");
if (!definesFtransform) {
out.append("uniform mat4 gl_ModelViewProjectionMatrix;\n");
out.append("vec4 iris_ftransform() { return gl_ModelViewProjectionMatrix * gl_Vertex; }\n");
}
}
private static void emitVertexReplayPrelude(StringBuilder out, Set<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared, ReplayLayout replay) {
final List<VertexInput> inputs = replay.inputs();
out.append("layout(local_size_x = 64) in;\n\n");
emitImageDecls(out, writtenImages, nonWriteonlyImages, declared);
final Map<String, ImageInformation> infos = activeCustomImages;
for (int i = 0; i < replay.readImages().size(); i++) {
final ImageDecl d = declared.get(replay.readImages().get(i));
final InternalTextureFormat fmt = infos.get(d.name()) != null ? infos.get(d.name()).internalTextureFormat() : null;
final String format = fmt != null ? fmt.name().toLowerCase(Locale.ROOT) : d.layoutFormat();
out.append("layout(binding = ").append(writtenImages.size() + i).append(format != null ? ", " + format : "").append(") readonly uniform ")
.append(d.glslType()).append(' ').append(d.name()).append(";\n");
}
out.append('\n');
for (int i = 0; i < VG_REPLAY_VERTEX_BUFFER_COUNT; i++) {
out.append("layout(std430, binding = ").append(VG_REPLAY_VERTEX_BUFFER_BINDING + i)
.append(") readonly buffer _VgVertices").append(i).append(" { uint d[]; } _vg_vertices").append(i).append(";\n");
}
out.append("layout(std430, binding = ").append(VG_REPLAY_INDEX_BUFFER_BINDING).append(") readonly buffer _VgIndices { uint d[]; } _vg_indices;\n\n");
final int slots = Math.max(1, inputs.size());
out.append("uniform uvec4 _vg_attr[").append(slots).append("];\n");
out.append("uniform vec4 _vg_attrDefault[").append(slots).append("];\n");
out.append("uniform ivec4 _vg_draw;\n");
out.append("uniform ivec3 _vg_index;\n");
out.append("uniform uvec2 _vg_restart;\n");
out.append("uniform int _vg_invocationBase;\n\n");
out.append("vec4 _vg_sink_pos;\n");
out.append("float _vg_sink_psize;\n");
out.append("float _vg_sink_clip[8];\n");
out.append("int _vg_vertex_id;\n");
out.append("int _vg_instance_id;\n");
out.append("#define gl_Position _vg_sink_pos\n");
out.append("#define gl_PointSize _vg_sink_psize\n");
out.append("#define gl_ClipDistance _vg_sink_clip\n");
out.append("#define gl_VertexID _vg_vertex_id\n");
out.append("#define gl_VertexIndex _vg_vertex_id\n");
out.append("#define gl_InstanceID _vg_instance_id\n");
out.append("#define gl_InstanceIndex _vg_instance_id\n");
for (String suffix : new String[] { "", "ARB" }) {
out.append("#define gl_BaseVertex").append(suffix).append(" _vg_draw.w\n");
out.append("#define gl_BaseInstance").append(suffix).append(" 0\n");
out.append("#define gl_DrawID").append(suffix).append(" 0\n");
}
out.append('\n');
for (VertexInput input : inputs) out.append(input.glslType()).append(' ').append(input.name()).append(";\n");
out.append('\n');
out.append(ShaderLoader.getShaderSource("angelica:include/vertex_replay_fetch.glsl"));
}
private static void emitCompositeFshPrelude(StringBuilder out, Set<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared) {
@@ -643,7 +1042,7 @@ public final class RwImageStoreExtractor {
}
}
private static void emitDispatchMain(StringBuilder out, RwExtractMode mode, List<AttributeSlot> attrs) {
private static void emitDispatchMain(StringBuilder out, RwExtractMode mode, List<AttributeSlot> attrs, List<VertexInput> inputs, Set<String> quadInputs) {
out.append("void main() {\n");
switch (mode) {
case CHUNK -> {
@@ -665,6 +1064,7 @@ public final class RwImageStoreExtractor {
out.append(" uint vid = gl_GlobalInvocationID.x;\n");
out.append(" if (vid >= 4u) return;\n");
out.append(" _vg_gl_vertex = _vg_quad[vid];\n");
for (String input : quadInputs) out.append(" ").append(input).append(" = _vg_quad[vid];\n");
out.append(" _vg_gl_vertex_id = int(vid);\n");
out.append(" _vg_body();\n");
}
@@ -674,10 +1074,37 @@ public final class RwImageStoreExtractor {
out.append(" _vg_gl_fragcoord_raw = vec4(vec2(px) + 0.5, 0.0, 1.0);\n");
out.append(" _vg_body();\n");
}
case VERTEX_REPLAY -> {
out.append(" uint _vg_g = uint(_vg_invocationBase) + gl_GlobalInvocationID.x;\n");
out.append(" uint _vg_count = uint(_vg_draw.y);\n");
out.append(" if (_vg_count == 0u || _vg_g >= _vg_count * uint(_vg_draw.z)) return;\n");
out.append(" uint _vg_v = _vg_g % _vg_count;\n");
out.append(" uint _vg_i = _vg_g / _vg_count;\n");
out.append(" uint _vg_vertex = uint(_vg_draw.x) + _vg_v;\n");
out.append(" if (_vg_index.x != 0) {\n");
out.append(" uint _vg_index_value = _vg_v < uint(_vg_index.z) ? _vg_read(4u, uint(_vg_index.y) + _vg_v * uint(_vg_index.x), uint(_vg_index.x)) : 0u;\n");
out.append(" if (_vg_restart.x != 0u && _vg_index_value == _vg_restart.y) return;\n");
out.append(" _vg_vertex = _vg_index_value + uint(_vg_draw.w);\n");
out.append(" }\n");
out.append(" _vg_vertex_id = int(_vg_vertex);\n");
out.append(" _vg_instance_id = int(_vg_i);\n");
for (int k = 0; k < inputs.size(); k++) {
final VertexInput input = inputs.get(k);
out.append(" ").append(input.name()).append(" = ").append(input.glslType()).append('(')
.append(fetchFunctionFor(input.glslType())).append('(').append(k).append("u, _vg_vertex, _vg_i));\n");
}
out.append(" _vg_body();\n");
}
}
out.append("}\n");
}
private static String fetchFunctionFor(String glslType) {
if (glslType.startsWith("u")) return "_vg_uint";
if (glslType.startsWith("i")) return "_vg_int";
return "_vg_float";
}
private static List<AttributeSlot> collectVertexAttributes(Set<String> chunkAttrsUsed, Map<String, String> chunkAttrAstTypes) {
final List<AttributeSlot> out = new ArrayList<>();
for (String name : chunkAttrsUsed) {
@@ -35,6 +35,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -237,7 +238,8 @@ public class ShaderTransformer {
final ShaderDiskCache.Key k = ShaderDiskCache.key("iris-transform").str(patch.name());
parameters.appendDiskKey(k);
k.i(RenderSystem.getMaxGlslVersion()).b(RenderSystem.supportsSSBO()).b(RenderSystem.supportsImageLoadStore())
.b(RenderSystem.isGLES()).b(BackendManager.RENDER_BACKEND.isSDLGPU()).i(maxSupportedHoistVersion);
.b(RenderSystem.isGLES()).b(BackendManager.RENDER_BACKEND.isSDLGPU()).b(BackendManager.RENDER_BACKEND.supportsComputeImageAtomics()).str(RwImageStoreExtractor.atomicCustomImagesKey())
.i(maxSupportedHoistVersion);
if (enabledRequirements == null) {
k.i(-1);
} else {
@@ -559,18 +561,23 @@ public class ShaderTransformer {
artifactsOut.put(shaderType, new StageArtifact(treeHolder[0], formattedShader.length() - printedBody.length()));
}
}
if (BackendManager.RENDER_BACKEND.isSDLGPU()) extractRwImageStores(result, patchType, artifactsOut);
if (BackendManager.RENDER_BACKEND.isSDLGPU()) extractRwImageStores(result, patchType, artifactsOut, RwImageStoreExtractor::supportsAtomics);
watch.stop();
Iris.logger.info("[Load #{}] Transformed shader for {} in {}", Iris.getShaderPackLoadId(), patchType.name(), watch);
return result;
}
static void extractRwImageStores(EnumMap<PatchShaderType, String> result, Patch patchType, EnumMap<PatchShaderType, StageArtifact> artifactsOut) {
static void extractRwImageStores(EnumMap<PatchShaderType, String> result, Patch patchType, EnumMap<PatchShaderType, StageArtifact> artifactsOut, Predicate<String> imageAtomics) {
if (result.containsKey(PatchShaderType.COMPUTE)) return;
if (patchType == Patch.ATTRIBUTES) {
extractVertexReplay(result, artifactsOut, imageAtomics);
return;
}
final String vsh = result.get(PatchShaderType.VERTEX);
final String fsh = result.get(PatchShaderType.FRAGMENT);
final RwImageStoreExtractor.Result vshResult = (vsh != null) ? RwImageStoreExtractor.tryExtract(vsh, PatchShaderType.VERTEX, patchType.name()) : null;
final RwImageStoreExtractor.RwExtractMode vertexMode = patchType == Patch.COMPOSITE ? RwImageStoreExtractor.RwExtractMode.COMPOSITE_VSH : null;
final RwImageStoreExtractor.Result vshResult = (vsh != null) ? RwImageStoreExtractor.tryExtract(vsh, PatchShaderType.VERTEX, patchType.name(), vertexMode) : null;
final RwImageStoreExtractor.Result fshResult = (fsh != null) ? RwImageStoreExtractor.tryExtract(fsh, PatchShaderType.FRAGMENT, patchType.name()) : null;
if (vshResult != null && fshResult != null) {
@@ -581,14 +588,46 @@ public class ShaderTransformer {
final PatchShaderType stage = vshResult != null ? PatchShaderType.VERTEX : PatchShaderType.FRAGMENT;
result.put(stage, extracted.strippedSource());
if (artifactsOut != null) artifactsOut.remove(stage);
if (patchType == Patch.COMPOSITE || patchType == Patch.CELERITAS_TERRAIN) {
if (patchType != Patch.COMPOSITE && patchType != Patch.CELERITAS_TERRAIN) {
Iris.logger.info("[RwImageStoreExtractor] Dropped image writes from {} (written={})", patchType.name(), extracted.writtenImages());
} else if (needsUnsupportedAtomics(extracted, imageAtomics)) {
Iris.logger.info("[RwImageStoreExtractor] Dropped image writes from {}, they need image atomics this backend cannot run (written={})", patchType.name(), extracted.writtenImages());
} else {
result.put(PatchShaderType.COMPUTE, extracted.computeSource());
Iris.logger.info("[RwImageStoreExtractor] Extracted compute pre-pass for {} (mode={}, written={})", patchType.name(), extracted.mode(), extracted.writtenImages());
} else {
Iris.logger.info("[RwImageStoreExtractor] Dropped image writes from {} (written={})", patchType.name(), extracted.writtenImages());
}
}
private static boolean needsUnsupportedAtomics(RwImageStoreExtractor.Result extracted, Predicate<String> imageAtomics) {
return !extracted.atomicImages().stream().allMatch(imageAtomics);
}
private static void extractVertexReplay(EnumMap<PatchShaderType, String> result, EnumMap<PatchShaderType, StageArtifact> artifactsOut, Predicate<String> imageAtomics) {
final String fsh = result.get(PatchShaderType.FRAGMENT);
final RwImageStoreExtractor.Result fshResult = (fsh != null) ? RwImageStoreExtractor.tryExtract(fsh, PatchShaderType.FRAGMENT, Patch.ATTRIBUTES.name()) : null;
if (fshResult != null) {
result.put(PatchShaderType.FRAGMENT, fshResult.strippedSource());
if (artifactsOut != null) artifactsOut.remove(PatchShaderType.FRAGMENT);
Iris.logger.info("[RwImageStoreExtractor] Dropped fragment image writes from ATTRIBUTES (written={})", fshResult.writtenImages());
}
final String vsh = result.get(PatchShaderType.VERTEX);
final RwImageStoreExtractor.Result vshResult = (vsh != null) ? RwImageStoreExtractor.tryExtractVertexReplay(vsh) : null;
if (vshResult == null) return;
result.put(PatchShaderType.VERTEX, vshResult.strippedSource());
if (artifactsOut != null) artifactsOut.remove(PatchShaderType.VERTEX);
if (vshResult.computeSource() == null) {
Iris.logger.info("[RwImageStoreExtractor] Dropped vertex image writes from ATTRIBUTES, an input cannot be replayed (written={})", vshResult.writtenImages());
return;
}
if (needsUnsupportedAtomics(vshResult, imageAtomics)) {
Iris.logger.info("[RwImageStoreExtractor] Dropped vertex image writes from ATTRIBUTES, they need image atomics this backend cannot run (written={})", vshResult.writtenImages());
return;
}
result.put(PatchShaderType.COMPUTE, vshResult.computeSource());
Iris.logger.info("[RwImageStoreExtractor] Extracted vertex replay for ATTRIBUTES (written={})", vshResult.writtenImages());
}
private static void doTransform(Transformer transformer, Patch patchType, Parameters parameters, int versionInt) {
switch (patchType) {
case CELERITAS_TERRAIN:
@@ -717,12 +756,6 @@ public class ShaderTransformer {
}
}
public static void addIfNotExistsType(Transformer transformer, String name, String type) {
if (!transformer.hasVariable(name)) {
transformer.injectVariable(type + " " + name + ";");
}
}
private static final class CeleritasHeader {
static final String VALUE = computeCeleritasHeader();
}
@@ -0,0 +1,184 @@
{
"providers": [
{
"type": "bitmap",
"file": "minecraft:font/nonlatin_european.png",
"ascent": 7,
"chars": [
"\u00a1\u2030\u00ad\u00b7\u20b4\u2260\u00bf\u00d7\u00d8\u00de\u04bb\u00f0\u00f8\u00fe\u0391\u0392",
"\u0393\u0394\u0395\u0396\u0397\u0398\u0399\u039a\u039b\u039c\u039d\u039e\u039f\u03a0\u03a1\u03a3",
"\u03a4\u03a5\u03a6\u03a7\u03a8\u03a9\u03b1\u03b2\u03b3\u03b4\u03b5\u03b6\u03b7\u03b8\u03b9\u03ba",
"\u03bb\u03bc\u03bd\u03be\u03bf\u03c0\u03c1\u03c2\u03c3\u03c4\u03c5\u03c6\u03c7\u03c8\u03c9\u0402",
"\u0405\u0406\u0408\u0409\u040a\u040b\u0410\u0411\u0412\u0413\u0414\u0415\u0416\u0417\u0418\u041a",
"\u041b\u041c\u041d\u041e\u041f\u0420\u0421\u0422\u0423\u0424\u0425\u0426\u0427\u0428\u0429\u042a",
"\u042b\u042c\u042d\u042e\u042f\u0430\u0431\u0432\u0433\u0434\u0435\u0436\u0437\u0438\u043a\u043b",
"\u043c\u043d\u043e\u043f\u0440\u0441\u0442\u0443\u0444\u0445\u0446\u0447\u0448\u0449\u044a\u044b",
"\u044c\u044d\u044e\u044f\u0454\u0455\u0456\u0458\u0459\u045a\u2013\u2014\u2018\u2019\u201c\u201d",
"\u201e\u2026\u204a\u2190\u2191\u2192\u2193\u21c4\uff0b\u018f\u0259\u025b\u026a\u04ae\u04af\u04e8",
"\u04e9\u02bb\u02cc\u037e\u0138\u1e9e\u00df\u20bd\u20ac\u0462\u0463\u0474\u0475\u04c0\u0472\u0473",
"\u2070\u00b9\u00b3\u2074\u2075\u2076\u2077\u2078\u2079\u207a\u207b\u207c\u207d\u207e\u2071\u2122",
"\u0294\u0295\u29c8\u2694\u2620\u049a\u049b\u0492\u0493\u04b0\u04b1\u04d8\u04d9\u0496\u0497\u04a2",
"\u04a3\u04ba\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9\u05db\u05dc\u05de\u05dd",
"\u05e0\u05df\u05e1\u05e2\u05e4\u05e3\u05e6\u05e5\u05e7\u05e8\u00a2\u00a4\u00a5\u00a9\u00ae\u00b5",
"\u00b6\u00bc\u00bd\u00be\u0387\u2010\u201a\u2020\u2021\u2022\u2031\u2032\u2033\u2034\u2035\u2036",
"\u2037\u2039\u203a\u203b\u203c\u203d\u2042\u2048\u2049\u204b\u204e\u204f\u2051\u2052\u2057\u2117",
"\u2212\u2213\u221e\u2600\u2601\u2608\u0404\u2632\u2635\u263d\u2640\u2642\u26a5\u2660\u2663\u2665",
"\u2666\u2669\u266a\u266b\u266c\u266d\u266e\u266f\u2680\u2681\u2682\u2683\u2684\u2685\u02ac\u26a1",
"\u26cf\u2714\u2744\u274c\u2764\u2b50\u2e18\u2e2e\u2e35\u2e38\u2e41\u2e4b\u295d\u1614\u0190\u07c8",
"\u03db\u3125\u2c6f\u15fa\u0186\u15e1\u018e\u2132\u2141\ua7b0\ua780\u0500\ua779\u1d1a\u27d8\u2229",
"\u0245\u2144\u0250\u0254\u01dd\u025f\u1d77\u0265\u1d09\u027e\u029e\ua781\u026f\u0279\u0287\u028c",
"\u028d\u028e\u0531\u0532\u0533\u0534\u0536\u0537\u0539\u053a\u053b\u053c\u053d\u053e\u053f\u0540",
"\u0541\u0542\u0543\u0544\u0545\u0546\u0547\u0548\u0549\u054b\u054c\u054d\u054e\u054f\u0550\u0551",
"\u0552\u0553\u0554\u0555\u0556\u0559\u0561\u0562\u0563\u0564\u0565\u0566\u0567\u0568\u0569\u056a",
"\u056b\u056c\u056d\u056e\u056f\u0570\u0571\u0572\u0573\u0574\u0575\u0576\u0577\u0578\u0579\u057a",
"\u057b\u057c\u057d\u057e\u057f\u0580\u0581\u0582\u0583\u0584\u0585\u0586\u0587\u05e9\u05ea\u0538",
"\u055a\u055b\u055c\u055d\u055e\u055f\u0560\u0588\u058f\u00af\u017f\u01b7\u0292\u01f7\u01bf\u021c",
"\u021d\u0224\u0225\u02d9\ua75a\ua75b\u2011\u214b\u23cf\u23e9\u23ea\u23ed\u23ee\u23ef\u23f4\u23f5",
"\u23f6\u23f7\u23f8\u23f9\u23fa\u23fb\u23fc\u23fd\u2b58\u25b2\u25b6\u25bc\u25c0\u25cf\u25e6\u25d8",
"\u2693\u26e8\u0132\u0133\u01c9\ua728\ua729\ua739\ua73b\ufb00\ufb01\ufb02\ufb03\ufb05\ufffd\u0535",
"\u054a\u16a0\u16a2\u16a3\u16a4\u16a5\u16a6\u16a7\u16a8\u16a9\u16aa\u16ab\u16ac\u16ad\u16ae\u16af",
"\u16b0\u16b1\u16b2\u16b3\u16b4\u16b6\u16b7\u16b8\u16b9\u16ba\u16bb\u16bc\u16bd\u16be\u16bf\u16c0",
"\u16c1\u16c2\u16c3\u16c4\u16c5\u16c6\u16c7\u16c8\u16c9\u16ca\u16cb\u16cc\u16cd\u16ce\u16cf\u16d0",
"\u16d1\u16d2\u16d3\u16d4\u16d5\u16d6\u16d7\u16d8\u16d9\u16da\u16db\u16dc\u16dd\u16de\u16df\u16e0",
"\u16e1\u16e2\u16e3\u16e4\u16e5\u16e6\u16e7\u16e8\u16e9\u16ea\u16eb\u16ec\u16ed\u16ee\u16ef\u16f0",
"\u16f1\u16f2\u16f3\u16f4\u16f5\u16f6\u16f7\u16f8\u263a\u263b\u00a6\u2639\u05da\u05f3\u05f4\u05f0",
"\u05f1\u05f2\u05be\u05c3\u05c6\u00b4\u00a8\u1d00\u0299\u1d04\u1d05\u1d07\ua730\u0262\u029c\u1d0a",
"\u1d0b\u029f\u1d0d\u0274\u1d0f\u1d18\ua7af\u0280\ua731\u1d1b\u1d1c\u1d20\u1d21\u028f\u1d22\u00a7",
"\u0271\u0273\u0272\u0288\u0256\u0261\u02a1\u0255\u0291\u0278\u029d\u02a2\u027b\u0281\u0266\u028b",
"\u0270\u026c\u026e\u0298\u01c0\u01c3\u01c2\u01c1\u0253\u0257\u1d91\u0284\u0260\u029b\u0267\u026b",
"\u0268\u0289\u028a\u0258\u0275\u0264\u025c\u025e\u0251\u0252\u025a\u025d\u0181\u0189\u0191\u01a9",
"\u01b2\u10a0\u10a1\u10a2\u10a3\u10a4\u10a5\u10a6\u10a7\u10a8\u10a9\u10aa\u10ab\u10ac\u10ad\u10ae",
"\u10af\u10b0\u10b1\u10b2\u10b3\u10b4\u10b5\u10b6\u10b7\u10b8\u10b9\u10ba\u10bb\u10bc\u10bd\u10be",
"\u10bf\u10c0\u10c1\u10c2\u10c3\u10c4\u10c5\u10c7\u10cd\u10d0\u10d1\u10d2\u10d3\u10d4\u10d5\u10d6",
"\u10d7\u10d8\u10d9\u10da\u10db\u10dc\u10dd\u10de\u10df\u10e0\u10e1\u10e2\u10e3\u10e4\u10e5\u10e6",
"\u10e7\u10e8\u10e9\u10ea\u10eb\u10ec\u10ed\u10ee\u10ef\u10f0\u10f1\u10f2\u10f3\u10f4\u10f5\u10f6",
"\u10f7\u10f8\u10f9\u10fa\u10fb\u10fc\u10fd\u10fe\u10ff\ufb4a\ufb2b\ufb4e\ufb44\ufb3b\ufb1f\ufb1d",
"\ufb4b\ufb35\ufb4c\ufb31\ua727\ua726\u027a\u2c71\u02a0\u0297\u0296\u026d\u0277\u027f\u0285\u0286",
"\u0293\u029a\u20aa\u20be\u058a\u2d00\u2d01\u2d02\u2d03\u2d04\u2d05\u2d06\u2d21\u2d07\u2d08\u2d09",
"\u2d0a\u2d0b\u2d0c\u2d22\u2d0d\u2d0e\u2d0f\u2d10\u2d11\u2d12\u2d23\u2d13\u2d14\u2d15\u2d16\u2d17",
"\u2d18\u2d19\u2d1a\u2d1b\u2d1c\u2d1d\u2d1e\u2d24\u2d1f\u2d20\u2d25\u215b\u215c\u215d\u215e\u2153",
"\u2154\u2709\u2602\u2614\u2604\u26c4\u2603\u231b\u231a\u2690\u270e\u2763\u2664\u2667\u2661\u2662",
"\u26c8\u2630\u2631\u2633\u2634\u2636\u2637\u2194\u21d2\u21cf\u21d4\u21f5\u2200\u2203\u2204\u2209",
"\u220b\u220c\u2282\u2283\u2284\u2285\u2227\u2228\u22bb\u22bc\u22bd\u2225\u2262\u22c6\u2211\u22a4",
"\u22a5\u22a2\u22a8\u2254\u2201\u2234\u2235\u221b\u221c\u2202\u22c3\u2286\u2287\u25a1\u25b3\u25b7",
"\u25bd\u25c1\u25c6\u25c7\u25cb\u25ce\u2606\u2605\u2718\u2080\u2081\u2082\u2083\u2084\u2085\u2086",
"\u2087\u2088\u2089\u208a\u208b\u208c\u208d\u208e\u222b\u222e\u221d\u2300\u2302\u2318\u3012\u027c",
"\u0184\u0185\u1e9f\u023d\u019a\u019b\u0220\u019e\u019f\u01a7\u01a8\u01aa\u01b8\u01b9\u01bb\u01bc",
"\u01bd\u01be\u0221\u0234\u0235\u0236\u023a\u2c65\u023b\u023c\u0246\u0247\u023e\u2c66\u0241\u0242",
"\u0243\u0244\u0248\u0249\u024a\u024b\u024c\u024d\u024e\u024f\u1e9c\u1e9d\u1efc\u1efd\u1efe\u1eff",
"\ua7a8\ua7a9\ud800\udf30\ud800\udf31\ud800\udf32\ud800\udf33\ud800\udf34\ud800\udf35\ud800\udf36\ud800\udf37\ud800\udf38\ud800\udf39\ud800\udf3a\ud800\udf3b\ud800\udf3c\ud800\udf3d",
"\ud800\udf3e\ud800\udf3f\ud800\udf40\ud800\udf41\ud800\udf42\ud800\udf43\ud800\udf44\ud800\udf45\ud800\udf46\ud800\udf47\ud800\udf48\ud800\udf49\ud800\udf4a\ud83c\udf27\ud83d\udd25\ud83c\udf0a",
"\u2150\u2151\u2155\u2156\u2157\u2159\u215a\u215f\u2189\ud83d\udde1\ud83c\udff9\ud83e\ude93\ud83d\udd31\ud83c\udfa3\ud83e\uddea\u2697",
"\u2bea\u2beb\u2c6d\ud83d\udee1\u2702\ud83c\udf56\ud83e\udea3\ud83d\udd14\u23f3\u2691\u20a0\u20a1\u20a2\u20a3\u20a4\u20a5",
"\u20a6\u20a9\u20ab\u20ad\u20ae\u20b0\u20b1\u20b2\u20b3\u20b5\u20b6\u20b7\u20b8\u20b9\u20ba\u20bb",
"\u20bc\u20bf\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
]
},
{
"type": "bitmap",
"file": "minecraft:font/accented.png",
"height": 12,
"ascent": 10,
"chars": [
"\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c6\u00c7\u00c8\u00c9\u00ca\u00cb\u00cc\u00cd\u00ce\u00cf",
"\u00d0\u00d1\u00d2\u00d3\u00d4\u00d5\u00d6\u00d9\u00da\u00db\u00dc\u00dd\u00e0\u00e1\u00e2\u00e3",
"\u00e4\u00e5\u00e6\u00e7\u00ec\u00ed\u00ee\u00ef\u00f1\u00f2\u00f3\u00f4\u00f5\u00f6\u00f9\u00fa",
"\u00fb\u00fc\u00fd\u00ff\u0100\u0101\u0102\u0103\u0104\u0105\u0106\u0107\u0108\u0109\u010a\u010b",
"\u010c\u010d\u010e\u010f\u0110\u0111\u0112\u0113\u0114\u0115\u0116\u0117\u0118\u0119\u011a\u011b",
"\u011c\u011d\u1e20\u1e21\u011e\u011f\u0120\u0121\u0122\u0123\u0124\u0125\u0126\u0127\u0128\u0129",
"\u012a\u012b\u012c\u012d\u012e\u012f\u0130\u0131\u0134\u0135\u0136\u0137\u0139\u013a\u013b\u013c",
"\u013d\u013e\u013f\u0140\u0141\u0142\u0143\u0144\u0145\u0146\u0147\u0148\u014a\u014b\u014c\u014d",
"\u014e\u014f\u0150\u0151\u0152\u0153\u0154\u0155\u0156\u0157\u0158\u0159\u015a\u015b\u015c\u015d",
"\u015e\u015f\u0160\u0161\u0162\u0163\u0164\u0165\u0166\u0167\u0168\u0169\u016a\u016b\u016c\u016d",
"\u016e\u016f\u0170\u0171\u0172\u0173\u0174\u0175\u0176\u0177\u0178\u0179\u017a\u017b\u017c\u017d",
"\u017e\u01fc\u01fd\u01fe\u01ff\u0218\u0219\u021a\u021b\u0386\u0388\u0389\u038a\u038c\u038e\u038f",
"\u0390\u03aa\u03ab\u03ac\u03ad\u03ae\u03af\u03b0\u03ca\u03cb\u03cc\u03cd\u03ce\u0400\u0401\u0403",
"\u0407\u040c\u040d\u040e\u0419\u0439\u0450\u0451\u0452\u0453\u0457\u045b\u045c\u045d\u045e\u045f",
"\u0490\u0491\u1e02\u1e03\u1e0a\u1e0b\u1e1e\u1e1f\u1e22\u1e23\u1e30\u1e31\u1e40\u1e41\u1e56\u1e57",
"\u1e60\u1e61\u1e6a\u1e6b\u1e80\u1e81\u1e82\u1e83\u1e84\u1e85\u1ef2\u1ef3\u00e8\u00e9\u00ea\u00eb",
"\u0149\u01e7\u01eb\u040f\u1e0d\u1e25\u1e5b\u1e6d\u1e92\u1eca\u1ecb\u1ecc\u1ecd\u1ee4\u1ee5\u2116",
"\u0207\u0194\u0263\u0283\u2047\u01f1\u01f2\u01f3\u01c4\u01c5\u01c6\u01c7\u01c8\u01ca\u01cb\u01cc",
"\u2139\u1d6b\ua732\ua733\ua734\ua735\ua736\ua737\ua738\ua73a\ua73c\ua73d\ua74e\ua74f\ua760\ua761",
"\ufb04\ufb06\u16a1\u16b5\u01a0\u01a1\u01af\u01b0\u1eae\u1eaf\u1ea4\u1ea5\u1ebe\u1ebf\u1ed1\u1eda",
"\u1edb\u1ee8\u1ee9\u1eb0\u1eb1\u1ea6\u1ea7\u1ec0\u1ec1\u1ed3\u1edc\u1edd\u1eea\u1eeb\u1ea2\u1ea3",
"\u1eb2\u1eb3\u1ea8\u1ea9\u1eba\u1ebb\u1ed5\u1ede\u1ec2\u1ec3\u1ec8\u1ec9\u1ece\u1ecf\u1ed4\u1edf",
"\u1ee6\u1ee7\u1eec\u1eed\u1ef6\u1ef7\u1ea0\u1ea1\u1eb6\u1eb7\u1eac\u1ead\u1eb8\u1eb9\u1ec6\u1ec7",
"\u1ed8\u1ed9\u1ee2\u1ee3\u1ef0\u1ef1\u1ef4\u1ef5\u1ed0\u0195\u1eaa\u1eab\u1ed6\u1ed7\u1eef\u261e",
"\u261c\u262e\u1eb4\u1eb5\u1ebc\u1ebd\u1ec4\u1ec5\u1ed2\u1ee0\u1ee1\u1eee\u1ef8\u1ef9\u0498\u0499",
"\u04a0\u04a1\u04aa\u04ab\u01f6\u26a0\u24ea\u2460\u2461\u2462\u2463\u2464\u2465\u2466\u2467\u2468",
"\u2469\u246a\u246b\u246c\u246d\u246e\u246f\u2470\u2471\u2472\u2473\u24b6\u24b7\u24b8\u24b9\u24ba",
"\u24bb\u24bc\u24bd\u24be\u24bf\u24c0\u24c1\u24c2\u24c3\u24c4\u24c5\u24c6\u24c7\u24c8\u24c9\u24ca",
"\u24cb\u24cc\u24cd\u24ce\u24cf\u24d0\u24d1\u24d2\u24d3\u24d4\u24d5\u24d6\u24d7\u24d8\u24d9\u24da",
"\u24db\u24dc\u24dd\u24de\u24df\u24e0\u24e1\u24e2\u24e3\u24e4\u24e5\u24e6\u24e7\u24e8\u24e9\u0327",
"\u0282\u0290\u0276\u01cd\u01ce\u01de\u01df\u01fa\u01fb\u0202\u0203\u0226\u0227\u01e0\u01e1\u1e00",
"\u1e01\u0200\u0201\u1e06\u1e07\u1e04\u1e05\u1d6c\u1e08\u1e09\u1e10\u1e11\u1e12\u1e13\u1e0e\u1e0f",
"\u1e0c\u1d6d\u1e14\u1e15\u1e16\u1e17\u1e18\u1e19\u1e1c\u1e1d\u0228\u0229\u1e1a\u1e1b\u0204\u0205",
"\u0206\u1d6e\u01f4\u01f5\u01e6\u1e26\u1e27\u1e28\u1e29\u1e2a\u1e2b\u021e\u021f\u1e24\u1e96\u1e2e",
"\u1e2f\u020a\u020b\u01cf\u01d0\u0208\u0209\u1e2c\u1e2d\u01f0\u0237\u01e8\u01e9\u1e32\u1e33\u1e34",
"\u1e35\u1e3a\u1e3b\u1e3c\u1e3d\u1e36\u1e37\u1e38\u1e39\u2c62\u1e3e\u1e3f\u1e42\u1e43\u1d6f\u1e44",
"\u1e45\u1e46\u1e47\u1e4a\u1e4b\u01f8\u01f9\u1e48\u1e49\u1d70\u01ec\u01ed\u022c\u022d\u1e4c\u1e4d",
"\u1e4e\u1e4f\u1e50\u1e51\u1e52\u1e53\u020e\u020f\u022a\u022b\u01d1\u01d2\u022e\u022f\u0230\u0231",
"\u020c\u020d\u01ea\u1e54\u1e55\u1d71\u0212\u0213\u1e58\u1e59\u1e5c\u1e5d\u1e5e\u1e5f\u0210\u0211",
"\u1e5a\u1d73\u1d72\u1e64\u1e65\u1e66\u1e67\u1e62\u1e63\u1e68\u1e69\u1d74\u1e70\u1e71\u1e6e\u1e6f",
"\u1e6c\u1e97\u1d75\u1e72\u1e73\u1e76\u1e77\u1e78\u1e79\u1e7a\u1e7b\u01d3\u01d4\u01d5\u01d6\u01d7",
"\u01d8\u01d9\u01da\u01db\u01dc\u1e74\u1e75\u0214\u0215\u0216\u1e7e\u1e7f\u1e7c\u1e7d\u1e86\u1e87",
"\u1e88\u1e89\u1e98\u1e8c\u1e8d\u1e8a\u1e8b\u0232\u0233\u1e8e\u1e8f\u1e99\u1e94\u1e95\u1e90\u1e91",
"\u1e93\u1d76\u01ee\u01ef\u1e9b\ua73e\ua73f\u01e2\u01e3\u1d7a\u1efb\u1d02\u1d14\uab63\u0238\u02a3",
"\u02a5\u02a4\u02a9\u02aa\u02ab\u0239\u02a8\u02a6\u02a7\uab50\uab51\u20a7\u1efa\ufb2e\ufb2f\u0180",
"\u0182\u0183\u0187\u0188\u018a\u018b\u018c\u0193\u01e4\u01e5\u0197\u0196\u0269\u0198\u0199\u019d",
"\u01a4\u01a5\u027d\u01a6\u01ac\u01ad\u01ab\u01ae\u0217\u01b1\u019c\u01b3\u01b4\u01b5\u01b6\u01a2",
"\u01a3\u0222\u0223\u02ad\u02ae\u02af\ufb14\ufb15\ufb17\ufb16\ufb13\u04d0\u04d1\u04d2\u04d3\u04f6",
"\u04f7\u0494\u0495\u04d6\u04d7\u04bc\u04bd\u04be\u04bf\u04da\u04db\u04dc\u04dd\u04c1\u04c2\u04de",
"\u04df\u04e2\u04e3\u04e4\u04e5\u04e6\u04e7\u04ea\u04eb\u04f0\u04f1\u04ee\u04ef\u04f2\u04f3\u04f4",
"\u04f5\u04f8\u04f9\u04ec\u04ed\u0476\u0477\u04d4\u04fa\u0502\ua682\ua680\ua688\u052a\u052c\ua684",
"\u0504\u0510\u04e0\u0506\u048a\u04c3\u049e\u049c\u051e\u051a\u04c5\u052e\u0512\u0520\u0508\u0514",
"\u04cd\u04c9\u0528\u04c7\u04a4\u0522\u050a\u04a8\u0524\u04a6\u048e\u0516\u050c\ua690\u04ac\ua68a",
"\ua68c\u050e\u04b2\u04fc\u04fe\u0526\ua694\u04b4\ua68e\u04b6\u04cb\u04b8\ua692\ua696\ua686\u048c",
"\u0518\u051c\u04d5\u04fb\u0503\ua683\ua681\ua689\u052b\u052d\ua685\u0505\u0511\u04e1\u0507\u048b",
"\u04c4\u049f\u049d\u051f\u051b\u04c6\u052f\u0513\u0521\u0509\u0515\u04ce\u04ca\u0529\u04c8\u04a5",
"\u0523\u050b\u04a9\u0525\u04a7\u048f\u0517\u050d\ua691\u04ad\ua68b\ua68d\u050f\u04b3\u04fd\u04ff",
"\u0527\ua695\u04b5\ua68f\u04b7\u04cc\u04b9\ua693\ua697\ua687\u048d\u0519\u051d\u1f08\u1f00\u1f09",
"\u1f01\u1f0a\u1f02\u1f0b\u1f03\u1f0c\u1f04\u1f0d\u1f05\u1f0e\u1f06\u1f0f\u1f07\u1fba\u1f70\u1fb8",
"\u1fb0\u1fb9\u1fb1\u1fbb\u1f71\u1f88\u1f80\u1f89\u1f81\u1f8a\u1f82\u1f8b\u1f83\u1f8c\u1f84\u1f8d",
"\u1f85\u1f8e\u1f86\u1f8f\u1f87\u1fbc\u1fb4\u1fb6\u1fb7\u1fb2\u1fb3\u1f18\u1f10\u1f19\u1f11\u1f1a",
"\u1f12\u1f1b\u1f13\u1f1c\u1f14\u1f1d\u1f15\u1fc8\u1fc9\u1f72\u1f73\u1f28\u1f20\u1fca\u1f74\u1f29",
"\u1f21\u1f2a\u1f22\u1f2b\u1f23\u1f2c\u1f24\u1f2d\u1f25\u1f2e\u1f26\u1f2f\u1f27\u1f98\u1f90\u1f99",
"\u1f91\u1f9a\u1f92\u1f9b\u1f93\u1f9c\u1f94\u1f9d\u1f95\u1f9e\u1f96\u1f9f\u1f97\u1fcb\u1f75\u1fcc",
"\u1fc3\u1fc2\u1fc4\u1fc6\u1fc7\u1fda\u1f76\u1fdb\u1f77\u1f38\u1f30\u1f39\u1f31\u1f3a\u1f32\u1f3b",
"\u1f33\u1f3c\u1f34\u1f3d\u1f35\u1f3e\u1f36\u1f3f\u1f37\u1fd8\u1fd0\u1fd9\u1fd1\u1fd2\u1fd3\u1fd6",
"\u1fd7\u1ff8\u1f78\u1ff9\u1f79\u1f48\u1f40\u1f49\u1f41\u1f4a\u1f42\u1f4b\u1f43\u1f4c\u1f44\u1f4d",
"\u1f45\u1fec\u1fe4\u1fe5\u1fea\u1f7a\u1feb\u1f7b\u1f59\u1f51\u1f5b\u1f53\u1f5d\u1f55\u1f5f\u1f57",
"\u1fe8\u1fe0\u1fe9\u1fe1\u03d3\u03d4\u1fe2\u1fe3\u1fe7\u1f50\u1f52\u1f54\u1fe6\u1f56\u1ffa\u1f7c",
"\u1ffb\u1f7d\u1f68\u1f60\u1f69\u1f61\u1f6a\u1f62\u1f6b\u1f63\u1f6c\u1f64\u1f6d\u1f65\u1f6e\u1f66",
"\u1f6f\u1f67\u1fa8\u1fa0\u1fa9\u1fa1\u1faa\u1fa2\u1fab\u1fa3\u1fac\u1fa4\u1fad\u1fa5\u1fae\u1fa6",
"\u1faf\u1fa7\u1ffc\u1ff3\u1ff2\u1ff4\u1ff6\u1ff7\u262f\u2610\u2611\u2612\u018d\u01ba\u2c7e\u023f",
"\u2c7f\u0240\u1d80\ua7c4\ua794\u1d81\u1d82\u1d83\ua795\u1d84\u1d85\u1d86\u1d87\u1d88\u1d89\u1d8a",
"\u1d8b\u1d8c\u1d8d\ua7c6\u1d8e\u1d8f\u1d90\u1d92\u1d93\u1d94\u1d95\u1d96\u1d97\u1d98\u1d99\u1d9a",
"\u1e9a\u2152\u2158\u20a8\u20af\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
]
},
{
"type": "bitmap",
"file": "minecraft:font/ascii.png",
"ascent": 7,
"chars": [
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000",
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000",
"\u0020\u0021\u0022\u0023\u0024\u0025\u0026\u0027\u0028\u0029\u002a\u002b\u002c\u002d\u002e\u002f",
"\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039\u003a\u003b\u003c\u003d\u003e\u003f",
"\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004a\u004b\u004c\u004d\u004e\u004f",
"\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0057\u0058\u0059\u005a\u005b\u005c\u005d\u005e\u005f",
"\u0060\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006a\u006b\u006c\u006d\u006e\u006f",
"\u0070\u0071\u0072\u0073\u0074\u0075\u0076\u0077\u0078\u0079\u007a\u007b\u007c\u007d\u007e\u0000",
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000",
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u00a3\u0000\u0000\u0192",
"\u0000\u0000\u0000\u0000\u0000\u0000\u00aa\u00ba\u0000\u0000\u00ac\u0000\u0000\u0000\u00ab\u00bb",
"\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510",
"\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567",
"\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580",
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u2205\u2208\u0000",
"\u2261\u00b1\u2265\u2264\u2320\u2321\u00f7\u2248\u00b0\u2219\u0000\u221a\u207f\u00b2\u25a0\u0000"
]
}
]
}
@@ -0,0 +1,103 @@
// Taken mostly from mesa
uint _vg_word(uint slot, uint i) {
switch (slot) {
case 0u: return _vg_vertices0.d[i];
case 1u: return _vg_vertices1.d[i];
case 2u: return _vg_vertices2.d[i];
case 3u: return _vg_vertices3.d[i];
default: return _vg_indices.d[i];
}
}
uint _vg_read(uint slot, uint at, uint size) {
uint shift = (at & 3u) * 8u;
uint value = _vg_word(slot, at >> 2) >> shift;
if (shift + size * 8u > 32u) value |= _vg_word(slot, (at >> 2) + 1u) << (32u - shift);
return size == 4u ? value : value & ((1u << (size * 8u)) - 1u);
}
const uint _VG_COMPONENT_SIZE[12] = uint[12](4u, 2u, 1u, 1u, 2u, 2u, 4u, 4u, 4u, 4u, 4u, 4u);
bool _vg_enabled(uint a) {
return (_vg_attr[a].x & 0x2000u) != 0u;
}
uint _vg_element(uint a, uint vertex, uint instance) {
uvec4 desc = _vg_attr[a];
uint divisor = desc.x >> 16;
uint element = divisor == 0u ? vertex : instance / divisor;
return element > desc.w ? 0u : element;
}
uvec4 _vg_raw(uint a, uint element) {
uvec4 desc = _vg_attr[a];
uint type = (desc.x >> 4) & 15u;
uint at = desc.y + element * desc.z;
bool none = (desc.x & 0x8000u) != 0u;
if (type >= 9u) {
uint packed = none ? 0u : _vg_read(desc.x & 7u, at, 4u);
if (type == 9u) return uvec4(ivec4(int(packed << 22), int(packed << 12), int(packed << 2), int(packed)) >> ivec4(22, 22, 22, 30));
if (type == 10u) return (uvec4(packed) >> uvec4(0u, 10u, 20u, 30u)) & uvec4(1023u, 1023u, 1023u, 3u);
return uvec4(packed, 0u, 0u, 1u);
}
uint count = (desc.x >> 8) & 7u;
uint size = _VG_COMPONENT_SIZE[type];
uvec4 raw = uvec4(0u, 0u, 0u, 1u);
for (uint c = 0u; c < count; c++) {
uint v = none ? 0u : _vg_read(desc.x & 7u, at + c * size, size);
if (type == 2u) v = uint(int(v << 24) >> 24);
else if (type == 4u) v = uint(int(v << 16) >> 16);
raw[c] = v;
}
return raw;
}
vec4 _vg_float(uint a, uint vertex, uint instance) {
if (!_vg_enabled(a)) return _vg_attrDefault[a];
uvec4 desc = _vg_attr[a];
uint type = (desc.x >> 4) & 15u;
uint count = (desc.x >> 8) & 7u;
bool normalized = (desc.x & 0x800u) != 0u;
uvec4 raw = _vg_raw(a, _vg_element(a, vertex, instance));
vec4 v = vec4(0.0, 0.0, 0.0, 1.0);
if (type == 9u) {
vec4 c = vec4(ivec4(raw));
v = normalized ? max(c / vec4(511.0, 511.0, 511.0, 1.0), vec4(-1.0)) : c;
} else if (type == 10u) {
vec4 c = vec4(raw);
v = normalized ? c / vec4(1023.0, 1023.0, 1023.0, 3.0) : c;
} else if (type == 11u) {
uint p = raw.x;
v = vec4(unpackHalf2x16((p & 0x7ffu) << 4).x, unpackHalf2x16((p & 0x3ff800u) >> 7).x,
unpackHalf2x16((p & 0xffc00000u) >> 17).x, 1.0);
} else {
for (uint c = 0u; c < count; c++) {
uint r = raw[c];
switch (type) {
case 0u: v[c] = uintBitsToFloat(r); break;
case 1u: v[c] = unpackHalf2x16(r).x; break;
case 2u: v[c] = normalized ? max(float(int(r)) / 127.0, -1.0) : float(int(r)); break;
case 3u: v[c] = normalized ? float(r) / 255.0 : float(r); break;
case 4u: v[c] = normalized ? max(float(int(r)) / 32767.0, -1.0) : float(int(r)); break;
case 5u: v[c] = normalized ? float(r) / 65535.0 : float(r); break;
case 6u: v[c] = normalized ? max(float(int(r)) / 2147483647.0, -1.0) : float(int(r)); break;
case 7u: v[c] = normalized ? float(r) / 4294967295.0 : float(r); break;
default: v[c] = float(int(r)) / 65536.0; break;
}
}
}
return (desc.x & 0x4000u) != 0u ? v.zyxw : v;
}
ivec4 _vg_int(uint a, uint vertex, uint instance) {
if (!_vg_enabled(a)) return ivec4(_vg_attrDefault[a]);
if ((_vg_attr[a].x & 0x1000u) == 0u) return ivec4(_vg_float(a, vertex, instance));
return ivec4(_vg_raw(a, _vg_element(a, vertex, instance)));
}
uvec4 _vg_uint(uint a, uint vertex, uint instance) {
if (!_vg_enabled(a)) return uvec4(_vg_attrDefault[a]);
if ((_vg_attr[a].x & 0x1000u) == 0u) return uvec4(_vg_float(a, vertex, instance));
return _vg_raw(a, _vg_element(a, vertex, instance));
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

@@ -19,11 +19,11 @@ public class MixinMinecraftServer {
@Surround(method = "run", remap = false)
private void angelica$setServerThread() {
ThreadedBlockData.serverThread = Thread.currentThread();
ThreadedBlockData.setServerThread(Thread.currentThread());
}
@Surround.Finally
private void angelica$clearServerThread() {
ThreadedBlockData.serverThread = null;
ThreadedBlockData.setServerThread(null);
}
}
@@ -22,7 +22,7 @@ public class MixinBlock implements ThreadedBlockData.Getter {
private ThreadedBlockData angelica$serverData;
@Override
public ThreadedBlockData angelica$getThreadData() {
public final ThreadedBlockData angelica$getThreadData() {
Thread t = Thread.currentThread();
if (t == ThreadedBlockData.MAIN_THREAD) {
@@ -30,9 +30,9 @@ public class MixinBlock implements ThreadedBlockData.Getter {
return data != null ? data : (angelica$mainData = createThreadedBlockData());
}
if (t == ThreadedBlockData.serverThread) {
if (t == ThreadedBlockData.getServerThread()) {
ThreadedBlockData data = angelica$serverData;
return (data == null || data.owner != t) ? (angelica$serverData = createThreadedBlockData()) : data;
return (data == null || !data.isCurrent()) ? (angelica$serverData = createThreadedBlockData()) : data;
}
ThreadedBlockData data = angelica$threadData.get();
@@ -2,18 +2,13 @@ package com.gtnewhorizons.angelica.mixins.early.shaders;
import com.gtnewhorizons.angelica.experimental.surround.Surround;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import net.coderbot.iris.gbuffer_overrides.matching.SpecialCondition;
import net.coderbot.iris.layer.GbufferPrograms;
import net.coderbot.iris.shaderpack.materialmap.NamespacedId;
import net.coderbot.iris.uniforms.CapturedRenderingState;
import com.gtnewhorizons.angelica.api.EnderDragonBeams;
import net.minecraft.client.renderer.entity.RenderDragon;
import net.minecraft.entity.boss.EntityDragon;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.gen.Invoker;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Mixin allows devs to target the ender crystal beams and dragon death rays as separate entities.
@@ -21,43 +16,24 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
*/
@Mixin(RenderDragon.class)
public abstract class MixinRenderDragon {
@Unique
private static final NamespacedId END_CRYSTAL_BEAM = new NamespacedId("minecraft", "end_crystal_beam");
@Unique
private static final NamespacedId DRAGON_DEATH_RAY = new NamespacedId("minecraft", "dragon_death_rays");
@Unique
private boolean angelica$beamScope;
@Unique
private int angelica$depthPassReplay = 0;
@Invoker("renderEquippedItems")
protected abstract void angelica$invokeRenderEquippedItems(EntityDragon dragon, float partialTicks);
@Inject(
@Surround(
id = "crystalBeam",
method = "doRender(Lnet/minecraft/entity/boss/EntityDragon;DDDFF)V",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glPushMatrix()V", ordinal = 0, shift = At.Shift.AFTER, remap = false)
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;draw()I")
)
private void iris$setBeamEntityId(EntityDragon dragon, double x, double y, double z, float entityYaw, float partialTicks, CallbackInfo ci) {
// Only set the ID if the dragon is being healed by a crystal
if (dragon.healingEnderCrystal != null) {
CapturedRenderingState.INSTANCE.pushCurrentEntityAndItem();
CapturedRenderingState.INSTANCE.setCurrentNamedEntity(END_CRYSTAL_BEAM);
angelica$beamScope = true;
}
private void iris$beginCrystalBeam() {
EnderDragonBeams.beginCrystalBeam();
}
@Inject(
method = "doRender(Lnet/minecraft/entity/boss/EntityDragon;DDDFF)V",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glPopMatrix()V", ordinal = 0, shift = At.Shift.BEFORE, remap = false)
)
private void iris$restoreEntityId(EntityDragon dragon, double x, double y, double z, float entityYaw, float partialTicks, CallbackInfo ci) {
if (angelica$beamScope) {
CapturedRenderingState.INSTANCE.popCurrentEntityAndItem();
angelica$beamScope = false;
}
@Surround.Finally("crystalBeam")
private void iris$endCrystalBeam() {
EnderDragonBeams.endCrystalBeam();
}
/**
@@ -72,9 +48,7 @@ public abstract class MixinRenderDragon {
@Surround.Carry
final boolean deathBeams = angelica$depthPassReplay == 0 && dragon.deathTicks > 0;
if (deathBeams) {
GbufferPrograms.setupSpecialRenderCondition(SpecialCondition.LIGHTNING);
CapturedRenderingState.INSTANCE.pushCurrentEntityAndItem();
CapturedRenderingState.INSTANCE.setCurrentNamedEntity(DRAGON_DEATH_RAY);
EnderDragonBeams.beginDeathRays();
angelica$depthPassReplay++;
GLStateManager.glColorMask(false, false, false, false);
@@ -114,8 +88,6 @@ public abstract class MixinRenderDragon {
@Surround.Finally("deathBeams")
private void angelica$endDeathBeamsLighting(@Surround.Carry boolean deathBeams) {
if (!deathBeams) return;
CapturedRenderingState.INSTANCE.popCurrentEntityAndItem();
GbufferPrograms.teardownSpecialRenderCondition();
if (deathBeams) EnderDragonBeams.endDeathRays();
}
}
@@ -0,0 +1,85 @@
package net.coderbot.iris.pipeline.transform;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.EnumMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CompositeVertexWriteExtractionTest {
// Shaped like Euphoria Patches' End prepare pass
static final String PREPARE_VERTEX = """
#version 130
#extension GL_ARB_shader_image_load_store : enable
uniform float frameTime;
layout(r32i) uniform iimage2D endcrystal_img;
varying vec2 texCoord;
void main() {
texCoord = gl_MultiTexCoord0.xy;
gl_Position = ftransform();
vec4 position0 = ftransform();
if (position0.x < 0.0 && position0.y > 0.0) {
for (int index = 0; index < 20; index++) {
int state = imageLoad(endcrystal_img, ivec2(index, 8)).r - max(1, int(10000.0 * frameTime));
int previousWeight = imageAtomicCompSwap(endcrystal_img, ivec2(index, 8), 0, -1);
imageStore(endcrystal_img, ivec2(index, 5), ivec4(state + previousWeight));
}
}
}
""";
static final String PREPARE_FRAGMENT = """
#version 130
varying vec2 texCoord;
void main() { gl_FragData[0] = vec4(texCoord, 0.0, 1.0); }
""";
@BeforeAll
static void initShaderTransformer() throws Exception {
Reflect.setStatic(RenderSystem.class, "maxGlslVersion", 460);
ShaderTransformer.init();
}
@BeforeEach
void clearCaches() {
TransformPatcher.clearCache();
ShaderTransformer.clearCache();
}
@Test
void compositeVertexWritesRunOverTheQuadNotTerrain() {
final Map<PatchShaderType, String> transformed = TransformPatcher.patchComposite(PREPARE_VERTEX, null, PREPARE_FRAGMENT);
final EnumMap<PatchShaderType, String> result = new EnumMap<>(transformed);
result.remove(PatchShaderType.COMPUTE);
ShaderTransformer.extractRwImageStores(result, Patch.COMPOSITE, null, image -> true);
final String compute = result.get(PatchShaderType.COMPUTE);
assertEquals(RwImageStoreExtractor.RwExtractMode.COMPOSITE_VSH, RwImageStoreExtractor.parseSentinel(compute));
assertTrue(compute.contains("iris_Vertex = _vg_quad[vid];"), compute);
assertTrue(compute.contains("iris_MultiTexCoord0 = _vg_quad[vid];"), compute);
assertFalse(compute.contains("location"), "a location on a former interface variable does not compile in compute");
assertEquals(1, compute.split("vec4 iris_ftransform", -1).length - 1, "the transform's own iris_ftransform is the only one");
assertTrue(compute.contains("#define gl_Position _vg_sink_pos"), compute);
}
@Test
void keptAtomicDropsThePrePassWithoutBackendSupport() {
final EnumMap<PatchShaderType, String> result = new EnumMap<>(TransformPatcher.patchComposite(PREPARE_VERTEX, null, PREPARE_FRAGMENT));
result.remove(PatchShaderType.COMPUTE);
// The CompSwap's result is used, so it stays a real atomic, which native Metal cannot bind
ShaderTransformer.extractRwImageStores(result, Patch.COMPOSITE, null, image -> false);
assertFalse(result.containsKey(PatchShaderType.COMPUTE));
assertFalse(result.get(PatchShaderType.VERTEX).contains("imageAtomic"), result.get(PatchShaderType.VERTEX));
}
}
@@ -0,0 +1,86 @@
package net.coderbot.iris.pipeline.transform;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.glsm.texture.InternalTextureFormat;
import com.gtnewhorizons.angelica.glsm.texture.PixelFormat;
import com.gtnewhorizons.angelica.glsm.texture.PixelType;
import com.gtnewhorizons.angelica.glsm.texture.TextureType;
import net.coderbot.iris.gl.image.ImageInformation;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
class CustomImageAtomicsTest {
private RenderBackend original;
private RenderBackend backend;
@BeforeEach
void installBackend() {
original = BackendManager.RENDER_BACKEND;
backend = mock(RenderBackend.class, delegatesTo(original));
doAnswer(call -> call.<Integer>getArgument(0) == 40 && call.<Integer>getArgument(1) == 10 && call.<Integer>getArgument(2) == 1).when(backend).supportsComputeImageAtomics(anyInt(), anyInt(), anyInt());
Reflect.setStaticFinal(BackendManager.class, "RENDER_BACKEND", RenderBackend.class, backend);
}
@AfterEach
void restoreBackend() {
Reflect.setStaticFinal(BackendManager.class, "RENDER_BACKEND", RenderBackend.class, original);
RwImageStoreExtractor.setActiveCustomImages(null);
}
private static ImageInformation image(String name, int width, int height, boolean relative) {
return new ImageInformation(name, name + "_sampler", TextureType.TEXTURE_2D, PixelFormat.RED_INTEGER, InternalTextureFormat.R32I, PixelType.INT, width, height, 1, false, relative, relative ? 1.0f : 0.0f, relative ? 1.0f : 0.0f);
}
private static Map<String, ImageInformation> images() {
return Map.of("small_img", image("small_img", 40, 10, false), "large_img", image("large_img", 64, 64, false), "screen_img", image("screen_img", 40, 10, true));
}
@Test
void onlyImagesTheBackendAcceptsBySizeKeepAtomics() {
doReturn(false).when(backend).supportsComputeImageAtomics();
RwImageStoreExtractor.setActiveCustomImages(images());
assertTrue(RwImageStoreExtractor.supportsAtomics("small_img"));
assertFalse(RwImageStoreExtractor.supportsAtomics("large_img"));
assertFalse(RwImageStoreExtractor.supportsAtomics("screen_img"));
assertFalse(RwImageStoreExtractor.supportsAtomics("colortex0"));
assertEquals("small_img", RwImageStoreExtractor.atomicCustomImagesKey());
}
@Test
void backendWithImageAtomicsEverywhereNeedsNoPerImageList() {
doReturn(true).when(backend).supportsComputeImageAtomics();
RwImageStoreExtractor.setActiveCustomImages(images());
assertTrue(RwImageStoreExtractor.supportsAtomics("small_img"));
assertTrue(RwImageStoreExtractor.supportsAtomics("large_img"));
assertTrue(RwImageStoreExtractor.supportsAtomics("screen_img"));
assertTrue(RwImageStoreExtractor.supportsAtomics("colortex0"));
assertEquals("", RwImageStoreExtractor.atomicCustomImagesKey());
}
@Test
void clearingTheImagesClearsTheList() {
doReturn(false).when(backend).supportsComputeImageAtomics();
RwImageStoreExtractor.setActiveCustomImages(images());
RwImageStoreExtractor.setActiveCustomImages(null);
assertFalse(RwImageStoreExtractor.supportsAtomics("small_img"));
assertEquals("", RwImageStoreExtractor.atomicCustomImagesKey());
}
}
@@ -246,7 +246,7 @@ class ShaderTransformerTest {
final EnumMap<PatchShaderType, String> result = new EnumMap<>(out);
final String fragmentBefore = result.get(PatchShaderType.FRAGMENT);
ShaderTransformer.extractRwImageStores(result, Patch.COMPOSITE, artifacts);
ShaderTransformer.extractRwImageStores(result, Patch.COMPOSITE, artifacts, image -> true);
assertNotNull(result.get(PatchShaderType.COMPUTE), "fixture must actually exercise the extraction");
assertFalse(fragmentBefore.equals(result.get(PatchShaderType.FRAGMENT)), "fixture must actually rewrite the fragment stage");
@@ -263,7 +263,7 @@ class ShaderTransformerTest {
assertNotNull(out);
final EnumMap<PatchShaderType, String> result = new EnumMap<>(out);
ShaderTransformer.extractRwImageStores(result, Patch.COMPOSITE, artifacts);
ShaderTransformer.extractRwImageStores(result, Patch.COMPOSITE, artifacts, image -> true);
assertFalse(artifacts.isEmpty(), "fixture must leave at least one artifact to check");
for (Map.Entry<PatchShaderType, ShaderTransformer.StageArtifact> e : artifacts.entrySet()) {
@@ -0,0 +1,317 @@
package net.coderbot.iris.pipeline.transform;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs;
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability;
import net.coderbot.iris.pipeline.transform.RwImageStoreExtractor.VertexInput;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class VertexReplayExtractionTest {
// Shaped like Euphoria Patches' end crystal voxelization in its shadow program
private static final String CRYSTAL_VERTEX = """
#version 130
#extension GL_ARB_shader_image_load_store : enable
uniform int entityId;
uniform mat4 shadowModelViewInverse;
uniform mat4 shadowProjectionInverse;
attribute vec4 mc_Entity;
layout(r32i) uniform iimage2D endcrystal_img;
varying vec2 texCoord;
void UpdateEndCrystalMap(vec3 pos) {
int packedPos = int(pos.x + 512.5);
for (int k = 0; k < 20; k++) {
int registeredPos = imageAtomicCompSwap(endcrystal_img, ivec2(k, 0), 0, packedPos);
if (registeredPos == 0 || registeredPos == packedPos) {
imageAtomicAdd(endcrystal_img, ivec2(k, 1), int(10.0 * pos.x));
break;
}
}
}
void main() {
texCoord = gl_MultiTexCoord0.xy;
vec3 normal = normalize(gl_NormalMatrix * gl_Normal);
vec4 position = shadowModelViewInverse * shadowProjectionInverse * ftransform();
if (entityId == 50000 && abs(normal.y) > 0.5 && mc_Entity.x < 0.0) UpdateEndCrystalMap(position.xyz);
gl_Position = ftransform();
}
""";
private static final String FRAGMENT = """
#version 130
uniform sampler2D tex;
varying vec2 texCoord;
void main() { gl_FragData[0] = texture2D(tex, texCoord); }
""";
private static final InputAvailability TEX_LM = InputAvailability.of(true, true);
@BeforeAll
static void initShaderTransformer() throws Exception {
Reflect.setStatic(RenderSystem.class, "maxGlslVersion", 460);
ShaderTransformer.init();
}
@BeforeEach
void clearCaches() {
TransformPatcher.clearCache();
ShaderTransformer.clearCache();
}
private static RwImageStoreExtractor.Result replayOf(Map<PatchShaderType, String> transformed) {
final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay(transformed.get(PatchShaderType.VERTEX));
assertNotNull(result);
assertNotNull(result.computeSource(), "every input of an Iris gbuffers vertex shader is replayable");
return result;
}
private static VertexInput input(List<VertexInput> inputs, String name) {
return inputs.stream().filter(i -> i.name().equals(name)).findFirst()
.orElseThrow(() -> new AssertionError(name + " missing from " + inputs));
}
@Test
void replayKeepsWritesAtomicAndRasterDropsThem() {
final RwImageStoreExtractor.Result result = replayOf(TransformPatcher.patchAttributes(CRYSTAL_VERTEX, null, FRAGMENT, TEX_LM));
assertEquals(RwImageStoreExtractor.RwExtractMode.VERTEX_REPLAY, RwImageStoreExtractor.parseSentinel(result.computeSource()));
assertFalse(result.strippedSource().contains("imageAtomic"), result.strippedSource());
assertFalse(result.strippedSource().contains("endcrystal_img"), result.strippedSource());
// Many vertices of one crystal add into the same texels at once
assertTrue(result.computeSource().contains("imageAtomicAdd"), result.computeSource());
assertFalse(result.computeSource().contains("_vg_prev"), result.computeSource());
assertTrue(result.computeSource().contains("layout(binding = 0, r32i) uniform iimage2D endcrystal_img;"), result.computeSource());
assertEquals(List.of("endcrystal_img"), RwImageStoreExtractor.parseVertexReplayImages(result.computeSource()));
}
@Test
void replayFetchesEveryDeclaredInput() {
final RwImageStoreExtractor.Result result = replayOf(TransformPatcher.patchAttributes(CRYSTAL_VERTEX, null, FRAGMENT, TEX_LM));
final List<VertexInput> inputs = RwImageStoreExtractor.parseVertexReplayInputs(result.computeSource());
assertEquals("vec4", input(inputs, "mc_Entity").glslType());
for (int k = 0; k < inputs.size(); k++) {
final VertexInput in = inputs.get(k);
assertTrue(result.computeSource().contains(in.name() + " = " + in.glslType() + "("), "no fetch for " + in.name());
assertTrue(result.computeSource().contains("(" + k + "u, _vg_vertex, _vg_i)"), "input " + in.name() + " is not fetched from slot " + k);
}
}
@Test
void instancedVariantReadsEntityIdFromItsInstanceAttribute() {
final RwImageStoreExtractor.Result result = replayOf(TransformPatcher.patchAttributesInstanced(CRYSTAL_VERTEX, null, null, null, FRAGMENT, TEX_LM, false, Instancing.TEMPLATE));
final List<VertexInput> inputs = RwImageStoreExtractor.parseVertexReplayInputs(result.computeSource());
final VertexInput entity = input(inputs, "iris_Entity");
assertEquals(InstancedAttribs.LOC_ENTITY, entity.location());
assertEquals("uvec4", entity.glslType());
assertTrue(result.computeSource().contains("iris_Entity = uvec4(_vg_uint("), result.computeSource());
assertFalse(result.computeSource().contains("location"), "a location on a former interface variable does not compile in compute");
}
@Test
void everyDeclaratorOfAnInputListIsFetched() {
final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay("""
#version 460 core
in vec3 position, normal;
layout(r32ui) uniform uimage2D marks;
void main() {
imageStore(marks, ivec2(normal.xy), uvec4(1u));
gl_Position = vec4(position, 1.0);
}
""");
assertNotNull(result.computeSource());
final List<VertexInput> inputs = RwImageStoreExtractor.parseVertexReplayInputs(result.computeSource());
assertEquals(List.of("position", "normal"), inputs.stream().map(VertexInput::name).toList());
assertFalse(result.computeSource().contains("in vec3"), "a stage input does not compile in compute\n" + result.computeSource());
assertTrue(result.computeSource().contains("normal = vec3(_vg_float(1u, _vg_vertex, _vg_i));"), result.computeSource());
}
@Test
void samplerOnlyTheWriteReadsIsListedForThePipeline() {
final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay("""
#version 460 core
in vec4 iris_Vertex;
uniform sampler2D noisetex;
layout(rgba8) uniform image2D marks;
void main() {
imageStore(marks, ivec2(0), textureLod(noisetex, iris_Vertex.xy, 0.0));
gl_Position = iris_Vertex;
}
""");
assertFalse(result.strippedSource().contains("textureLod"), result.strippedSource());
assertEquals(List.of("noisetex"), RwImageStoreExtractor.parseVertexReplaySamplers(result.computeSource()));
}
@Test
void readOnlyImagesBindAfterTheWrittenOnes() {
final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay("""
#version 460 core
in vec4 iris_Vertex;
layout(rgba8) uniform image2D source;
layout(rgba8) uniform image2D target;
void main() {
imageStore(target, ivec2(0), imageLoad(source, ivec2(iris_Vertex.xy)));
gl_Position = iris_Vertex;
}
""");
final String compute = result.computeSource();
assertEquals(List.of("target"), RwImageStoreExtractor.parseVertexReplayImages(compute));
assertEquals(List.of("source"), RwImageStoreExtractor.parseVertexReplayReadImages(compute));
assertTrue(compute.contains("layout(binding = 0, rgba8) writeonly uniform image2D target;"), compute);
assertTrue(compute.contains("layout(binding = 1, rgba8) readonly uniform image2D source;"), compute);
assertEquals(1, compute.split("image2D source", -1).length - 1, compute);
}
@Test
void outParametersOfHelpersStayOutParameters() {
final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay("""
#version 460 core
in vec4 iris_Vertex;
out vec2 texCoord;
layout(r32f) uniform image2D marks;
void fill(out float value) { value = iris_Vertex.x; }
void main() {
float v = 0.0;
fill(v);
imageStore(marks, ivec2(0), vec4(v));
texCoord = iris_Vertex.xy;
gl_Position = iris_Vertex;
}
""");
final String compute = result.computeSource().replaceAll("\\s+", " ");
assertTrue(compute.contains("void fill ( out float value )"), compute);
assertFalse(compute.contains("out vec2 texCoord"), compute);
}
@Test
void outputBlocksBecomeGlobalsAndLayoutDefaultsStay() {
final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay("""
#version 460 core
layout(std430) buffer;
in vec4 iris_Vertex;
out gl_PerVertex { vec4 gl_Position; };
out Outputs { flat int id; vec2 uv; } v;
out Extra { float fade; };
buffer Marks { uint counts[]; };
layout(r32ui) uniform uimage2D marks;
void main() {
v.uv = iris_Vertex.xy;
fade = iris_Vertex.z;
imageStore(marks, ivec2(v.uv), uvec4(counts[0] + uint(fade)));
gl_Position = iris_Vertex;
}
""");
assertNotNull(result);
final String compute = result.computeSource().replaceAll("\\s+", " ");
assertTrue(compute.contains("layout ( std430 ) buffer ;"), compute);
assertFalse(compute.contains("gl_PerVertex"), compute);
assertTrue(compute.contains("struct Outputs { int id ; vec2 uv ; }; Outputs v ;"), compute);
assertTrue(compute.contains("float fade ;"), compute);
assertFalse(compute.contains("Extra"), compute);
}
@Test
void primitiveRestartMarkersAreNotReplayedAsVertices() {
final String compute = replayOf(TransformPatcher.patchAttributes(CRYSTAL_VERTEX, null, FRAGMENT, TEX_LM)).computeSource();
final int skip = compute.indexOf("if (_vg_restart.x != 0u && _vg_index_value == _vg_restart.y) return;");
assertTrue(skip >= 0, compute);
assertTrue(skip < compute.indexOf("_vg_vertex = _vg_index_value + uint(_vg_draw.w);"), compute);
}
@Test
void atomicReplayIsDroppedWithoutBackendSupport() {
final EnumMap<PatchShaderType, String> atomic = new EnumMap<>(PatchShaderType.class);
atomic.put(PatchShaderType.VERTEX, TransformPatcher.patchAttributes(CRYSTAL_VERTEX, null, FRAGMENT, TEX_LM).get(PatchShaderType.VERTEX));
ShaderTransformer.extractRwImageStores(atomic, Patch.ATTRIBUTES, null, image -> false);
assertFalse(atomic.containsKey(PatchShaderType.COMPUTE));
assertFalse(atomic.get(PatchShaderType.VERTEX).contains("imageAtomic"), atomic.get(PatchShaderType.VERTEX));
final EnumMap<PatchShaderType, String> plain = new EnumMap<>(PatchShaderType.class);
plain.put(PatchShaderType.VERTEX, """
#version 460 core
in vec4 iris_Vertex;
layout(r32ui) uniform uimage2D marks;
void main() {
imageStore(marks, ivec2(0), uvec4(1u));
gl_Position = iris_Vertex;
}
""");
ShaderTransformer.extractRwImageStores(plain, Patch.ATTRIBUTES, null, image -> false);
assertTrue(plain.containsKey(PatchShaderType.COMPUTE), "plain stores need no atomics, so the replay stays");
}
@Test
void atomicReplayFollowsTheImageItWrites() {
final String vertex = TransformPatcher.patchAttributes(CRYSTAL_VERTEX, null, FRAGMENT, TEX_LM).get(PatchShaderType.VERTEX);
final EnumMap<PatchShaderType, String> supported = new EnumMap<>(PatchShaderType.class);
supported.put(PatchShaderType.VERTEX, vertex);
ShaderTransformer.extractRwImageStores(supported, Patch.ATTRIBUTES, null, "endcrystal_img"::equals);
assertTrue(supported.containsKey(PatchShaderType.COMPUTE));
final EnumMap<PatchShaderType, String> unsupported = new EnumMap<>(PatchShaderType.class);
unsupported.put(PatchShaderType.VERTEX, vertex);
ShaderTransformer.extractRwImageStores(unsupported, Patch.ATTRIBUTES, null, "voxel_img"::equals);
assertFalse(unsupported.containsKey(PatchShaderType.COMPUTE));
}
@Test
void matrixInputIsNotReplayed() {
final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay("""
#version 460 core
in mat4 instanceMatrix;
layout(r32ui) uniform uimage2D marks;
void main() {
imageStore(marks, ivec2(0), uvec4(1u));
gl_Position = instanceMatrix[0];
}
""");
assertNotNull(result);
assertNull(result.computeSource());
assertFalse(result.strippedSource().contains("imageStore"), result.strippedSource());
}
@Test
void attributesProgramKeepsVertexReplayAndDropsFragmentWrites() {
final EnumMap<PatchShaderType, String> result = new EnumMap<>(PatchShaderType.class);
result.put(PatchShaderType.VERTEX, """
#version 460 core
in vec4 iris_Vertex;
layout(r32ui) uniform uimage2D marks;
void main() {
imageStore(marks, ivec2(0), uvec4(1u));
gl_Position = iris_Vertex;
}
""");
result.put(PatchShaderType.FRAGMENT, """
#version 460 core
layout(r32ui) uniform uimage2D marks;
out vec4 color;
void main() {
imageStore(marks, ivec2(1), uvec4(2u));
color = vec4(1.0);
}
""");
ShaderTransformer.extractRwImageStores(result, Patch.ATTRIBUTES, null, image -> true);
assertNotNull(result.get(PatchShaderType.COMPUTE));
assertFalse(result.get(PatchShaderType.VERTEX).contains("imageStore"));
assertFalse(result.get(PatchShaderType.FRAGMENT).contains("imageStore"));
}
}