Compare commits

...
8 Commits
91 changed files with 3139 additions and 616 deletions

No files matched your search

@@ -34,6 +34,7 @@ import com.gtnewhorizons.angelica.glsm.states.PixelStoreState;
import com.gtnewhorizons.angelica.glsm.states.ImageUnitArray;
import com.gtnewhorizons.angelica.glsm.states.SamplerUnitArray;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs;
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
@@ -104,6 +105,8 @@ public class GLContextState {
public boolean dirtyNormalAttrib;
public boolean dirtyTexCoordAttrib;
public boolean dirtyLightmapAttrib = true;
public long entityIds = InstancedAttribs.DEFAULT_ENTITY_IDS;
public boolean dirtyEntityIdsAttrib = true;
public boolean unit23TexCoordSetDuringDraw = false;
public int maxBoundTextureUnit = 0;
public int maxBoundImageUnit = 0;
@@ -236,6 +239,8 @@ public class GLContextState {
public int boundCopyReadBuffer;
public int boundCopyWriteBuffer;
public final Int2IntMap boundOtherBuffers = new Int2IntOpenHashMap();
public final int[] uniformBufferBindings = new int[GLStateManager.INDEXED_BUFFER_SLOTS];
public final int[] storageBufferBindings = new int[GLStateManager.INDEXED_BUFFER_SLOTS];
public PixelStoreState pixelUnpackState = PixelStoreState.DEFAULT;
public PixelStoreState pixelPackState = PixelStoreState.DEFAULT;
public final int[] clientAttribSavedMask = new int[GLStateManager.CLIENT_ATTRIB_STACK_DEPTH];
@@ -245,7 +250,6 @@ public class GLContextState {
public final int[] clientAttribSavedPackBuffer = new int[GLStateManager.CLIENT_ATTRIB_STACK_DEPTH];
public final VAOManager.ClientArrays[] clientAttribSavedArrays = new VAOManager.ClientArrays[GLStateManager.CLIENT_ATTRIB_STACK_DEPTH];
public int clientAttribStackPointer = 0;
public float maxPointSize = Float.NaN;
public int restoreChangedMask;
public long restoreUnitChangedMask;
public final BlendState vanillaBlendBefore = new BlendState();
@@ -12,6 +12,7 @@ import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.GLDebugMessageListener;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.ffp.FfpExtendedAttribs;
import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs;
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
@@ -25,6 +26,7 @@ import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.BatchStateGuard;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import com.gtnewhorizons.angelica.glsm.hooks.GlintColorHandler;
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
import com.gtnewhorizons.angelica.glsm.recording.CommandRecorder;
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
import com.gtnewhorizons.angelica.glsm.recording.ImmediateModeRecorder;
@@ -68,6 +70,7 @@ import com.gtnewhorizons.angelica.glsm.states.ImageUnitBinding;
import com.gtnewhorizons.angelica.glsm.states.TextureBinding;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import com.gtnewhorizons.angelica.glsm.streaming.LineQuads;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorEntityIds;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
@@ -93,7 +96,6 @@ import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.Drawable;
import org.lwjgl.opengl.EXTTextureFilterAnisotropic;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL42;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL14;
@@ -104,6 +106,8 @@ import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL33;
import org.lwjgl.opengl.GL42;
import org.lwjgl.opengl.GL43;
import org.lwjgl.opengl.GLContext;
import org.lwjgl.opengl.KHRDebug;
import org.lwjgl.opengl.KHRDebugCallback;
@@ -328,6 +332,7 @@ public class GLStateManager {
public static final int MAX_PROJECTION_STACK_DEPTH = 4;
public static final int MAX_TEXTURE_STACK_DEPTH = 4;
public static final int MAX_CLIP_PLANES = 8;
public static final int INDEXED_BUFFER_SLOTS = 64;
public static final int MAX_TEXTURE_UNITS = RENDER_BACKEND.getInteger(GL20.GL_MAX_TEXTURE_IMAGE_UNITS);
public static final int MAX_LEGACY_TEXTURE_UNITS = Math.min(VertexKey.MAX_UNITS, MAX_TEXTURE_UNITS);
public static final int MAX_TEXTURE_COORDS = Math.min(VAOManager.TEX_COORD_UNITS, MAX_TEXTURE_UNITS);
@@ -394,6 +399,26 @@ public class GLStateManager {
}
}
public static void setEntityIds(int entity, int blockEntity, int item) {
if (!isMainThread()) return;
final GLContextState glCtx = ctx();
final long ids = InstancedAttribs.packEntityInfo(entity, blockEntity, item);
if (ids == glCtx.entityIds) return;
TessellatorEntityIds.onIdsChanged(glCtx.entityIds, ids);
glCtx.entityIds = ids;
glCtx.dirtyEntityIdsAttrib = true;
}
public static void flushEntityIds(GLContextState glCtx) {
if (!glCtx.dirtyEntityIdsAttrib || glCtx.activeProgram == 0) return;
final ImmediateExtendedAttribHandler handler = GLSMHooks.immediateExtendedHandler;
if (handler == null || !handler.wantsEntityIds()) return;
final long ids = glCtx.entityIds;
RENDER_BACKEND.vertexAttrib4f(ImmediateExtendedAttribHandler.LOC_ENTITY_IDS,
InstancedAttribs.normalizedId(ids, 0), InstancedAttribs.normalizedId(ids, 16), InstancedAttribs.normalizedId(ids, 32), 0.0f);
glCtx.dirtyEntityIdsAttrib = false;
}
public static int getMaxBoundTextureUnit() {
return ctx().maxBoundTextureUnit;
@@ -415,6 +440,7 @@ public class GLStateManager {
// This setting varies depending on driver, so it gets queried at runtime
public static int DEFAULT_DRAW_BUFFER = GL11.GL_BACK;
private static float maxPointSize;
private static volatile Thread CurrentThread = MainThread;
@Setter @Getter private static boolean runningSplash = true;
@@ -517,18 +543,6 @@ public class GLStateManager {
}
}
private static float maxPointSize(GLContextState glCtx) {
if (Float.isNaN(glCtx.maxPointSize)) {
final FloatBuffer range = glCtx.queryScratch;
range.clear();
RENDER_BACKEND.getFloat(GL11.GL_POINT_SIZE_RANGE, range);
final float smooth = range.get(1);
RENDER_BACKEND.getFloat(GL12.GL_ALIASED_POINT_SIZE_RANGE, range);
glCtx.maxPointSize = Math.max(smooth, range.get(1));
}
return glCtx.maxPointSize;
}
private static <T extends CowStateStack<?>> T mod(T s) {
s.beforeModify();
return s;
@@ -777,6 +791,13 @@ public class GLStateManager {
initCallback.run();
}
final FloatBuffer range = glCtx.queryScratch;
range.clear();
RENDER_BACKEND.getFloat(GL11.GL_POINT_SIZE_RANGE, range);
final float smooth = range.get(1);
RENDER_BACKEND.getFloat(GL12.GL_ALIASED_POINT_SIZE_RANGE, range);
maxPointSize = Math.max(smooth, range.get(1));
// Drain any pending GL errors from initialization. In core profile, some legacy queries may generate GL_INVALID_ENUM. The splash thread inherits the
// DrawableGL context and its error state, so stale errors here would cause SplashProgress.checkGLError() to fail.
int err;
@@ -1482,7 +1503,7 @@ public class GLStateManager {
case GL11.GL_LINE_WIDTH -> glCtx.lineState.getWidth();
case GL11.GL_POINT_SIZE -> glCtx.pointState.getSize();
case GL14.GL_POINT_SIZE_MIN -> glCtx.pointState.getMinSize();
case GL14.GL_POINT_SIZE_MAX -> Float.isNaN(glCtx.pointState.getMaxSize()) ? maxPointSize(glCtx) : glCtx.pointState.getMaxSize();
case GL14.GL_POINT_SIZE_MAX -> Float.isNaN(glCtx.pointState.getMaxSize()) ? maxPointSize : glCtx.pointState.getMaxSize();
case GL14.GL_POINT_FADE_THRESHOLD_SIZE -> glCtx.pointState.getFadeThreshold();
default -> {
final int desc = isCachingEnabled() ? LegacyQueries.desc(pname) : 0;
@@ -4825,6 +4846,7 @@ public class GLStateManager {
glCtx.dirtyNormalAttrib = true;
glCtx.dirtyTexCoordAttrib = true;
glCtx.dirtyLightmapAttrib = true;
glCtx.dirtyEntityIdsAttrib = true;
}
private static void applyPolygonOffset(GLContextState glCtx) {
@@ -7267,6 +7289,10 @@ public class GLStateManager {
e.setValue(0);
RENDER_BACKEND.bindBuffer(e.getIntKey(), 0);
}
for (int i = 0; i < INDEXED_BUFFER_SLOTS; i++) {
if (glCtx.uniformBufferBindings[i] == buffer) glCtx.uniformBufferBindings[i] = 0;
if (glCtx.storageBufferBindings[i] == buffer) glCtx.storageBufferBindings[i] = 0;
}
writeMappedBuffers.remove(buffer);
glCtx.vaos.onDeleteBuffer(buffer);
@@ -7881,7 +7907,9 @@ public class GLStateManager {
public static void glBindBufferBase(int target, int index, int buffer) {
final boolean locked = acquireDrawLock();
try {
trackBufferBinding(ctx(), target, buffer);
final GLContextState glCtx = ctx();
trackBufferBinding(glCtx, target, buffer);
trackIndexedBufferBinding(glCtx, target, index, buffer);
RENDER_BACKEND.bindBufferBase(target, index, buffer);
} finally {
if (locked) releaseDrawLock();
@@ -7891,13 +7919,35 @@ public class GLStateManager {
public static void glBindBufferRange(int target, int index, int buffer, long offset, long size) {
final boolean locked = acquireDrawLock();
try {
trackBufferBinding(ctx(), target, buffer);
final GLContextState glCtx = ctx();
trackBufferBinding(glCtx, target, buffer);
trackIndexedBufferBinding(glCtx, target, index, buffer);
RENDER_BACKEND.bindBufferRange(target, index, buffer, offset, size);
} finally {
if (locked) releaseDrawLock();
}
}
private static int[] indexedBufferBindings(GLContextState glCtx, int target, int index) {
if (index < 0 || index >= INDEXED_BUFFER_SLOTS) return null;
if (target == GL31.GL_UNIFORM_BUFFER) return glCtx.uniformBufferBindings;
if (target == GL43.GL_SHADER_STORAGE_BUFFER) return glCtx.storageBufferBindings;
return null;
}
private static void trackIndexedBufferBinding(GLContextState glCtx, int target, int index, int buffer) {
final int[] bindings = indexedBufferBindings(glCtx, target, index);
if (bindings != null) bindings[index] = buffer;
}
public static int getIndexedBufferBinding(int target, int index) {
if (isCachingEnabled()) {
final int[] bindings = indexedBufferBindings(ctx(), target, index);
if (bindings != null) return bindings[index];
}
return RENDER_BACKEND.getIndexedBufferBinding(target, index);
}
public static int glGetUniformBlockIndex(int program, CharSequence name) { return RENDER_BACKEND.getUniformBlockIndex(program, name); }
public static void glUniformBlockBinding(int program, int blockIndex, int binding) { RENDER_BACKEND.uniformBlockBinding(program, blockIndex, binding); }
public static void glBindFragDataLocation(int program, int colorNumber, CharSequence name) { RENDER_BACKEND.bindFragDataLocation(program, colorNumber, name); }
@@ -8656,7 +8706,9 @@ public class GLStateManager {
return ctx().imageUnits.get(unit);
}
private static int imageBindingOrBackend(int pname, int index) {
private static int indexedIntegerOrBackend(int pname, int index) {
if (pname == GL31.GL_UNIFORM_BUFFER_BINDING) return getIndexedBufferBinding(GL31.GL_UNIFORM_BUFFER, index);
if (pname == GL43.GL_SHADER_STORAGE_BUFFER_BINDING) return getIndexedBufferBinding(GL43.GL_SHADER_STORAGE_BUFFER, index);
final ImageUnitBinding binding = !isCachingEnabled() ? null : ctx().imageUnits.get(index);
if (binding == null) return RENDER_BACKEND.getIntegerIndexed(pname, index);
return switch (pname) {
@@ -8671,11 +8723,11 @@ public class GLStateManager {
}
public static int glGetInteger(int pname, int index) {
return imageBindingOrBackend(pname, index);
return indexedIntegerOrBackend(pname, index);
}
public static int glGetIntegeri(int pname, int index) {
return imageBindingOrBackend(pname, index);
return indexedIntegerOrBackend(pname, index);
}
public static void glGetIntegeri(int pname, int index, IntBuffer params) {
@@ -8683,11 +8735,11 @@ public class GLStateManager {
}
public static void glGetIntegeri_v(int pname, int index, IntBuffer params) {
params.put(params.position(), imageBindingOrBackend(pname, index));
params.put(params.position(), indexedIntegerOrBackend(pname, index));
}
public static void glGetBooleani_v(int pname, int index, ByteBuffer params) {
params.put(params.position(), (byte) (imageBindingOrBackend(pname, index) != 0 ? GL11.GL_TRUE : GL11.GL_FALSE));
params.put(params.position(), (byte) (indexedIntegerOrBackend(pname, index) != 0 ? GL11.GL_TRUE : GL11.GL_FALSE));
}
public static void glGetBooleani(int pname, int index, ByteBuffer params) {
@@ -556,7 +556,7 @@ public class RenderSystem {
}
public static int getIndexedBufferBinding(int target, int index) {
return RENDER_BACKEND.getIndexedBufferBinding(target, index);
return GLStateManager.getIndexedBufferBinding(target, index);
}
public static void bindBufferBase(int target, int index, int buffer) {
@@ -21,7 +21,11 @@ public final class BackendOptions {
}
public static String sdlGpuDriver() {
return SystemProperties.SDL_GPU_DRIVER_OVERRIDE != null ? SystemProperties.SDL_GPU_DRIVER_OVERRIDE : configDriver;
return sdlGpuDriver(configDriver);
}
public static String sdlGpuDriver(String driver) {
return SystemProperties.SDL_GPU_DRIVER_OVERRIDE != null ? SystemProperties.SDL_GPU_DRIVER_OVERRIDE : driver;
}
public static boolean configSdlGpu() {
@@ -1,55 +1,96 @@
package com.gtnewhorizons.angelica.glsm.backend;
import com.google.common.primitives.Ints;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
public final class BackendStartGuard {
private static final Logger LOGGER = LogManager.getLogger("Angelica/BackendStartGuard");
private static File pending;
static final int STRIKE_LIMIT = 3;
private static File marker;
private static File verified;
private static String key;
private static int strikes;
private static boolean pending;
private static boolean tripped;
private BackendStartGuard() {}
public static boolean begin(File marker, boolean sdlGpuConfigured, boolean flagForced) {
pending = null;
tripped = false;
if (!sdlGpuConfigured || flagForced) {
public static boolean begin(File dir, boolean sdlGpuConfigured, boolean flagForced, int requirementsRevision, String requestedDriver) {
marker = new File(dir, "sdlgpu-starting");
verified = new File(dir, "sdlgpu-verified");
key = requirementsRevision + "/" + requestedDriver;
pending = tripped = false;
if (flagForced) return false;
if (!sdlGpuConfigured) {
delete(marker);
return false;
}
if (marker.isFile()) {
if (key.equals(read(verified))) return false;
final String count = read(marker);
final Integer parsed = count == null ? null : Ints.tryParse(count.trim());
strikes = parsed == null ? 0 : parsed;
if (strikes >= STRIKE_LIMIT) {
LOGGER.warn("SDL GPU failed to bring up the device {} times in a row; resetting the render backend to OpenGL", strikes);
delete(marker);
tripped = true;
return true;
}
try {
final File dir = marker.getParentFile();
if (dir != null && !dir.isDirectory() && !dir.mkdirs()) throw new IOException("cannot create " + dir);
if (marker.createNewFile()) pending = marker;
} catch (IOException e) {
LOGGER.warn("Could not write {}: {}", marker, e.toString());
if (strikes == STRIKE_LIMIT - 1) {
LOGGER.warn("SDL GPU failed to bring up the device {} times in a row; one more and the render backend switches to OpenGL", strikes);
} else if (strikes > 0) {
LOGGER.info("The last start ended while bringing up the SDL GPU device");
}
pending = true;
return false;
}
public static void finish() {
if (pending == null) return;
delete(pending);
pending = null;
public static void arm() {
if (pending) write(marker, Integer.toString(strikes + 1));
}
public static void disarm() {
if (pending) delete(marker);
}
public static void latch() {
if (!pending) return;
pending = false;
write(verified, key);
LOGGER.info("SDL GPU booted; the startup guard stays off until the requirements revision or driver changes");
}
public static boolean tripped() {
return tripped;
}
private static void delete(File marker) {
if (marker.isFile() && !marker.delete()) {
LOGGER.warn("Could not delete {}", marker);
private static String read(File file) {
try {
return file.isFile() ? new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8) : null;
} catch (IOException e) {
LOGGER.warn("Could not read {}: {}", file, e.toString());
return null;
}
}
private static void write(File file, String content) {
try {
Files.createDirectories(file.toPath().getParent());
Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
LOGGER.warn("Could not write {}: {}", file, e.toString());
}
}
private static void delete(File file) {
if (file.isFile() && !file.delete()) LOGGER.warn("Could not delete {}", file);
}
}
@@ -25,6 +25,8 @@ public final class InstancedAttribs {
public static final int OFFSET_LIGHTMAP = HEAD_SIZE;
public static final int STRIDE = 72;
public static final long DEFAULT_ENTITY_IDS = packEntityInfo(-1, 0, -1);
public static void writeHead(long ptr, float[] colMajor, int off, int colorABGR, int overlayABGR, long entityInfo) {
writeRows(ptr, colMajor, off);
writeTail(ptr, colorABGR, overlayABGR, entityInfo);
@@ -60,13 +62,21 @@ public final class InstancedAttribs {
return id == 0xFFFF ? -1 : id;
}
public static float normalizedId(long info, int shift) {
return ((info >>> shift) & 0xFFFF) / 65535.0f;
}
public static void pointHead(long base, int stride) {
for (int r = 0; r < 3; r++) {
GLStateManager.glVertexAttribPointer(LOC_ROW0 + r, 4, GL11.GL_FLOAT, false, stride, base + OFFSET_ROWS + r * 16L);
}
GLStateManager.glVertexAttribPointer(LOC_COLOR, 4, GL11.GL_UNSIGNED_BYTE, true, stride, base + OFFSET_COLOR);
GLStateManager.glVertexAttribPointer(LOC_OVERLAY, 4, GL11.GL_UNSIGNED_BYTE, true, stride, base + OFFSET_OVERLAY);
GLStateManager.glVertexAttribIPointer(LOC_ENTITY, 4, GL11.GL_UNSIGNED_SHORT, stride, base + OFFSET_ENTITY);
pointEntityIds(LOC_ENTITY, stride, base + OFFSET_ENTITY);
}
public static void pointEntityIds(int location, int stride, long offset) {
GLStateManager.glVertexAttribPointer(location, 4, GL11.GL_UNSIGNED_SHORT, true, stride, offset);
}
public static void pointTemplate(long base) {
@@ -139,6 +139,7 @@ public final class ShaderManager {
GLSMHooks.resolvePendingProgram();
final DeferredBlendHandler bh = GLSMHooks.blendHandler;
if (bh != null) bh.flushDeferredBlend();
GLStateManager.flushEntityIds(glCtx);
// Handle FFP & Iris uniforms
final int currentProgramId = glCtx.activeProgram;
@@ -3,15 +3,18 @@ package com.gtnewhorizons.angelica.glsm.hooks;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs;
import org.lwjgl.opengl.GL11;
public interface ImmediateExtendedAttribHandler {
int EXT_STRIDE = 12;
int ENTITY_IDS_STRIDE = 8;
int LOC_MC_ENTITY = 11;
int LOC_MID_TEX = 12;
int LOC_TANGENT = 13;
int LOC_MIDBLOCK = 14;
int LOC_ENTITY_IDS = 15;
int RAW_NORMAL_INDEX = 6;
@@ -19,6 +22,8 @@ public interface ImmediateExtendedAttribHandler {
boolean wantsTangent();
boolean wantsEntityIds();
boolean wantsExtendedCapture();
boolean wantsFaceNormals();
@@ -65,4 +70,9 @@ public interface ImmediateExtendedAttribHandler {
GLStateManager.glEnableVertexAttribArray(LOC_TANGENT);
GLStateManager.glVertexAttribPointer(LOC_TANGENT, 4, GL11.GL_BYTE, true, stride, baseOffset + 8);
}
static void setupEntityIdsPointer(long offset, int stride) {
GLStateManager.glEnableVertexAttribArray(LOC_ENTITY_IDS);
InstancedAttribs.pointEntityIds(LOC_ENTITY_IDS, stride, offset);
}
}
@@ -34,6 +34,17 @@ public final class GlslVulkanPreprocess {
"const_cast", "dynamic_cast", "reinterpret_cast", "static_cast", "export",
"and", "or", "xor", "bitand", "bitor", "compl", "and_eq", "or_eq", "xor_eq", "not_eq");
private static final Object2ObjectLinkedOpenHashMap<String, String> IDENTIFIER_RENAMES = identifierRenames();
private static Object2ObjectLinkedOpenHashMap<String, String> identifierRenames() {
final Object2ObjectLinkedOpenHashMap<String, String> renames = new Object2ObjectLinkedOpenHashMap<>();
renames.put("sampler", SAMPLER_RENAMED);
renames.put("gl_VertexID", "gl_VertexIndex");
renames.put("gl_InstanceID", "gl_InstanceIndex");
for (String reserved : TARGET_RESERVED) renames.put(reserved, RESERVED_RENAMED_PREFIX + reserved);
return renames;
}
public static final String SAMPLERLESS_EXTENSION = "#extension GL_EXT_samplerless_texture_functions : require";
private static final int MAX_VS_INPUT_LOCATIONS = 16;
@@ -191,14 +202,13 @@ public final class GlslVulkanPreprocess {
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));
addSamplerlessExtension(index, edits);
}
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);
for (var it = IDENTIFIER_RENAMES.object2ObjectEntrySet().fastIterator(); it.hasNext(); ) {
final var rename = it.next();
rename(index, rename.getKey(), rename.getValue(), edits);
}
if (maxExplicitLoc >= 0 && !unlocatedVsInputStarts.isEmpty()) {
int next = maxExplicitLoc + 1;
@@ -216,6 +226,15 @@ public final class GlslVulkanPreprocess {
return new Metadata(bools, explicitInputs, needsSamplerless);
}
public static boolean renamesIdentifier(String name) {
return IDENTIFIER_RENAMES.containsKey(name);
}
public static void addSamplerlessExtension(ShaderIndex index, List<Edit> edits) {
final int versionEnd = index.directives().versionEnd();
if (versionEnd >= 0) edits.add(new Edit(versionEnd, versionEnd - 1, "\n" + SAMPLERLESS_EXTENSION));
}
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));
}
@@ -21,8 +21,8 @@ import java.util.Set;
*/
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);
public record Directives(Set<String> identifiers, Set<String> definedMacros, boolean hasConditionalOrMacro, int versionEnd) {
private static final Directives NONE = new Directives(Set.of(), Set.of(), false, -1);
}
private final GLSLParser.Translation_unitContext root;
@@ -83,6 +83,7 @@ public final class ShaderIndex {
if (source.indexOf('#') < 0) return Directives.NONE;
final GLSLLexer lexer = GlslTransformUtils.quietLexer(source);
final Set<String> names = new HashSet<>();
final Set<String> macros = new HashSet<>();
boolean conditionalOrMacro = false;
int versionEnd = -1;
boolean inVersion = false;
@@ -100,11 +101,17 @@ public final class ShaderIndex {
inVersion = false;
switch (type) {
case GLSLLexer.PROGRAM_TEXT, GLSLLexer.MACRO_TEXT, GLSLLexer.CONSTANT_EXPRESSION -> collectIdentifiers(token.getText(), names);
case GLSLLexer.MACRO_NAME -> macros.add(leadingIdentifier(token.getText()));
case GLSLLexer.IF_DIRECTIVE, GLSLLexer.IFDEF_DIRECTIVE, GLSLLexer.IFNDEF_DIRECTIVE, GLSLLexer.DEFINE_DIRECTIVE -> conditionalOrMacro = true;
default -> { }
}
}
return new Directives(names, conditionalOrMacro, versionEnd);
return new Directives(names, macros, conditionalOrMacro, versionEnd);
}
private static String leadingIdentifier(String text) {
final int args = text.indexOf('(');
return args < 0 ? text : text.substring(0, args);
}
private static void collectIdentifiers(String text, Set<String> out) {
@@ -5,12 +5,25 @@ import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
public final class StreamingVaos {
private static final int FORMAT_COUNT = VertexFlags.BITSET_SIZE;
static final int MID_TEX_AND_TANGENT = 1;
static final int ENTITY_IDS = 2;
private static final int EXTENDED_LAYOUTS = MID_TEX_AND_TANGENT | ENTITY_IDS;
final int[] persistentVAOs = new int[FORMAT_COUNT];
final int[] orphanVAOs = new int[FORMAT_COUNT];
final int[] extendedPersistentVAOs = new int[FORMAT_COUNT];
final int[] extendedOrphanVAOs = new int[FORMAT_COUNT];
// An extended draw always carries at least one layout, so row 0 is layout 1
private final int[][] extendedPersistentVAOs = new int[EXTENDED_LAYOUTS][FORMAT_COUNT];
private final int[][] extendedOrphanVAOs = new int[EXTENDED_LAYOUTS][FORMAT_COUNT];
final int[] lineQuadPersistentVAOs = new int[FORMAT_COUNT];
final int[] lineQuadOrphanVAOs = new int[FORMAT_COUNT];
int[] extendedPersistent(int layout) {
return extendedPersistentVAOs[layout - 1];
}
int[] extendedOrphan(int layout) {
return extendedOrphanVAOs[layout - 1];
}
}
@@ -0,0 +1,78 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import net.minecraft.client.renderer.Tessellator;
import java.util.Arrays;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutLong;
/**
* Entity, block entity, and item ids of the vertices in the main Tessellator batch.
*/
public final class TessellatorEntityIds {
private static Tessellator owner;
private static int runCount;
private static int[] runStarts = new int[8];
private static long[] runIds = new long[8];
private TessellatorEntityIds() {}
public static void onIdsChanged(long previous, long next) {
final Tessellator tess = Tessellator.instance;
if (tess == null || !tess.isDrawing || tess.vertexCount == 0) return;
final int start = tess.vertexCount;
if (owner != tess) {
owner = tess;
runCount = 0;
}
if (runCount == 0) addRun(0, previous);
if (runStarts[runCount - 1] == start) {
runIds[runCount - 1] = next;
if (runCount > 1 && runIds[runCount - 2] == next) runCount--;
} else {
addRun(start, next);
}
}
public static void onReset(Tessellator tess) {
if (tess != owner) return;
owner = null;
runCount = 0;
}
public static boolean hasIdChanges(Tessellator tess) {
return tess == owner && runCount > 1;
}
static int runsFor(Tessellator tess, int vertexCount) {
if (tess != owner) return 0;
int runs = runCount;
while (runs > 0 && runStarts[runs - 1] >= vertexCount) runs--;
return runs;
}
static long firstRunIds() {
return runIds[0];
}
static void write(long dst, int stride, int vertexCount, int runs) {
for (int run = 0; run < runs; run++) {
final int end = run + 1 < runs ? runStarts[run + 1] : vertexCount;
final long ids = runIds[run];
for (int vertex = runStarts[run]; vertex < end; vertex++) {
memPutLong(dst + (long) vertex * stride, ids);
}
}
}
private static void addRun(int start, long ids) {
if (runCount == runStarts.length) {
runStarts = Arrays.copyOf(runStarts, runCount * 2);
runIds = Arrays.copyOf(runIds, runCount * 2);
}
runStarts[runCount] = start;
runIds[runCount] = ids;
runCount++;
}
}
@@ -109,53 +109,7 @@ public class TessellatorStreamingDrawer {
if (GLSMConfig.expandVertexFormats) {
effectiveFlags = VertexFlags.COLOR_BIT | VertexFlags.TEXTURE_BIT | VertexFlags.NORMAL_BIT | VertexFlags.BRIGHTNESS_BIT;
if (effectiveFlags != flags) {
final int[] rawBuffer = tess.rawBuffer;
final int defaultColor;
if (!tess.hasColor) {
final var c = GLStateManager.getColor();
defaultColor = ((int)(clamp(0f, 1f, c.getAlpha()) * 255) << 24) | ((int)(clamp(0f, 1f, c.getBlue()) * 255) << 16)
| ((int)(clamp(0f, 1f, c.getGreen()) * 255) << 8) | (int)(clamp(0f, 1f, c.getRed()) * 255);
} else {
defaultColor = 0;
}
final int defaultBrightness;
if (!tess.hasBrightness) {
defaultBrightness = GLSMConfig.packedLastBrightness();
} else {
defaultBrightness = 0;
}
final ImmediateExtendedAttribHandler normalHandler = GLSMHooks.immediateExtendedHandler;
final boolean faceNormals = !tess.hasNormals && normalHandler != null
&& ImmediateExtendedAttribHandler.extPrimVerts(tess.drawMode, vertexCount) == 4 && normalHandler.wantsFaceNormals();
final boolean fillNormal = !tess.hasNormals && !faceNormals;
final int defaultNormal;
if (fillNormal) {
final var n = ShaderManager.getCurrentNormal();
defaultNormal = ((int)(clamp(-1f, 1f, n.z) * 127) << 16) | (((int)(clamp(-1f, 1f, n.y) * 127) & 0xFF) << 8) | ((int)(clamp(-1f, 1f, n.x) * 127) & 0xFF);
} else {
defaultNormal = 0;
}
final int defaultTexU, defaultTexV;
if (!tess.hasTexture) {
final var tc = ShaderManager.getCurrentTexCoord();
defaultTexU = Float.floatToRawIntBits(tc.x);
defaultTexV = Float.floatToRawIntBits(tc.y);
} else {
defaultTexU = 0; defaultTexV = 0;
}
for (int i = 0; i < vertexCount; i++) {
final int base = i * 8;
if (!tess.hasTexture) { rawBuffer[base + 3] = defaultTexU; rawBuffer[base + 4] = defaultTexV; }
if (!tess.hasColor) { rawBuffer[base + 5] = defaultColor; }
if (fillNormal) { rawBuffer[base + 6] = defaultNormal; }
if (!tess.hasBrightness) { rawBuffer[base + 7] = defaultBrightness; }
}
if (faceNormals) normalHandler.writeFaceNormals(rawBuffer, vertexCount);
fillMissingAttributes(tess);
}
} else {
effectiveFlags = flags;
@@ -176,20 +130,34 @@ public class TessellatorStreamingDrawer {
final int extPrim = extHandler == null ? 0 : ImmediateExtendedAttribHandler.extPrimVerts(tess.drawMode, vertexCount);
final boolean wantsExt = extHandler != null && extHandler.wantsExtended();
final boolean doExt = extPrim != 0 && tess.hasTexture && wantsExt;
if (doExt) {
final int combinedStride = vertexSize + ImmediateExtendedAttribHandler.EXT_STRIDE;
final int combinedBytes = vertexCount * combinedStride;
ensureExtScratch(combinedBytes);
interleaveBase(repackAddress, extScratchAddress, vertexCount, vertexSize, combinedStride);
// format expansion backfills a normal into rawBuffer, so trust effectiveFlags rather than tess.hasNormals
final int normalIndex = (effectiveFlags & VertexFlags.NORMAL_BIT) != 0 ? ImmediateExtendedAttribHandler.RAW_NORMAL_INDEX : -1;
extHandler.build(tess.rawBuffer, vertexCount, extPrim, normalIndex, extScratchAddress + vertexSize, combinedStride);
extScratch.position(0);
extScratch.limit(combinedBytes);
uploadAndDrawExtended(extScratch, effectiveFlags, format, combinedStride, tess.drawMode, vertexCount);
shrinkExtScratchIfOversized(combinedBytes);
} else {
uploadAndDraw(repackBuffer, effectiveFlags, format, vertexSize, tess.drawMode, vertexCount);
final int idRuns = TessellatorEntityIds.runsFor(tess, vertexCount);
final boolean doIds = idRuns > 1 && extHandler != null && extHandler.wantsEntityIds() && !LineQuads.isLineMode(tess.drawMode);
final GLContextState swappedCtx = idRuns >= 1 && !doIds ? GLStateManager.ctx() : null;
final long restoreIds = swappedCtx != null ? swapEntityIds(swappedCtx, TessellatorEntityIds.firstRunIds()) : 0L;
try {
if (doExt || doIds) {
final int layout = (doExt ? StreamingVaos.MID_TEX_AND_TANGENT : 0) | (doIds ? StreamingVaos.ENTITY_IDS : 0);
final int combinedStride = combinedStride(format, layout);
final int combinedBytes = vertexCount * combinedStride;
ensureExtScratch(combinedBytes);
interleaveBase(repackAddress, extScratchAddress, vertexCount, vertexSize, combinedStride);
long extAddress = extScratchAddress + vertexSize;
if (doExt) {
// format expansion backfills a normal into rawBuffer, so trust effectiveFlags rather than tess.hasNormals
final int normalIndex = (effectiveFlags & VertexFlags.NORMAL_BIT) != 0 ? ImmediateExtendedAttribHandler.RAW_NORMAL_INDEX : -1;
extHandler.build(tess.rawBuffer, vertexCount, extPrim, normalIndex, extAddress, combinedStride);
extAddress += ImmediateExtendedAttribHandler.EXT_STRIDE;
}
if (doIds) TessellatorEntityIds.write(extAddress, combinedStride, vertexCount, idRuns);
extScratch.position(0);
extScratch.limit(combinedBytes);
uploadAndDrawExtended(extScratch, effectiveFlags, format, layout, combinedStride, tess.drawMode, vertexCount);
shrinkExtScratchIfOversized(combinedBytes);
} else {
uploadAndDraw(repackBuffer, effectiveFlags, format, vertexSize, tess.drawMode, vertexCount);
}
} finally {
if (swappedCtx != null) swapEntityIds(swappedCtx, restoreIds);
}
} finally {
if (locked) GLStateManager.releaseDrawLock();
@@ -206,6 +174,57 @@ public class TessellatorStreamingDrawer {
return result;
}
public static void fillMissingAttributes(Tessellator tess) {
final int vertexCount = tess.vertexCount;
final int[] rawBuffer = tess.rawBuffer;
final int defaultColor;
if (!tess.hasColor) {
final var c = GLStateManager.getColor();
defaultColor = ((int)(clamp(0f, 1f, c.getAlpha()) * 255) << 24) | ((int)(clamp(0f, 1f, c.getBlue()) * 255) << 16)
| ((int)(clamp(0f, 1f, c.getGreen()) * 255) << 8) | (int)(clamp(0f, 1f, c.getRed()) * 255);
} else {
defaultColor = 0;
}
final int defaultBrightness;
if (!tess.hasBrightness) {
defaultBrightness = GLSMConfig.packedLastBrightness();
} else {
defaultBrightness = 0;
}
final ImmediateExtendedAttribHandler normalHandler = GLSMHooks.immediateExtendedHandler;
final boolean faceNormals = !tess.hasNormals && normalHandler != null
&& ImmediateExtendedAttribHandler.extPrimVerts(tess.drawMode, vertexCount) == 4 && normalHandler.wantsFaceNormals();
final boolean fillNormal = !tess.hasNormals && !faceNormals;
final int defaultNormal;
if (fillNormal) {
final var n = ShaderManager.getCurrentNormal();
defaultNormal = ((int)(clamp(-1f, 1f, n.z) * 127) << 16) | (((int)(clamp(-1f, 1f, n.y) * 127) & 0xFF) << 8) | ((int)(clamp(-1f, 1f, n.x) * 127) & 0xFF);
} else {
defaultNormal = 0;
}
final int defaultTexU, defaultTexV;
if (!tess.hasTexture) {
final var tc = ShaderManager.getCurrentTexCoord();
defaultTexU = Float.floatToRawIntBits(tc.x);
defaultTexV = Float.floatToRawIntBits(tc.y);
} else {
defaultTexU = 0; defaultTexV = 0;
}
for (int i = 0; i < vertexCount; i++) {
final int base = i * 8;
if (!tess.hasTexture) { rawBuffer[base + 3] = defaultTexU; rawBuffer[base + 4] = defaultTexV; }
if (!tess.hasColor) { rawBuffer[base + 5] = defaultColor; }
if (fillNormal) { rawBuffer[base + 6] = defaultNormal; }
if (!tess.hasBrightness) { rawBuffer[base + 7] = defaultBrightness; }
}
if (faceNormals) normalHandler.writeFaceNormals(rawBuffer, vertexCount);
}
/**
* Draw DirectTessellator data via streaming VBO+VAO. Used for live immediate mode emulation.
*/
@@ -248,7 +267,7 @@ public class TessellatorStreamingDrawer {
final long srcBase = memAddress0(packed) + packed.position();
final int texOffset = ImmediateExtendedAttribHandler.texOffset(format);
final int normalOffset = ImmediateExtendedAttribHandler.normalOffset(format);
final int combinedStride = vertexSize + ImmediateExtendedAttribHandler.EXT_STRIDE;
final int combinedStride = combinedStride(format, StreamingVaos.MID_TEX_AND_TANGENT);
final int combinedBytes = vertexCount * combinedStride;
final boolean locked = GLStateManager.acquireDrawLock();
try {
@@ -257,7 +276,7 @@ public class TessellatorStreamingDrawer {
extHandler.buildPacked(srcBase, vertexSize, 0, texOffset, normalOffset, vertexCount, extPrim, extScratchAddress + vertexSize, combinedStride);
extScratch.position(0);
extScratch.limit(combinedBytes);
uploadAndDrawExtended(extScratch, flags, format, combinedStride, drawMode, vertexCount);
uploadAndDrawExtended(extScratch, flags, format, StreamingVaos.MID_TEX_AND_TANGENT, combinedStride, drawMode, vertexCount);
shrinkExtScratchIfOversized(combinedBytes);
} finally {
if (locked) GLStateManager.releaseDrawLock();
@@ -443,6 +462,7 @@ public class TessellatorStreamingDrawer {
FfpExtendedAttribs.beginInternalDraw();
try {
if (drawMode == GL11.GL_QUADS) {
GLStateManager.drawCalls++;
QuadConverter.drawQuadsAsTriangles(glCtx, firstVertex, vertexCount);
} else {
GLStateManager.glDrawArrays(drawMode, firstVertex, vertexCount);
@@ -452,11 +472,11 @@ public class TessellatorStreamingDrawer {
}
}
private static void uploadAndDrawExtended(ByteBuffer combined, int flags, VertexFormat format, int combinedStride, int drawMode, int vertexCount) {
private static void uploadAndDrawExtended(ByteBuffer combined, int flags, VertexFormat format, int layout, int combinedStride, int drawMode, int vertexCount) {
final GLContextState glCtx = GLStateManager.ctx();
final StreamingVaos vaos = glCtx.streamingVaos;
ensureVAO(vaos.persistentVAOs, vaos.orphanVAOs, flags, format);
ensureExtendedVAOs(vaos, flags, format);
ensureExtendedVAOs(vaos, flags, format, layout);
int firstVertex = -1;
@@ -465,9 +485,9 @@ public class TessellatorStreamingDrawer {
}
if (firstVertex >= 0) {
GLStateManager.glBindVertexArray(vaos.extendedPersistentVAOs[flags]);
GLStateManager.glBindVertexArray(vaos.extendedPersistent(layout)[flags]);
} else {
GLStateManager.glBindVertexArray(vaos.extendedOrphanVAOs[flags]);
GLStateManager.glBindVertexArray(vaos.extendedOrphan(layout)[flags]);
orphanBuffers[flags].upload(combined);
firstVertex = 0;
}
@@ -476,14 +496,14 @@ public class TessellatorStreamingDrawer {
GLStateManager.glBindVertexArray(0);
}
private static void ensureExtendedVAOs(StreamingVaos vaos, int flags, VertexFormat format) {
final int[] extendedOrphanVAOs = vaos.extendedOrphanVAOs;
final int[] extendedPersistentVAOs = vaos.extendedPersistentVAOs;
private static void ensureExtendedVAOs(StreamingVaos vaos, int flags, VertexFormat format, int layout) {
final int[] extendedOrphanVAOs = vaos.extendedOrphan(layout);
final int[] extendedPersistentVAOs = vaos.extendedPersistent(layout);
if (extendedOrphanVAOs[flags] == 0) {
extendedOrphanVAOs[flags] = GLStateManager.glGenVertexArrays();
GLStateManager.glBindVertexArray(extendedOrphanVAOs[flags]);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, orphanBuffers[flags].getBufferId());
setupExtendedBufferState(format);
setupExtendedBufferState(format, layout);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
}
@@ -492,20 +512,42 @@ public class TessellatorStreamingDrawer {
extendedPersistentVAOs[flags] = GLStateManager.glGenVertexArrays();
GLStateManager.glBindVertexArray(extendedPersistentVAOs[flags]);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, persistentBuffer.getBufferId());
setupExtendedBufferState(format);
setupExtendedBufferState(format, layout);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
}
}
private static void setupExtendedBufferState(VertexFormat format) {
final int combinedStride = format.getVertexSize() + ImmediateExtendedAttribHandler.EXT_STRIDE;
private static long swapEntityIds(GLContextState glCtx, long ids) {
final long previous = glCtx.entityIds;
if (previous != ids) {
glCtx.entityIds = ids;
glCtx.dirtyEntityIdsAttrib = true;
}
return previous;
}
private static int combinedStride(VertexFormat format, int layout) {
int stride = format.getVertexSize();
if ((layout & StreamingVaos.MID_TEX_AND_TANGENT) != 0) stride += ImmediateExtendedAttribHandler.EXT_STRIDE;
if ((layout & StreamingVaos.ENTITY_IDS) != 0) stride += ImmediateExtendedAttribHandler.ENTITY_IDS_STRIDE;
return stride;
}
private static void setupExtendedBufferState(VertexFormat format, int layout) {
final int combinedStride = combinedStride(format, layout);
long offset = 0;
for (final VertexFormatElement element : format.elementsArray) {
element.setupBufferState(offset, combinedStride);
offset += element.getByteSize();
}
ImmediateExtendedAttribHandler.setupExtAttribPointers(offset, combinedStride);
if ((layout & StreamingVaos.MID_TEX_AND_TANGENT) != 0) {
ImmediateExtendedAttribHandler.setupExtAttribPointers(offset, combinedStride);
offset += ImmediateExtendedAttribHandler.EXT_STRIDE;
}
if ((layout & StreamingVaos.ENTITY_IDS) != 0) {
ImmediateExtendedAttribHandler.setupEntityIdsPointer(offset, combinedStride);
}
}
private static void ensureExtScratch(int requiredBytes) {
@@ -6,6 +6,7 @@ import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL21;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -61,6 +62,18 @@ public class GLSM_DeletedBufferBinding_GLTest {
}
}
@Test
void deletedIndexedUniformBufferIsNotRestoredFromCache() {
final int index = 3;
final int ubo = makeBoundBuffer(GL31.GL_UNIFORM_BUFFER);
GLStateManager.glBindBufferBase(GL31.GL_UNIFORM_BUFFER, index, ubo);
assertEquals(ubo, GLStateManager.getIndexedBufferBinding(GL31.GL_UNIFORM_BUFFER, index));
GLStateManager.glDeleteBuffers(ubo);
assertEquals(0, GL30.glGetInteger(GL31.GL_UNIFORM_BUFFER_BINDING, index));
assertEquals(0, GLStateManager.getIndexedBufferBinding(GL31.GL_UNIFORM_BUFFER, index));
}
@Test
void deletedElementArrayBufferDoesNotBlockClientDrawElements() {
final int ebo = makeBoundBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER);
@@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -12,54 +11,54 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class BackendStartGuardTest {
private static File marker(Path dir) {
return dir.resolve("angelica").resolve("sdlgpu-starting").toFile();
@TempDir
Path dir;
private boolean boot() {
return boot(1);
}
private boolean boot(int revision) {
return BackendStartGuard.begin(dir.toFile(), true, false, revision, "");
}
private File marker() {
return dir.resolve("sdlgpu-starting").toFile();
}
@Test
void firstStartCreatesTheMarkerAndFinishRemovesIt(@TempDir Path dir) {
final File marker = marker(dir);
assertFalse(BackendStartGuard.begin(marker, true, false));
assertFalse(BackendStartGuard.tripped());
assertTrue(marker.isFile());
BackendStartGuard.finish();
assertFalse(marker.exists());
void bootsThatReachTheBackendNeverCountAsStrikes() {
for (int i = 0; i < BackendStartGuard.STRIKE_LIMIT; i++) {
assertFalse(boot());
BackendStartGuard.arm();
BackendStartGuard.disarm();
}
assertFalse(boot());
}
@Test
void leftoverMarkerTripsOnceAndIsRemoved(@TempDir Path dir) {
final File marker = marker(dir);
assertFalse(BackendStartGuard.begin(marker, true, false));
assertTrue(BackendStartGuard.begin(marker, true, false));
void thirdDeathInTheDeviceWindowResets() {
for (int i = 0; i < BackendStartGuard.STRIKE_LIMIT; i++) {
assertFalse(boot());
BackendStartGuard.arm();
}
assertTrue(boot());
assertTrue(BackendStartGuard.tripped());
assertFalse(marker.exists());
BackendStartGuard.finish();
assertFalse(marker.exists());
assertFalse(BackendStartGuard.begin(marker, true, false));
assertFalse(BackendStartGuard.tripped());
BackendStartGuard.finish();
assertFalse(marker().exists());
}
@Test
void openGlConfigWritesNoMarkerAndClearsAStaleOne(@TempDir Path dir) throws IOException {
final File marker = marker(dir);
assertFalse(BackendStartGuard.begin(marker, false, false));
assertFalse(marker.exists());
assertTrue(marker.getParentFile().mkdirs());
assertTrue(marker.createNewFile());
assertFalse(BackendStartGuard.begin(marker, false, false));
assertFalse(BackendStartGuard.tripped());
assertFalse(marker.exists());
}
void latchStopsArmingUntilTheRequirementsRevisionChanges() {
assertFalse(boot());
BackendStartGuard.arm();
BackendStartGuard.disarm();
BackendStartGuard.latch();
assertFalse(boot());
BackendStartGuard.arm();
assertFalse(marker().exists());
@Test
void forcedFlagNeverTripsOrWrites(@TempDir Path dir) throws IOException {
final File marker = marker(dir);
assertFalse(BackendStartGuard.begin(marker, true, true));
assertFalse(marker.exists());
assertTrue(marker.getParentFile().mkdirs());
assertTrue(marker.createNewFile());
assertFalse(BackendStartGuard.begin(marker, true, true));
assertFalse(BackendStartGuard.tripped());
assertFalse(boot(2));
BackendStartGuard.arm();
assertTrue(marker().isFile());
}
}
@@ -85,7 +85,7 @@ class InstancedFFPDrawGLTest {
GLSMHooks.pendingProgramSelection = null;
GLSMHooks.blendHandler = null;
log.clear();
FfpFixture.IntegerInstances.delete();
FfpFixture.EntityIdInstances.delete();
FfpFixture.resetFfpState();
CubeParityFixture.disableDirectionalLight();
CubeParityFixture.deleteResources();
@@ -196,14 +196,14 @@ class InstancedFFPDrawGLTest {
GLStateManager.disableDepthTest();
GLStateManager.disableCull();
FfpFixture.IntegerInstances.build(0.66f);
FfpFixture.EntityIdInstances.build(0.66f);
GLStateManager.glClearColor(0f, 0f, 0f, 1f);
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
FfpFixture.IntegerInstances.draw();
FfpFixture.EntityIdInstances.draw();
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "instanced ivec4 draw must not raise a GL error");
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "instanced entity id draw must not raise a GL error");
assertPixel(51, 50, 255, 0, 0, "entity -1 must render red");
assertPixel(150, 50, 0, 255, 0, "entity 7 must render green");
@@ -0,0 +1,69 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizons.angelica.glsm.EntityIdsOnlyHandler;
import com.gtnewhorizons.angelica.glsm.GLCompatTest;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.GlTestSupport;
import com.gtnewhorizons.angelica.glsm.ffp.FfpFixture;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@GLCompatTest
class TessellatorEntityIdsGLTest {
private static final int[] GREEN = { 0, 255, 0, 255 };
private static final int[] BLUE = { 0, 0, 255, 255 };
private ImmediateExtendedAttribHandler previousHandler;
private int program;
@BeforeEach
void setUp() {
previousHandler = EntityIdsOnlyHandler.install();
program = GlTestSupport.program(TessellatorEntityIdsFixture.VERTEX, TessellatorEntityIdsFixture.FRAGMENT);
GLStateManager.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
GLStateManager.disableDepthTest();
GLStateManager.disableCull();
GLStateManager.glViewport(0, 0, 8, 8);
GLStateManager.glUseProgram(program);
GLStateManager.glClearColor(0, 0, 0, 1);
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
}
@AfterEach
void tearDown() {
GLStateManager.glUseProgram(0);
GLStateManager.glDeleteProgram(program);
GLStateManager.glPopAttrib();
GLSMHooks.immediateExtendedHandler = previousHandler;
}
@Test
void idsChangedMidBatchApplyToTheVerticesAddedAfterTheChange() {
TessellatorEntityIdsFixture.drawIdsChangedMidBatch();
assertColor(1, GREEN, "left quad was added under item 7");
assertColor(6, BLUE, "right quad was added under item 50020");
}
@Test
void idsChangedAfterTheLastVertexLeaveTheBatchAlone() {
TessellatorEntityIdsFixture.drawIdsChangedAfterTheLastVertex();
assertColor(1, GREEN, "every vertex was added under item 7");
assertColor(6, GREEN, "every vertex was added under item 7");
}
private static void assertColor(int x, int[] expected, String message) {
final int[] pixel = FfpFixture.readPixel(x, 4);
assertArrayEquals(expected, pixel, message + ", got " + Arrays.toString(pixel));
}
}
@@ -0,0 +1,24 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
public final class EntityIdsOnlyHandler implements ImmediateExtendedAttribHandler {
public static ImmediateExtendedAttribHandler install() {
final ImmediateExtendedAttribHandler previous = GLSMHooks.immediateExtendedHandler;
GLSMHooks.immediateExtendedHandler = new EntityIdsOnlyHandler();
GLStateManager.ctx().dirtyEntityIdsAttrib = true;
return previous;
}
@Override public boolean wantsExtended() { return false; }
@Override public boolean wantsTangent() { return false; }
@Override public boolean wantsEntityIds() { return GLStateManager.getActiveProgram() != 0; }
@Override public boolean wantsExtendedCapture() { return false; }
@Override public boolean wantsFaceNormals() { return false; }
@Override public void writeFaceNormals(int[] rawBuffer, int vertexCount) {}
@Override public void build(int[] rawBuffer, int vertexCount, int vertsPerPrim, int normalIntIndex, long dstAddr, int dstStride) {}
@Override public void buildPacked(long srcBase, int stride, int posOffset, int texOffset, int normalOffset,
int vertexCount, int vertsPerPrim, long dstAddr, int dstStride) {}
}
@@ -21,7 +21,7 @@ public final class FfpFixture {
private FfpFixture() {}
public static final class IntegerInstances {
public static final class EntityIdInstances {
private static final int INSTANCES = 3;
@@ -31,16 +31,17 @@ public final class FfpFixture {
layout(location = 5) in vec4 a_InstRow0;
layout(location = 6) in vec4 a_InstRow1;
layout(location = 7) in vec4 a_InstRow2;
layout(location = 11) in uvec4 iris_Entity;
layout(location = 11) in vec4 iris_InstEntity;
flat out vec3 v_Color;
void main() {
""";
private static final String VS_TAIL = """
gl_Position = instMV * vec4(a_Position, 1.0);
if (iris_Entity.x == 65535u) { v_Color = vec3(1.0, 0.0, 0.0); }
else if (iris_Entity.x == 7u) { v_Color = vec3(0.0, 1.0, 0.0); }
else if (iris_Entity.x == 50020u) { v_Color = vec3(0.0, 0.0, 1.0); }
int entity = int(round(iris_InstEntity.x * 65535.0));
if (entity == 65535) { v_Color = vec3(1.0, 0.0, 0.0); }
else if (entity == 7) { v_Color = vec3(0.0, 1.0, 0.0); }
else if (entity == 50020) { v_Color = vec3(0.0, 0.0, 1.0); }
else { v_Color = vec3(1.0, 0.0, 1.0); }
}
""";
@@ -59,7 +60,7 @@ public final class FfpFixture {
private static int templateVbo;
private static int instanceVbo;
private IntegerInstances() {}
private EntityIdInstances() {}
public static void build(float spacing) {
program = buildProgram();
@@ -0,0 +1,112 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
import net.minecraft.client.renderer.Tessellator;
import org.lwjgl.opengl.GL11;
public final class TessellatorEntityIdsFixture {
public static final String VERTEX = """
#version 330 core
layout(location = 0) in vec3 a_Position;
layout(location = %d) in vec4 iris_Entity;
flat out vec3 v_Color;
void main() {
gl_Position = vec4(a_Position, 1.0);
int item = int(round(iris_Entity.z * 65535.0));
v_Color = item == 7 ? vec3(0.0, 1.0, 0.0) : item == 50020 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
}
""".formatted(ImmediateExtendedAttribHandler.LOC_ENTITY_IDS);
public static final String FRAGMENT = """
#version 330 core
flat in vec3 v_Color;
out vec4 fragColor;
void main() { fragColor = vec4(v_Color, 1.0); }
""";
private TessellatorEntityIdsFixture() {}
public static void drawIdsChangedMidBatch() {
draw(() -> {
GLStateManager.setEntityIds(-1, 0, 7);
startQuads();
addQuad(-1.0f, 0.0f);
GLStateManager.setEntityIds(-1, 0, 50020);
addQuad(0.0f, 1.0f);
GLStateManager.setEntityIds(-1, 0, -1);
});
}
public static void drawIdsChangedAfterTheLastVertex() {
draw(() -> {
GLStateManager.setEntityIds(-1, 0, 7);
startQuads();
addQuad(-1.0f, 1.0f);
GLStateManager.setEntityIds(-1, 0, 50020);
});
}
private static void draw(Runnable fill) {
final Tessellator tess = Tessellator.instance;
final int[] savedRawBuffer = tess.rawBuffer;
final int savedRawBufferSize = tess.rawBufferSize;
final int savedRawBufferIndex = tess.rawBufferIndex;
final int savedVertexCount = tess.vertexCount;
final int savedDrawMode = tess.drawMode;
final boolean savedIsDrawing = tess.isDrawing;
final boolean savedHasTexture = tess.hasTexture;
final boolean savedHasColor = tess.hasColor;
final boolean savedHasNormals = tess.hasNormals;
final boolean savedHasBrightness = tess.hasBrightness;
try {
fill.run();
TessellatorStreamingDrawer.draw(tess);
} finally {
TessellatorEntityIds.onReset(tess);
GLStateManager.setEntityIds(-1, 0, -1);
tess.rawBuffer = savedRawBuffer;
tess.rawBufferSize = savedRawBufferSize;
tess.rawBufferIndex = savedRawBufferIndex;
tess.vertexCount = savedVertexCount;
tess.drawMode = savedDrawMode;
tess.isDrawing = savedIsDrawing;
tess.hasTexture = savedHasTexture;
tess.hasColor = savedHasColor;
tess.hasNormals = savedHasNormals;
tess.hasBrightness = savedHasBrightness;
}
}
private static void startQuads() {
final Tessellator tess = Tessellator.instance;
tess.rawBuffer = new int[64];
tess.rawBufferSize = tess.rawBuffer.length;
tess.rawBufferIndex = 0;
tess.vertexCount = 0;
tess.drawMode = GL11.GL_QUADS;
tess.hasTexture = false;
tess.hasColor = false;
tess.hasNormals = false;
tess.hasBrightness = false;
tess.isDrawing = true;
}
private static void addQuad(float left, float right) {
addVertex(left, -1.0f);
addVertex(right, -1.0f);
addVertex(right, 1.0f);
addVertex(left, 1.0f);
}
private static void addVertex(float x, float y) {
final Tessellator tess = Tessellator.instance;
final int base = tess.rawBufferIndex;
tess.rawBuffer[base] = Float.floatToRawIntBits(x);
tess.rawBuffer[base + 1] = Float.floatToRawIntBits(y);
tess.rawBuffer[base + 2] = Float.floatToRawIntBits(0.0f);
tess.rawBufferIndex += 8;
tess.vertexCount++;
}
}
@@ -39,9 +39,11 @@ public final class GlsmSdlRedirectAgent {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> redefined, ProtectionDomain domain, byte[] classfile) {
if (className == null || !className.startsWith(GTNHLIB)) return null;
if (className == null) return null;
try {
if (!redirector.shouldTransform(classfile)) return null;
final byte[] observed = MovedSamplerProbe.instrument(className, classfile);
if (observed != null) return observed;
if (!className.startsWith(GTNHLIB) || !redirector.shouldTransform(classfile)) return null;
final ClassNode node = new ClassNode();
new ClassReader(classfile).accept(node, 0);
if (!redirector.transformClassNode(className.replace('/', '.'), node)) return null;
@@ -0,0 +1,73 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public final class MovedSamplerProbe implements AutoCloseable {
private static final String COPIES = "com/gtnewhorizons/angelica/sdlgpu/sampler/MovedSamplerCopies";
private static final String RESOURCES = "com/gtnewhorizons/angelica/sdlgpu/resource/ResourceManager";
private static final String PROBE = "com/gtnewhorizons/angelica/sdlgpu/glsm/MovedSamplerProbe";
private static volatile MovedSamplerProbe active;
private final AtomicInteger refreshCalls = new AtomicInteger();
private final ConcurrentHashMap<Long, AtomicInteger> writes = new ConcurrentHashMap<>();
public MovedSamplerProbe() {
if (active != null) throw new IllegalStateException("A moved-sampler probe is already active");
active = this;
}
public int refreshCalls() {
return refreshCalls.get();
}
public void watchWrites(long texture) {
writes.putIfAbsent(texture, new AtomicInteger());
}
public int writesTo(long texture) {
return writes.get(texture).get();
}
public static void onRefresh() {
final MovedSamplerProbe probe = active;
if (probe != null) probe.refreshCalls.incrementAndGet();
}
public static void onWrite(long texture) {
final MovedSamplerProbe probe = active;
if (probe == null) return;
final AtomicInteger count = probe.writes.get(texture);
if (count != null) count.incrementAndGet();
}
@Override
public void close() {
active = null;
}
static byte[] instrument(String className, byte[] classfile) {
final boolean writes = RESOURCES.equals(className);
if (!writes && !COPIES.equals(className)) return null;
final String methodName = writes ? "markTextureWritten" : "refresh";
final ClassNode node = new ClassNode();
new ClassReader(classfile).accept(node, 0);
final var method = node.methods.stream().filter(m -> m.name.equals(methodName)).findFirst()
.orElseThrow(() -> new IllegalStateException("Missing observed method: " + className + "." + methodName));
final InsnList hook = new InsnList();
if (writes) hook.add(new VarInsnNode(Opcodes.LLOAD, 1));
hook.add(new MethodInsnNode(Opcodes.INVOKESTATIC, PROBE, writes ? "onWrite" : "onRefresh", writes ? "(J)V" : "()V", false));
method.instructions.insert(hook);
final ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
node.accept(writer);
return writer.toByteArray();
}
}
@@ -4,6 +4,7 @@ import static org.lwjgl.sdl.SDLVideo.SDL_PROP_WINDOW_CREATE_METAL_BOOLEAN;
import com.gtnewhorizons.angelica.glsm.backend.BackendOptions;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.BackendStartGuard;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.loading.DependencyVerifier;
import com.gtnewhorizons.angelica.glsm.loading.DependencyVerifier.Check;
@@ -55,6 +56,7 @@ public final class SDLGPUGate {
}
public static final String LWJGL3IFY_MIN_VERSION = "3.0.25";
public static final int REQUIREMENTS_REVISION = 1;
private static final List<Check> REQUIREMENTS = List.of(
new Check("/org/lwjgl/sdl/SDLGPU.class", "no lwjgl-sdl on the classpath"),
@@ -90,12 +92,16 @@ public final class SDLGPUGate {
boolean ok;
try {
Sys.initialize();
BackendStartGuard.arm();
ok = MainThreadExec.runOnMainThread(device()::createDevice);
} catch (Throwable t) {
LOG.error("SDL GPU device probe failed", t);
ok = false;
}
if (!ok) resetSdlLogging();
if (!ok) {
resetSdlLogging();
BackendStartGuard.disarm();
}
deviceReady = ok;
return ok;
}
@@ -182,6 +188,7 @@ public final class SDLGPUGate {
throw new IllegalStateException("Celeritas already selected the SDL GPU LWJGL service; falling back to OpenGL would leave it bound to a dead backend");
}
SDLGPULWJGLService.unbind();
BackendStartGuard.disarm();
disarmed = true;
engaged = false;
deviceReady = false;
@@ -32,6 +32,8 @@ import com.gtnewhorizons.angelica.sdlgpu.pipeline.AttachmentClear;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.DepthStencilReadback;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.DrawDispatch;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.FFPTrace;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.FullscreenPass;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.MovedSamplerConversion;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineApplier;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineCache;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineStore;
@@ -54,9 +56,11 @@ import com.gtnewhorizons.angelica.sdlgpu.resource.ReadbackShadows;
import com.gtnewhorizons.angelica.sdlgpu.resource.TextureOps;
import com.gtnewhorizons.angelica.sdlgpu.resource.TextureSamplerState;
import com.gtnewhorizons.angelica.sdlgpu.resource.TransferThread;
import com.gtnewhorizons.angelica.sdlgpu.sampler.MovedSamplerCopies;
import com.gtnewhorizons.angelica.sdlgpu.sampler.SamplerBinder;
import com.gtnewhorizons.angelica.sdlgpu.sampler.StorageBufferBinder;
import com.gtnewhorizons.angelica.sdlgpu.sampler.StorageTextureBinder;
import com.gtnewhorizons.angelica.sdlgpu.shader.SamplerOverflow;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import com.gtnewhorizons.angelica.sdlgpu.splash.SplashDispatcher;
import com.gtnewhorizons.angelica.sdlgpu.frame.OffscreenTarget;
@@ -138,6 +142,7 @@ public class SDLGPURenderBackend extends RenderBackend {
private final Image3DClear image3DClear = new Image3DClear();
private final ShaderManager shaderManager = new ShaderManager(device);
private final PipelineStore pipelineStore = new PipelineStore(device);
private final FullscreenPass fullscreenPass = new FullscreenPass(shaderManager, device);
private String cachedRenderer;
private String cachedVendor;
@@ -152,7 +157,9 @@ public class SDLGPURenderBackend extends RenderBackend {
private final LongArrayFIFOQueue frameSyncs = new LongArrayFIFOQueue();
private final FBOClearTracker fboClearTracker = new FBOClearTracker(frameManager, resourceManager, shaderManager);
private final SamplerBinder samplerBinder = new SamplerBinder(device, resourceManager, shaderManager);
private final StorageTextureBinder storageTextureBinder = new StorageTextureBinder(resourceManager, shaderManager);
private final MovedSamplerConversion movedSamplerConversion = new MovedSamplerConversion(frameManager, pipelineStore, fullscreenPass);
private final MovedSamplerCopies movedSamplerCopies = new MovedSamplerCopies(device, frameManager, resourceManager, movedSamplerConversion);
private final StorageTextureBinder storageTextureBinder = new StorageTextureBinder(resourceManager, shaderManager, movedSamplerCopies);
private final StorageBufferBinder storageBufferBinder = new StorageBufferBinder(resourceManager, shaderManager);
private final ComputeBinder computeBinder = new ComputeBinder(frameManager, resourceManager, shaderManager, samplerBinder);
private final VoxelizationDispatcher voxelizationDispatcher =
@@ -165,10 +172,10 @@ public class SDLGPURenderBackend extends RenderBackend {
@Override public void enqueue(TransferThread.DeferredUpload upload) { enqueueUpload(upload); }
@Override public long nextSeq() { return TransferThread.nextSeq(); }
});
private final PipelineApplier pipelineApplier = new PipelineApplier(frameManager, resourceManager, shaderManager, pipelineStore, fboClearTracker, persistentSync, samplerBinder, storageTextureBinder, storageBufferBinder);
private final PipelineApplier pipelineApplier = new PipelineApplier(frameManager, resourceManager, shaderManager, pipelineStore, fboClearTracker, persistentSync, samplerBinder, storageTextureBinder, storageBufferBinder, movedSamplerCopies);
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 AttachmentClear attachmentClear = new AttachmentClear(frameManager, pipelineStore, fullscreenPass);
private final DepthStencilReadback depthStencilReadback = new DepthStencilReadback(frameManager, resourceManager, pipelineStore, fullscreenPass, fboClearTracker);
private final DrawDispatch drawDispatch = new DrawDispatch(device, frameManager, resourceManager, pipelineApplier, this::enqueuePreCopied);
private final FFPTrace ffpTrace = SystemProperties.FFP_TRACE ? new FFPTrace(resourceManager) : null;
@@ -306,6 +313,7 @@ public class SDLGPURenderBackend extends RenderBackend {
pipelineApplier.setDeferredUploadSink(this::enqueuePreCopied);
frameManager.setRenderPassEndHook(vertexWriteReplayer::flush);
resourceManager.setTextureReplaceHook(this::flushVertexWritesBeforeTextureChange);
resourceManager.setTextureReleaseListener(movedSamplerCopies::releaseCopyOf);
frameManager.setPreRenderPassHook(() -> {
final ContextState st = s();
pipelineApplier.flushUniformBlocks(st);
@@ -361,6 +369,7 @@ public class SDLGPURenderBackend extends RenderBackend {
LOG.info("Splash offscreen target {}x{} (window {}x{}, max desktop {}x{})", splashTexW, splashTexH, splashW, splashH, maxDesktop[0], maxDesktop[1]);
}
Lwjgl3GLCapabilitiesShim.installFunctionProvider();
Lwjgl3GLCapabilitiesShim.installOnCurrentThread(advertisedCapabilities());
LOG.info("SDLGPURenderBackend initialized: renderer={}, version={}", cachedRenderer, cachedVersion);
@@ -534,8 +543,10 @@ public class SDLGPURenderBackend extends RenderBackend {
}
attachmentClear.shutdown();
depthStencilReadback.shutdown();
fullscreenPass.shutdown();
textureOps.shutdown();
pipelineStore.shutdown();
movedSamplerCopies.releaseAll();
shaderManager.shutdown();
resourceManager.shutdown();
device.shutdown();
@@ -671,8 +682,10 @@ public class SDLGPURenderBackend extends RenderBackend {
if (shutdown) return;
if (splashTeardownPending) finishSplash();
if (GLStateManager.isSplashComplete()) applyPendingVSync();
final boolean wasActive = frameManager.isFrameActive();
frameManager.beginFrame();
beginFrameInit();
if (!wasActive) movedSamplerCopies.releaseUnused();
}
private void beginFrameInit() {
@@ -2632,6 +2645,7 @@ public class SDLGPURenderBackend extends RenderBackend {
prog.locationSamplerUnit[location] = v0;
prog.samplerTextureUnits.put(prog.locationName[location], v0);
prog.samplerUnitsDirty = true;
st.samplerBindGen++;
}
return;
}
@@ -3735,7 +3749,9 @@ public class SDLGPURenderBackend extends RenderBackend {
frameManager.endRenderPassIfActive(FrameManager.PASS_END_COPY);
frameManager.noteMipGen();
textureOps.clearPendingMipGen(texture);
SDL_GenerateMipmapsForGPUTexture(frameManager.getCommandBuffer(), resourceManager.getTextureHandle(texture));
final long texHandle = resourceManager.getTextureHandle(texture);
SDL_GenerateMipmapsForGPUTexture(frameManager.getCommandBuffer(), texHandle);
resourceManager.markTextureWritten(texHandle);
}
@Override public void textureImage2DEXT(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
if (texture == 0) return;
@@ -4016,8 +4032,8 @@ public class SDLGPURenderBackend extends RenderBackend {
@Override public int getInteger(int pname) {
final ContextState cs = s();
return switch (pname) {
case GL20.GL_MAX_TEXTURE_IMAGE_UNITS -> 17;
case GL20.GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS -> 17;
// Iris budgets unit assignments here; only 16 can be filtered, with up to 8 float 2D fetch-only samplers.
case GL20.GL_MAX_TEXTURE_IMAGE_UNITS, GL20.GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS -> SamplerOverflow.SAMPLER_SLOTS + SamplerOverflow.TEXTURE_SLOTS;
case GL20.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS -> 32;
case GL20.GL_MAX_VERTEX_ATTRIBS -> 16;
case GL20.GL_MAX_DRAW_BUFFERS -> 8;
@@ -4036,7 +4052,7 @@ public class SDLGPURenderBackend extends RenderBackend {
case GL43.GL_MAX_DEBUG_MESSAGE_LENGTH -> 1024;
case GL11.GL_DRAW_BUFFER -> drawBufferEnum(cs.boundFboId, cs.defaultFboId);
case GL11.GL_READ_BUFFER -> readBufferEnum(cs.boundReadFboId, cs.defaultFboId);
case GL11.GL_TEXTURE_BINDING_2D, GL11.GL_TEXTURE_BINDING_1D, GL12.GL_TEXTURE_BINDING_3D, GL13.GL_TEXTURE_BINDING_CUBE_MAP, GL30.GL_TEXTURE_BINDING_1D_ARRAY, GL30.GL_TEXTURE_BINDING_2D_ARRAY, GL31.GL_TEXTURE_BINDING_RECTANGLE, GL40.GL_TEXTURE_BINDING_CUBE_MAP_ARRAY -> boundTextureOf(cs.activeTextureUnit, cs.boundTextures);
case GL11.GL_TEXTURE_BINDING_2D, GL11.GL_TEXTURE_BINDING_1D, GL12.GL_TEXTURE_BINDING_3D, GL13.GL_TEXTURE_BINDING_CUBE_MAP, GL30.GL_TEXTURE_BINDING_1D_ARRAY, GL30.GL_TEXTURE_BINDING_2D_ARRAY, GL31.GL_TEXTURE_BINDING_RECTANGLE, GL40.GL_TEXTURE_BINDING_CUBE_MAP_ARRAY -> cs.boundTexture(cs.activeTextureUnit);
case GL11.GL_DEPTH_BITS -> getFramebufferAttachmentParameteri(GL30.GL_DRAW_FRAMEBUFFER, GL11.GL_DEPTH, GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE);
case GL11.GL_STENCIL_BITS -> getFramebufferAttachmentParameteri(GL30.GL_DRAW_FRAMEBUFFER, GL11.GL_STENCIL, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE);
case GL11.GL_PACK_ALIGNMENT -> cs.pixelStore.packAlignment;
@@ -4070,10 +4086,6 @@ public class SDLGPURenderBackend extends RenderBackend {
return bufferEnum(fboId, fbo == null ? -1 : fbo.readBufferIndex);
}
static int boundTextureOf(int unit, int[] boundTextures) {
return (unit >= 0 && unit < boundTextures.length) ? boundTextures[unit] : 0;
}
static int deviceLimit(int pname) {
return switch (pname) {
case GL20.GL_MAX_VERTEX_UNIFORM_COMPONENTS, GL20.GL_MAX_FRAGMENT_UNIFORM_COMPONENTS -> MAX_UNIFORM_COMPONENTS_PER_STAGE;
@@ -4320,6 +4332,7 @@ public class SDLGPURenderBackend extends RenderBackend {
final var dst = SDL_GPUTextureLocation.calloc(stack)
.texture(dstTex).mip_level(dstLevel).x(dstX).y(dstY).z(dstZ);
SDL_CopyGPUTextureToTexture(cp, src, dst, srcWidth, srcHeight, Math.max(1, srcDepth), false);
resourceManager.markTextureWritten(dstTex);
}
}
@@ -31,6 +31,16 @@ public final class Lwjgl3GLCapabilitiesShim {
LOG.info("Cleared fabricated LWJGL3 GLCapabilities on {}", Thread.currentThread().getName());
}
/**
* Aware mods probe extension entry points through {@code GL.getFunctionProvider()}, which is null with no GL context.
* Every name resolves to 0 so those probes see the extension as absent and use the core path, which GLSM routes.
*/
public static void installFunctionProvider() {
if (GL.getFunctionProvider() != null) return;
GL.create(name -> 0L);
LOG.info("Installed null-address LWJGL3 GL function provider");
}
public static GLCapabilities create(Set<String> advertised) {
final FunctionProvider provider = name -> SENTINEL_ADDRESS;
final IntFunction<PointerBuffer> bufferFactory = BufferUtils::createPointerBuffer;
@@ -8,6 +8,7 @@ import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import com.gtnewhorizons.angelica.sdlgpu.sampler.SamplerBinder;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import com.gtnewhorizons.angelica.sdlgpu.shader.UniformStaging;
import com.gtnewhorizons.angelica.sdlgpu.util.MemoryAccess;
import org.lwjgl.PointerBuffer;
import org.lwjgl.sdl.SDL_GPUStorageBufferReadWriteBinding;
import org.lwjgl.sdl.SDL_GPUStorageTextureReadWriteBinding;
@@ -251,6 +252,11 @@ public final class ComputeBinder implements ComputeDispatchSink {
final long pass = SDL_BeginGPUComputePass(cb, rwTex, rwBuf);
if (pass == 0) return 0;
frameManager.noteComputePassBegun();
if (rwTex != null) {
for (int i = 0; i < rwTex.limit(); i++) {
resourceManager.markTextureWritten(MemoryAccess.getAddress(rwTex.address(i) + SDL_GPUStorageTextureReadWriteBinding.TEXTURE));
}
}
final boolean ok = bindComputeSamplers(st, prog, pass, samplerSlots, map.samplerNames(), stack)
&& bindRoStorageTextures(st, prog, pass, map, stack)
@@ -361,6 +361,10 @@ public final class Device {
return isMetal(driverName);
}
public boolean isVulkan() {
return isVulkan(driverName);
}
public boolean isFencePollEnabled() {
return fencePollEnabled;
}
@@ -173,6 +173,12 @@ public final class ContextState {
public int lastAppliedStorageTexBindGen = -1;
public int lastAppliedStorageTexProgram;
public long lastAppliedStorageTexCb;
public ShaderManager.ProgramObject lastPreparedMovedSamplerProgram;
public final int[] lastPreparedVertexMovedSamplerTextures = new int[MAX_IMAGE_UNITS];
public final int[] lastPreparedFragmentMovedSamplerTextures = new int[MAX_IMAGE_UNITS];
public int lastPreparedMovedSamplerUnitsGen = -1;
public long lastPreparedMovedSamplerWriteGen = -1;
public long lastPreparedMovedSamplerPass = -1;
public int ssboBindGen;
public int lastAppliedStorageBufBindGen = -1;
public int lastAppliedStorageBufProgram;
@@ -304,6 +310,10 @@ public final class ContextState {
public final int[] boundStorageTextureAccess = new int[MAX_IMAGE_UNITS];
public final int[] boundStorageTextureFormat = new int[MAX_IMAGE_UNITS];
public final int[] boundTextures = new int[32];
public int boundTexture(int unit) {
return unit >= 0 && unit < boundTextures.length ? boundTextures[unit] : 0;
}
public final int[] boundSamplerObjects = new int[32];
public boolean computeBatchRequested;
@@ -290,13 +290,19 @@ public final class FrameManager {
f.emptyFramesThisFrame++;
}
static boolean swapchainClearNeedsPassBreak(ContextState st, FrameState f) {
return (st.pendingSwapchainClear || st.pendingSwapchainDepthClear || st.pendingSwapchainStencilClear) && f.renderPass != 0 && f.activeLayoutHash == FBO0_LAYOUT_HASH;
private static boolean isFbo0PassOpen(FrameState f) {
return f.renderPass != 0 && f.activeLayoutHash == FBO0_LAYOUT_HASH;
}
public static boolean fbo0PassStaysOpen(ContextState st, FrameState f) {
return isFbo0PassOpen(f)
&& !st.pendingSwapchainClear && !st.pendingSwapchainDepthClear && !st.pendingSwapchainStencilClear;
}
public boolean ensureFbo0RenderPass(FrameState f, ContextState st) {
if (fbo0PassStaysOpen(st, f)) return false;
final boolean clearColor = st.pendingSwapchainClear;
if (swapchainClearNeedsPassBreak(st, f)) {
if (isFbo0PassOpen(f)) {
endRenderPassIfActive(f, PASS_END_CLEAR);
}
final boolean applied = ensureFbo0RenderPass(f,
@@ -316,9 +322,6 @@ public final class FrameManager {
private boolean ensureFbo0RenderPass(FrameState f, float clearR, float clearG, float clearB, float clearA, boolean clear, boolean clearDepth, float depthValue, boolean clearStencil, int stencilValue) {
if (!finalTarget.isReady() || f.commandBuffer == 0) return false;
if (f.renderPass != 0 && f.activeLayoutHash == FBO0_LAYOUT_HASH) {
return false;
}
endActiveEncoders(f, PASS_END_TARGET);
@@ -452,6 +455,14 @@ public final class FrameManager {
f.fboPassesThisFrame++;
f.currentColorTarget = colorTargets != null && colorTargets.remaining() > 0 ? colorTargets.texture() : 0;
f.currentDepthTarget = depthTarget != null ? MemoryAccess.getAddress(depthTarget.address() + SDL_GPUDepthStencilTargetInfo.TEXTURE) : 0;
if (resourceManager != null) {
if (colorTargets != null) {
for (int i = colorTargets.position(); i < colorTargets.limit(); i++) {
resourceManager.markTextureWritten(MemoryAccess.getAddress(colorTargets.address(i) + SDL_GPUColorTargetInfo.TEXTURE));
}
}
resourceManager.markTextureWritten(f.currentDepthTarget);
}
return f.renderPass;
}
@@ -6,7 +6,6 @@ import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager.FrameState;
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
import com.gtnewhorizons.angelica.sdlgpu.resource.PixelOps;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
import org.lwjgl.sdl.SDL_GPUColorTargetDescription;
import org.lwjgl.sdl.SDL_GPUDepthStencilState;
@@ -48,8 +47,7 @@ public final class AttachmentClear {
private final Target fbo0Target = new Target();
private final FullscreenPass fullscreen;
private final long[] sdlFragmentShaders = new long[MAX_COLOR_TARGETS + 1];
private final boolean[] fragmentFailed = new boolean[MAX_COLOR_TARGETS + 1];
private final FullscreenPass.Fragment[] fragments = new FullscreenPass.Fragment[MAX_COLOR_TARGETS + 1];
private final long[] cacheKeys = new long[CACHE_SLOTS];
private final long[] cachePipelines = new long[CACHE_SLOTS];
@@ -65,10 +63,17 @@ public final class AttachmentClear {
private int buildStencilMask;
private int buildColorMask;
public AttachmentClear(FrameManager frameManager, PipelineStore pipelineStore, ShaderManager shaderManager) {
public AttachmentClear(FrameManager frameManager, PipelineStore pipelineStore, FullscreenPass fullscreen) {
this.frameManager = frameManager;
this.pipelineStore = pipelineStore;
this.fullscreen = new FullscreenPass(shaderManager);
this.fullscreen = fullscreen;
for (int i = 0; i <= MAX_COLOR_TARGETS; i++) {
final int count = i;
fragments[i] = fullscreen.fragment(FRAGMENT_SOURCE + " COLOR_TARGETS=" + count, count > 0 ? 16 : -1, () -> {
final String source = ShaderLoader.getShaderSource(FRAGMENT_SOURCE);
return count == 0 ? source : withColorOutputs(source, count);
});
}
}
private final class Builder implements LongSupplier {
@@ -171,7 +176,7 @@ public final class AttachmentClear {
}
long getOrCreatePipeline(Target t, int colorTargets, boolean depth, boolean stencil, int stencilMask, int colorMask) {
if (!ensureShaders(colorTargets)) return 0;
if (colorTargets < 0 || colorTargets > MAX_COLOR_TARGETS || fragments[colorTargets].shader() == 0) return 0;
final int[] formats = t.colorFormats;
final int count = formats != null ? formats.length : 0;
@@ -239,26 +244,10 @@ public final class AttachmentClear {
.compare_op(SDL_GPU_COMPAREOP_ALWAYS);
}
return fullscreen.createPipeline(pipelineStore, stack, sdlFragmentShaders[buildColorTargets], colorDesc, numColorTargets, depthStencil, t.depthFormat, "AttachmentClear");
return fullscreen.createPipeline(pipelineStore, stack, fragments[buildColorTargets].shader(), colorDesc, numColorTargets, depthStencil, t.depthFormat, "AttachmentClear");
}
}
private boolean ensureShaders(int colorTargets) {
if (colorTargets < 0 || colorTargets > MAX_COLOR_TARGETS) return false;
if (sdlFragmentShaders[colorTargets] != 0) return true;
if (fragmentFailed[colorTargets]) return false;
fragmentFailed[colorTargets] = true;
final String baseSource = ShaderLoader.getShaderSource(FRAGMENT_SOURCE);
final String fragmentSource = colorTargets == 0 ? baseSource : withColorOutputs(baseSource, colorTargets);
final long fragmentShader = fullscreen.compileFragment(fragmentSource, colorTargets > 0 ? 16 : -1, "AttachmentClear: clear_attachment COLOR_TARGETS=" + colorTargets);
if (fragmentShader == 0) return false;
sdlFragmentShaders[colorTargets] = fragmentShader;
fragmentFailed[colorTargets] = false;
return true;
}
private static String withColorOutputs(String source, int count) {
final GlslTransformUtils.QuietParse parsed = GlslTransformUtils.parseBothQuiet(source);
final Transformer transformer = new Transformer(parsed.full());
@@ -274,9 +263,6 @@ public final class AttachmentClear {
}
public void shutdown() {
fullscreen.shutdown();
Arrays.fill(sdlFragmentShaders, 0L);
Arrays.fill(fragmentFailed, false);
Arrays.fill(cacheKeys, 0L);
Arrays.fill(cachePipelines, 0L);
cacheNext = 0;
@@ -5,18 +5,12 @@ import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager.FrameState;
import com.gtnewhorizons.angelica.sdlgpu.resource.FBOClearTracker;
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import com.gtnewhorizons.angelica.sdlgpu.util.MemoryAccess;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
import org.lwjgl.sdl.SDLError;
import org.lwjgl.sdl.SDL_FColor;
import org.lwjgl.sdl.SDL_GPUColorTargetDescription;
import org.lwjgl.sdl.SDL_GPUColorTargetInfo;
import org.lwjgl.sdl.SDL_GPUDepthStencilState;
import org.lwjgl.sdl.SDL_GPUDepthStencilTargetInfo;
import org.lwjgl.sdl.SDL_GPUSamplerCreateInfo;
import org.lwjgl.sdl.SDL_GPUTextureSamplerBinding;
import org.lwjgl.system.MemoryStack;
@@ -29,8 +23,6 @@ import static org.lwjgl.system.MemoryStack.stackPush;
public final class DepthStencilReadback {
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
private static final String DEPTH_FRAGMENT_SOURCE = "angelica:sdlgpu/readback_depth.fsh";
private static final String STENCIL_FRAGMENT_SOURCE = "angelica:sdlgpu/readback_stencil.fsh";
private static final long KEY_SEED = 0x3C6EF372A54FF53AL;
@@ -44,11 +36,8 @@ public final class DepthStencilReadback {
private final FBOClearTracker fboClearTracker;
private final FullscreenPass fullscreen;
private long depthFragmentShader;
private boolean depthFragmentFailed;
private long stencilFragmentShader;
private boolean stencilFragmentFailed;
private long nearestSampler;
private final FullscreenPass.Fragment depthFragment;
private final FullscreenPass.Fragment stencilFragment;
private long depthPipeline;
private final long[] stencilPipelines = new long[STENCIL_BITS];
@@ -58,12 +47,14 @@ public final class DepthStencilReadback {
private int buildBit;
private int buildDepthFormat;
public DepthStencilReadback(FrameManager frameManager, ResourceManager resourceManager, PipelineStore pipelineStore, ShaderManager shaderManager, FBOClearTracker fboClearTracker) {
public DepthStencilReadback(FrameManager frameManager, ResourceManager resourceManager, PipelineStore pipelineStore, FullscreenPass fullscreen, FBOClearTracker fboClearTracker) {
this.frameManager = frameManager;
this.resourceManager = resourceManager;
this.pipelineStore = pipelineStore;
this.fboClearTracker = fboClearTracker;
this.fullscreen = new FullscreenPass(shaderManager);
this.fullscreen = fullscreen;
depthFragment = fullscreen.fragment(DEPTH_FRAGMENT_SOURCE, -1);
stencilFragment = fullscreen.fragment(STENCIL_FRAGMENT_SOURCE, 16);
}
private final class Builder implements LongSupplier {
@@ -75,7 +66,7 @@ public final class DepthStencilReadback {
if (!canRender(f, depthTexture, width, height)) return 0L;
if (depthTexture == resourceManager.getSwapchainDepthStencil() && !resourceManager.isSwapchainDepthStencilSampleable()) return 0L;
final long pipeline = getDepthPipeline();
final long sampler = getNearestSampler();
final long sampler = fullscreen.getNearestSampler();
if (pipeline == 0L || sampler == 0L) return 0L;
applyPendingClears(st, f, depthTexture);
final long scratch = ensureDepthScratch(st, width, height);
@@ -178,7 +169,7 @@ public final class DepthStencilReadback {
private long getDepthPipeline() {
if (depthPipeline != 0L) return depthPipeline;
if (!ensureDepthShader()) return 0L;
if (depthFragment.shader() == 0L) return 0L;
buildBit = -1;
buildDepthFormat = 0;
depthPipeline = pipelineStore.getOrBuild(Hashing.fmix64(KEY_SEED, -1), builder);
@@ -188,7 +179,7 @@ public final class DepthStencilReadback {
private boolean ensureStencilPipelines(int depthFormat) {
if (depthFormat == 0) return false;
if (stencilPipelinesFormat == depthFormat) return true;
if (!ensureStencilShader()) return false;
if (stencilFragment.shader() == 0L) return false;
buildDepthFormat = depthFormat;
final long formatKey = Hashing.fmix64(KEY_SEED, depthFormat);
for (int b = 0; b < STENCIL_BITS; b++) {
@@ -210,7 +201,7 @@ public final class DepthStencilReadback {
colorDesc.get(0).format(DEPTH_TARGET_FORMAT).blend_state().enable_blend(false);
final SDL_GPUDepthStencilState depthStencil = SDL_GPUDepthStencilState.calloc(stack)
.compare_op(SDL_GPU_COMPAREOP_ALWAYS);
return fullscreen.createPipeline(pipelineStore, stack, depthFragmentShader, colorDesc, 1, depthStencil, 0, "DepthStencilReadback depth");
return fullscreen.createPipeline(pipelineStore, stack, depthFragment.shader(), colorDesc, 1, depthStencil, 0, "DepthStencilReadback depth");
}
}
@@ -242,46 +233,10 @@ public final class DepthStencilReadback {
.pass_op(SDL_GPU_STENCILOP_KEEP)
.depth_fail_op(SDL_GPU_STENCILOP_KEEP)
.compare_op(SDL_GPU_COMPAREOP_EQUAL);
return fullscreen.createPipeline(pipelineStore, stack, stencilFragmentShader, colorDesc, 1, depthStencil, depthFormat, "DepthStencilReadback stencil");
return fullscreen.createPipeline(pipelineStore, stack, stencilFragment.shader(), colorDesc, 1, depthStencil, depthFormat, "DepthStencilReadback stencil");
}
}
private boolean ensureDepthShader() {
if (depthFragmentShader != 0L) return true;
if (depthFragmentFailed) return false;
depthFragmentFailed = true;
depthFragmentShader = fullscreen.compileFragment(ShaderLoader.getShaderSource(DEPTH_FRAGMENT_SOURCE), -1, "DepthStencilReadback: readback_depth");
if (depthFragmentShader == 0L) return false;
depthFragmentFailed = false;
return true;
}
private boolean ensureStencilShader() {
if (stencilFragmentShader != 0L) return true;
if (stencilFragmentFailed) return false;
stencilFragmentFailed = true;
stencilFragmentShader = fullscreen.compileFragment(ShaderLoader.getShaderSource(STENCIL_FRAGMENT_SOURCE), 16, "DepthStencilReadback: readback_stencil");
if (stencilFragmentShader == 0L) return false;
stencilFragmentFailed = false;
return true;
}
private long getNearestSampler() {
if (nearestSampler != 0L) return nearestSampler;
try (MemoryStack stack = stackPush()) {
final SDL_GPUSamplerCreateInfo ci = SDL_GPUSamplerCreateInfo.calloc(stack)
.min_filter(SDL_GPU_FILTER_NEAREST)
.mag_filter(SDL_GPU_FILTER_NEAREST)
.mipmap_mode(SDL_GPU_SAMPLERMIPMAPMODE_NEAREST)
.address_mode_u(SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE)
.address_mode_v(SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE)
.address_mode_w(SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE);
nearestSampler = SDL_CreateGPUSampler(pipelineStore.device().getDevice(), ci);
if (nearestSampler == 0L) LOG.error("DepthStencilReadback: sampler creation failed: {}", SDLError.SDL_GetError());
}
return nearestSampler;
}
public void release(ContextState st) {
if (st.readbackDepthScratch != 0L) resourceManager.releaseTextureHandle(st.readbackDepthScratch);
st.readbackDepthScratch = 0L;
@@ -294,19 +249,6 @@ public final class DepthStencilReadback {
}
public void shutdown() {
final long dev = pipelineStore.device().getDevice();
if (dev != 0L) {
if (depthFragmentShader != 0L) SDL_ReleaseGPUShader(dev, depthFragmentShader);
if (stencilFragmentShader != 0L) SDL_ReleaseGPUShader(dev, stencilFragmentShader);
if (fullscreen.vertexShader() != 0L) SDL_ReleaseGPUShader(dev, fullscreen.vertexShader());
if (nearestSampler != 0L) SDL_ReleaseGPUSampler(dev, nearestSampler);
}
fullscreen.shutdown();
depthFragmentShader = 0L;
depthFragmentFailed = false;
stencilFragmentShader = 0L;
stencilFragmentFailed = false;
nearestSampler = 0L;
depthPipeline = 0L;
Arrays.fill(stencilPipelines, 0L);
stencilPipelinesFormat = 0;
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.sdlgpu.pipeline;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import com.gtnewhorizons.angelica.sdlgpu.util.MemoryAccess;
import org.apache.logging.log4j.LogManager;
@@ -14,32 +15,90 @@ import org.lwjgl.sdl.SDL_GPUGraphicsPipelineCreateInfo;
import org.lwjgl.sdl.SDL_GPUGraphicsPipelineTargetInfo;
import org.lwjgl.sdl.SDL_GPUMultisampleState;
import org.lwjgl.sdl.SDL_GPURasterizerState;
import org.lwjgl.sdl.SDL_GPUSamplerCreateInfo;
import org.lwjgl.sdl.SDL_GPUVertexInputState;
import org.lwjgl.sdl.SDL_GPUViewport;
import org.lwjgl.sdl.SDL_Rect;
import org.lwjgl.system.MemoryStack;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import static org.lwjgl.sdl.SDLGPU.*;
final class FullscreenPass {
/** Shared fullscreen shaders and sampler, owned and released by the backend. */
public final class FullscreenPass {
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
private static final String VERTEX_SOURCE = "angelica:sdlgpu/clear_attachment.vsh";
private final ShaderManager shaderManager;
private final Device device;
private final List<Fragment> fragments = new ArrayList<>();
private int vertexShaderId;
private long sdlVertexShader;
private long nearestSampler;
private boolean samplerAttempted;
private boolean vertexFailed;
FullscreenPass(ShaderManager shaderManager) {
public FullscreenPass(ShaderManager shaderManager, Device device) {
this.shaderManager = shaderManager;
this.device = device;
}
long vertexShader() {
return sdlVertexShader;
final class Fragment {
private final String label;
private final Supplier<String> source;
private final int requiredUboSize;
private boolean attempted;
private long shader;
private Fragment(String label, Supplier<String> source, int requiredUboSize) {
this.label = label;
this.source = source;
this.requiredUboSize = requiredUboSize;
}
long shader() {
if (!attempted) {
attempted = true;
shader = compileFragment(source.get(), requiredUboSize, label);
}
return shader;
}
}
long compileFragment(String fragmentSource, int requiredUboSize, String label) {
Fragment fragment(String path, int requiredUboSize) {
return fragment(path, requiredUboSize, () -> ShaderLoader.getShaderSource(path));
}
Fragment fragment(String label, int requiredUboSize, Supplier<String> source) {
final Fragment fragment = new Fragment(label, source, requiredUboSize);
fragments.add(fragment);
return fragment;
}
long getNearestSampler() {
if (samplerAttempted) return nearestSampler;
samplerAttempted = true;
try (MemoryStack stack = MemoryStack.stackPush()) {
final SDL_GPUSamplerCreateInfo ci = SDL_GPUSamplerCreateInfo.calloc(stack)
.min_filter(SDL_GPU_FILTER_NEAREST)
.mag_filter(SDL_GPU_FILTER_NEAREST)
.mipmap_mode(SDL_GPU_SAMPLERMIPMAPMODE_NEAREST)
.address_mode_u(SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE)
.address_mode_v(SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE)
.address_mode_w(SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE);
nearestSampler = SDL_CreateGPUSampler(device.getDevice(), ci);
if (nearestSampler == 0L) LOG.error("FullscreenPass: sampler creation failed: {}", SDLError.SDL_GetError());
}
return nearestSampler;
}
private long compileFragment(String fragmentSource, int requiredUboSize, String label) {
if (vertexFailed) return 0;
if (vertexShaderId == 0) {
vertexShaderId = shaderManager.createShader(GL20.GL_VERTEX_SHADER);
shaderManager.shaderSource(vertexShaderId, ShaderLoader.getShaderSource(VERTEX_SOURCE));
@@ -47,38 +106,41 @@ final class FullscreenPass {
}
final int fs = shaderManager.createShader(GL20.GL_FRAGMENT_SHADER);
shaderManager.shaderSource(fs, fragmentSource);
shaderManager.compileShader(fs);
final int prog = shaderManager.createProgram();
shaderManager.attachShader(prog, vertexShaderId);
shaderManager.attachShader(prog, fs);
shaderManager.linkProgram(prog);
try {
shaderManager.shaderSource(fs, fragmentSource);
shaderManager.compileShader(fs);
shaderManager.attachShader(prog, vertexShaderId);
shaderManager.attachShader(prog, fs);
shaderManager.linkProgram(prog);
final ShaderManager.ProgramObject po = shaderManager.getProgram(prog);
if (po == null || !po.linked) {
LOG.error("{}: program failed to link: {}", label, po == null ? "no program object" : po.infoLog);
return 0;
}
if (requiredUboSize >= 0 && po.fragmentUboSize != requiredUboSize) {
LOG.error("{}: fragment UBO size {} != {}", label, po.fragmentUboSize, requiredUboSize);
return 0;
}
if (sdlVertexShader == 0) {
sdlVertexShader = shaderManager.createSDLShader(po.vertexSpirv, SDL_GPU_SHADERSTAGE_VERTEX, po.vertexResources.numSamplers(), po.vertexResources.numUBOs(), po.vertexResources.numStorageBuffers(), po.vertexResources.numStorageTextures());
if (sdlVertexShader == 0) {
LOG.error("{}: vertex SDL shader creation failed", label);
final ShaderManager.ProgramObject po = shaderManager.getProgram(prog);
if (po == null || !po.linked) {
vertexFailed = !shaderManager.isCompiled(vertexShaderId);
LOG.error("{}: program failed to link: {}", label, po == null ? "no program object" : po.infoLog);
return 0;
}
if (requiredUboSize >= 0 && po.fragmentUboSize != requiredUboSize) {
LOG.error("{}: fragment UBO size {} != {}", label, po.fragmentUboSize, requiredUboSize);
return 0;
}
}
final long fragmentShader = shaderManager.createSDLShader(po.fragmentSpirv, SDL_GPU_SHADERSTAGE_FRAGMENT, po.fragmentResources.numSamplers(), po.fragmentResources.numUBOs(), po.fragmentResources.numStorageBuffers(), po.fragmentResources.numStorageTextures());
if (fragmentShader == 0) {
LOG.error("{}: fragment SDL shader creation failed", label);
return 0;
if (sdlVertexShader == 0) {
sdlVertexShader = shaderManager.createSDLShader(po.vertexSpirv, SDL_GPU_SHADERSTAGE_VERTEX, po.vertexResources.numSamplers(), po.vertexResources.numUBOs(), po.vertexResources.numStorageBuffers(), po.vertexResources.numStorageTextures());
if (sdlVertexShader == 0) {
vertexFailed = true;
LOG.error("{}: vertex SDL shader creation failed", label);
return 0;
}
}
final long fragmentShader = shaderManager.createSDLShader(po.fragmentSpirv, SDL_GPU_SHADERSTAGE_FRAGMENT, po.fragmentResources.numSamplers(), po.fragmentResources.numUBOs(), po.fragmentResources.numStorageBuffers(), po.fragmentResources.numStorageTextures());
if (fragmentShader == 0) LOG.error("{}: fragment SDL shader creation failed", label);
return fragmentShader;
} finally {
shaderManager.releaseProgramFinal(prog);
shaderManager.deleteShader(fs);
}
return fragmentShader;
}
long createPipeline(PipelineStore pipelineStore, MemoryStack stack, long fragmentShader, SDL_GPUColorTargetDescription.Buffer colorDesc, int numColorTargets, SDL_GPUDepthStencilState depthStencil, int depthFormat, String label) {
@@ -144,8 +206,24 @@ final class FullscreenPass {
st.lastAppliedVboBindCb = 0;
}
void shutdown() {
public void shutdown() {
final long dev = device.getDevice();
if (dev != 0L) {
for (Fragment fragment : fragments) {
if (fragment.shader != 0L) SDL_ReleaseGPUShader(dev, fragment.shader);
}
if (sdlVertexShader != 0L) SDL_ReleaseGPUShader(dev, sdlVertexShader);
if (nearestSampler != 0L) SDL_ReleaseGPUSampler(dev, nearestSampler);
}
for (Fragment fragment : fragments) {
fragment.shader = 0;
fragment.attempted = false;
}
if (vertexShaderId != 0) shaderManager.deleteShader(vertexShaderId);
vertexShaderId = 0;
sdlVertexShader = 0;
nearestSampler = 0;
samplerAttempted = false;
vertexFailed = false;
}
}
@@ -75,7 +75,7 @@ public final class LogicOpEmulator {
cp = frameManager.ensureCopyPass(FrameManager.PASS_END_LOGIC_OP);
resourceManager.flushBatchedUploads(cp);
}
TextureOps.copyTexture(cp, tex, targetLevel(fbo, loc), 0, 0, scratch, 0, 0, 0, w, h);
TextureOps.copyTexture(resourceManager, cp, tex, targetLevel(fbo, loc), 0, 0, scratch, 0, 0, 0, w, h);
}
}
@@ -0,0 +1,86 @@
package com.gtnewhorizons.angelica.sdlgpu.pipeline;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager.FrameState;
import org.lwjgl.sdl.SDL_GPUColorTargetDescription;
import org.lwjgl.sdl.SDL_GPUColorTargetInfo;
import org.lwjgl.sdl.SDL_GPUDepthStencilState;
import org.lwjgl.sdl.SDL_GPUTextureSamplerBinding;
import org.lwjgl.system.MemoryStack;
import java.nio.ByteBuffer;
import java.util.function.LongSupplier;
import static org.lwjgl.sdl.SDLGPU.*;
import static org.lwjgl.system.MemoryStack.stackPush;
/**
* Writes a texture SDL cannot give storage-read usage (depth, sRGB) into one it can.
*/
public final class MovedSamplerConversion {
private static final String FRAGMENT_SOURCE = "angelica:sdlgpu/moved_sampler_convert.fsh";
private static final long KEY_SEED = 0x6A09E667F3BCC908L;
private final FrameManager frameManager;
private final PipelineStore pipelineStore;
private final FullscreenPass fullscreen;
private final FullscreenPass.Fragment fragment;
private final LongSupplier builder = this::createPipeline;
private int buildFormat;
public MovedSamplerConversion(FrameManager frameManager, PipelineStore pipelineStore, FullscreenPass fullscreen) {
this.frameManager = frameManager;
this.pipelineStore = pipelineStore;
this.fullscreen = fullscreen;
fragment = fullscreen.fragment(FRAGMENT_SOURCE, 16);
}
public boolean convert(ContextState st, long source, long target, int targetFormat, int width, int height, int levels) {
final FrameState f = frameManager.frame();
if (!f.frameActive) return false;
final long pipeline = pipelineFor(targetFormat);
final long sampler = fullscreen.getNearestSampler();
if (pipeline == 0L || sampler == 0L) return false;
try (MemoryStack stack = stackPush()) {
final SDL_GPUColorTargetInfo.Buffer color = SDL_GPUColorTargetInfo.calloc(1, stack);
final SDL_GPUTextureSamplerBinding.Buffer binding = SDL_GPUTextureSamplerBinding.calloc(1, stack);
binding.get(0).texture(source).sampler(sampler);
final ByteBuffer level = stack.calloc(16);
for (int l = 0; l < Math.max(1, levels); l++) {
final int w = Math.max(1, width >> l);
final int h = Math.max(1, height >> l);
color.get(0).texture(target).mip_level(l).load_op(SDL_GPU_LOADOP_DONT_CARE).store_op(SDL_GPU_STOREOP_STORE);
final long rp = frameManager.beginRenderPass(color, null);
SDL_BindGPUGraphicsPipeline(rp, pipeline);
FullscreenPass.setViewport(rp, st, w, h, 0f);
FullscreenPass.setScissor(rp, st, 0, 0, w, h);
SDL_BindGPUFragmentSamplers(rp, 0, binding);
level.putInt(0, l);
SDL_PushGPUFragmentUniformData(frameManager.getCommandBuffer(f), 0, level);
SDL_DrawGPUPrimitives(rp, 3, 1, 0, 0);
frameManager.endRenderPassIfActive(f, FrameManager.PASS_END_COPY);
}
}
FullscreenPass.markStateClobbered(st);
st.lastPushedProgramFs = -1;
return true;
}
private long pipelineFor(int targetFormat) {
if (fragment.shader() == 0L) return 0L;
buildFormat = targetFormat;
return pipelineStore.getOrBuild(Hashing.fmix64(KEY_SEED, targetFormat), builder);
}
private long createPipeline() {
try (MemoryStack stack = stackPush()) {
final SDL_GPUColorTargetDescription.Buffer colorDesc = SDL_GPUColorTargetDescription.calloc(1, stack);
colorDesc.get(0).format(buildFormat).blend_state().enable_blend(false);
final SDL_GPUDepthStencilState depthStencil = SDL_GPUDepthStencilState.calloc(stack).compare_op(SDL_GPU_COMPAREOP_ALWAYS);
return fullscreen.createPipeline(pipelineStore, stack, fragment.shader(), colorDesc, 1, depthStencil, 0, "MovedSamplerConversion");
}
}
}
@@ -23,6 +23,7 @@ import com.gtnewhorizons.angelica.sdlgpu.resource.PersistentMapping;
import com.gtnewhorizons.angelica.sdlgpu.resource.PixelOps;
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import com.gtnewhorizons.angelica.sdlgpu.sampler.SamplerBinder;
import com.gtnewhorizons.angelica.sdlgpu.sampler.MovedSamplerCopies;
import com.gtnewhorizons.angelica.sdlgpu.sampler.StorageBufferBinder;
import com.gtnewhorizons.angelica.sdlgpu.sampler.StorageTextureBinder;
import com.gtnewhorizons.angelica.sdlgpu.shader.MatrixMarshal;
@@ -57,6 +58,7 @@ public final class PipelineApplier {
private final PersistentBufferSync persistentSync;
private final SamplerBinder samplerBinder;
private final StorageTextureBinder storageTextureBinder;
private final MovedSamplerCopies movedSamplerCopies;
private final StorageBufferBinder storageBufferBinder;
private final LogicOpEmulator logicOpEmulator;
@@ -76,7 +78,7 @@ public final class PipelineApplier {
private final int[] scissorRect = new int[4];
public PipelineApplier(FrameManager frameManager, ResourceManager resourceManager, ShaderManager shaderManager, PipelineStore pipelineStore, FBOClearTracker fboClearTracker, PersistentBufferSync persistentSync, SamplerBinder samplerBinder, StorageTextureBinder storageTextureBinder, StorageBufferBinder storageBufferBinder) {
public PipelineApplier(FrameManager frameManager, ResourceManager resourceManager, ShaderManager shaderManager, PipelineStore pipelineStore, FBOClearTracker fboClearTracker, PersistentBufferSync persistentSync, SamplerBinder samplerBinder, StorageTextureBinder storageTextureBinder, StorageBufferBinder storageBufferBinder, MovedSamplerCopies movedSamplerCopies) {
this.frameManager = frameManager;
this.resourceManager = resourceManager;
this.shaderManager = shaderManager;
@@ -85,6 +87,7 @@ public final class PipelineApplier {
this.persistentSync = persistentSync;
this.samplerBinder = samplerBinder;
this.storageTextureBinder = storageTextureBinder;
this.movedSamplerCopies = movedSamplerCopies;
this.storageBufferBinder = storageBufferBinder;
this.logicOpEmulator = new LogicOpEmulator(frameManager, resourceManager, shaderManager, this);
this.instancedConstants = new InstancedConstantBuffers(resourceManager);
@@ -122,7 +125,7 @@ public final class PipelineApplier {
public void ensureDrawRenderPass(ContextState st, FrameState f) {
logicOpEmulator.beforeDraw(st, f);
ensureRenderPass(st, f);
ensureRenderPass(st, f, true);
}
public void ensureRenderPass(ContextState st) {
@@ -130,6 +133,10 @@ public final class PipelineApplier {
}
public void ensureRenderPass(ContextState st, FrameState f) {
ensureRenderPass(st, f, false);
}
private void ensureRenderPass(ContextState st, FrameState f, boolean draw) {
if (!f.frameActive) return;
fboClearTracker.flushPendingClearsForBoundSamplers(st);
@@ -151,6 +158,7 @@ public final class PipelineApplier {
}
if (st.boundFboId == 0) {
if (draw) movedSamplerCopies.prepare(st, f, FrameManager.fbo0PassStaysOpen(st, f));
if (frameManager.ensureFbo0RenderPass(f, st)) {
st.pipeline.setDepthTargetFormat(resourceManager.getSwapchainDepthStencilFormat());
}
@@ -167,7 +175,10 @@ public final class PipelineApplier {
if (!fbo.hasAnyColor && fbo.depthTexture == 0) return;
final long primaryTarget = fbo.primaryTarget;
if (f.renderPass != 0 && f.activeLayoutHash == fbo.structuralLayoutHash && f.currentColorTarget == primaryTarget && f.currentDepthTarget == fbo.depthTexture && !FBOClearTracker.fboHasPendingClear(st, fbo)) {
final boolean keepsOpenPass = f.renderPass != 0 && f.activeLayoutHash == fbo.structuralLayoutHash && f.currentColorTarget == primaryTarget && f.currentDepthTarget == fbo.depthTexture && !FBOClearTracker.fboHasPendingClear(st, fbo);
if (draw) movedSamplerCopies.prepare(st, f, keepsOpenPass);
// Preparing a stale sampler copy can end the pass even when the attachments still match.
if (keepsOpenPass && f.renderPass != 0) {
return;
}
@@ -106,6 +106,13 @@ public final class FBOClearTracker {
final int vertSamplers = Math.min(prog.vertexResources.numSamplers(), ContextState.MAX_SAMPLERS);
collectSamplerFlush(st, SamplerLookup.resolvedUnits(prog, false), vertSamplers, colorHandles, colorGlIds, depthHandles, depthGlIds);
if (prog.hasReadMovedSamplers) {
final int[] fragMoved = SamplerLookup.movedSamplerUnits(prog, true);
collectSamplerFlush(st, fragMoved, fragMoved.length, colorHandles, colorGlIds, depthHandles, depthGlIds);
final int[] vertMoved = SamplerLookup.movedSamplerUnits(prog, false);
collectSamplerFlush(st, vertMoved, vertMoved.length, colorHandles, colorGlIds, depthHandles, depthGlIds);
}
if (!colorHandles.isEmpty() || !depthHandles.isEmpty()) {
materializeFlush(st, colorHandles, colorGlIds, depthHandles, depthGlIds);
}
@@ -114,9 +121,7 @@ public final class FBOClearTracker {
private void collectSamplerFlush(ContextState st, int[] samplerUnits, int samplerCount, LongArrayList colorHandles, IntArrayList colorGlIds, LongArrayList depthHandles, IntArrayList depthGlIds) {
for (int i = 0; i < samplerCount; i++) {
final int glUnit = samplerUnits[i];
if (glUnit < 0 || glUnit >= st.boundTextures.length) continue;
final int glTexId = st.boundTextures[glUnit];
final int glTexId = st.boundTexture(samplerUnits[i]);
if (glTexId == 0) continue;
final long handle = resourceManager.getTextureHandle(glTexId);
if (handle == 0) continue;
@@ -222,7 +222,7 @@ public final class ResourceManager {
}
private static int defaultUsage(int sdlFormat) {
if (isDepthFormat(sdlFormat)) {
if (PixelOps.isDepthFormat(sdlFormat)) {
return SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
}
if (isIntegerFormat(sdlFormat)) {
@@ -371,6 +371,7 @@ public final class ResourceManager {
releaseTextureDeferred(handle);
return;
}
notifyTextureReleased(handle);
releaseTextureHandle(handle);
}
@@ -393,6 +394,46 @@ public final class ResourceManager {
}
private final LongOpenHashSet definedContentTextures = new LongOpenHashSet();
private volatile Long2ObjectOpenHashMap<AtomicLong> textureWriteVersions = new Long2ObjectOpenHashMap<>();
private final AtomicLong textureWriteGeneration = new AtomicLong();
public void trackTextureWrites(long handle) {
if (handle != 0) setWriteTracking(handle, true);
}
public void stopTrackingTextureWrites(long handle) {
if (textureWriteVersions.containsKey(handle)) setWriteTracking(handle, false);
}
private void setWriteTracking(long handle, boolean track) {
wLock.lock();
try {
if (textureWriteVersions.containsKey(handle) == track) return;
final Long2ObjectOpenHashMap<AtomicLong> versions = new Long2ObjectOpenHashMap<>(textureWriteVersions);
if (track) versions.put(handle, new AtomicLong());
else versions.remove(handle);
textureWriteVersions = versions;
textureWriteGeneration.incrementAndGet();
} finally {
wLock.unlock();
}
}
public long textureWriteGeneration() {
return textureWriteGeneration.get();
}
public long textureWriteVersion(long handle) {
final AtomicLong version = textureWriteVersions.get(handle);
return version == null ? 0 : version.get();
}
public void markTextureWritten(long handle) {
final AtomicLong version = textureWriteVersions.get(handle);
if (version == null) return;
version.incrementAndGet();
textureWriteGeneration.incrementAndGet();
}
public boolean isTextureContentDefined(long handle) {
if (handle == 0) return false;
@@ -448,6 +489,7 @@ public final class ResourceManager {
if (handle == 0) return;
dropBatchedUploadsTargeting(handle);
markTextureContentUndefined(handle);
stopTrackingTextureWrites(handle);
final boolean live;
wLock.lock();
try {
@@ -605,7 +647,7 @@ public final class ResourceManager {
return true;
}
private boolean copyAllMips(long srcTex, long dstTex, int glTarget, int width, int height, int depth, int levels) {
public boolean copyAllMips(long srcTex, long dstTex, int glTarget, int width, int height, int depth, int levels) {
final long cp = frameManager.ensureCopyPass();
if (cp == 0) return false;
flushBatchedUploads(cp);
@@ -625,6 +667,7 @@ public final class ResourceManager {
}
}
}
markTextureWritten(dstTex);
return true;
}
@@ -838,6 +881,18 @@ public final class ResourceManager {
} finally {
wLock.unlock();
}
notifyTextureReleased(handle);
}
private LongConsumer textureReleaseListener;
public void setTextureReleaseListener(LongConsumer listener) {
textureReleaseListener = listener;
}
private void notifyTextureReleased(long handle) {
final LongConsumer listener = textureReleaseListener;
if (listener != null) listener.accept(handle);
}
private static volatile boolean singleThreadedReads;
@@ -1781,6 +1836,7 @@ public final class ResourceManager {
}
}
markTextureContentDefined(gpuTexture);
markTextureWritten(gpuTexture);
frameManager.recordUploadCommands(sliceBytes * d, d);
return true;
} finally {
@@ -1834,6 +1890,8 @@ public final class ResourceManager {
f.pendingTexLevels.add(level);
f.pendingTexOffsets.add(f.batchOffset);
f.batchOffset += size;
// Mark queued uploads now: sampler copies test freshness before the copy pass flushes them.
markTextureWritten(gpuTexture);
}
public void flushBatchedUploads(long copyPass) {
@@ -1937,6 +1995,7 @@ public final class ResourceManager {
SDL_UploadToGPUTexture(copyPass, src, dst, false);
}
markTextureContentDefined(tex);
markTextureWritten(tex);
frameManager.recordUploadCommands(size, 1);
} finally {
returnTransferBufferThreadSafe(xfer, size);
@@ -1966,6 +2025,7 @@ public final class ResourceManager {
st.frameHighestEnqueuedSeq = seq;
lastTextureUploadSeq = seq;
markTextureContentDefined(tex);
markTextureWritten(tex);
tt.enqueue(TransferThread.TextureRegionUpload.acquire(xfer, tex, x, y, w, h, level, size, seq));
tt.wake();
}
@@ -2142,51 +2202,67 @@ public final class ResourceManager {
}
public long getOrCreateFallbackTexture() {
if (fallbackTexture != 0) return fallbackTexture;
if (fallbackTexture == 0) {
fallbackTexture = createFallback(SDL_GPU_TEXTURETYPE_2D, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
SDL_GPU_TEXTUREUSAGE_SAMPLER, new byte[] { 0, 0, 0, (byte) 0xFF });
}
return fallbackTexture;
}
public long createStorageReadTexture(TextureMeta meta, int sdlFormat, int extraUsage) {
final int type = mapTextureType(meta.glTarget());
final int usage = SDL_GPU_TEXTUREUSAGE_GRAPHICS_STORAGE_READ | extraUsage;
if (!SDL_GPUTextureSupportsFormat(device.getDevice(), sdlFormat, type, usage)) return 0;
try (var stack = stackPush()) {
final SDL_GPUTextureCreateInfo ci = SDL_GPUTextureCreateInfo.calloc(stack)
.type(SDL_GPU_TEXTURETYPE_2D)
.format(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM)
.usage(SDL_GPU_TEXTUREUSAGE_SAMPLER)
.width(1)
.height(1)
.layer_count_or_depth(1)
.num_levels(1);
fallbackTexture = SDL_CreateGPUTexture(device.getDevice(), ci);
if (fallbackTexture == 0) {
LOG.error("Failed to create fallback texture: {}", SDLError.SDL_GetError());
.type(type)
.format(sdlFormat)
.usage(usage)
.width(meta.width()).height(meta.height()).layer_count_or_depth(meta.depth()).num_levels(meta.levels());
final long handle = SDL_CreateGPUTexture(device.getDevice(), ci);
if (handle == 0) {
LOG.error("Failed to create storage-read texture {}x{} sdlFmt=0x{}: {}", meta.width(), meta.height(), Integer.toHexString(sdlFormat), SDLError.SDL_GetError());
return 0;
}
trackTextureHandle(fallbackTexture);
trackTextureHandle(handle);
return handle;
}
// GL 4.4 sec 11.1.3.5: sampling an incomplete texture yields (0,0,0,1).
final byte[] texel = { 0, 0, 0, (byte) 0xFF };
seedOneTexel(fallbackTexture, texel);
return fallbackTexture;
}
private long fallbackStorageTexture2D;
public long getOrCreateFallbackStorageTexture2D() {
if (fallbackStorageTexture2D == 0) {
fallbackStorageTexture2D = createFallback(SDL_GPU_TEXTURETYPE_2D, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
SDL_GPU_TEXTUREUSAGE_GRAPHICS_STORAGE_READ, new byte[] { 0, 0, 0, (byte) 0xFF });
}
return fallbackStorageTexture2D;
}
private long fallbackStorageTexture3D;
public long getOrCreateFallbackStorageTexture3D() {
if (fallbackStorageTexture3D != 0) return fallbackStorageTexture3D;
if (fallbackStorageTexture3D == 0) {
fallbackStorageTexture3D = createFallback(SDL_GPU_TEXTURETYPE_3D, SDL_GPU_TEXTUREFORMAT_R8_UINT,
SDL_GPU_TEXTUREUSAGE_GRAPHICS_STORAGE_READ | SDL_GPU_TEXTUREUSAGE_COMPUTE_STORAGE_READ
| SDL_GPU_TEXTUREUSAGE_COMPUTE_STORAGE_WRITE, new byte[1]);
}
return fallbackStorageTexture3D;
}
private long createFallback(int type, int format, int usage, byte[] texel) {
final long texture;
try (var stack = stackPush()) {
final SDL_GPUTextureCreateInfo ci = SDL_GPUTextureCreateInfo.calloc(stack)
.type(SDL_GPU_TEXTURETYPE_3D)
.format(SDL_GPU_TEXTUREFORMAT_R8_UINT)
.usage(SDL_GPU_TEXTUREUSAGE_GRAPHICS_STORAGE_READ
| SDL_GPU_TEXTUREUSAGE_COMPUTE_STORAGE_READ
| SDL_GPU_TEXTUREUSAGE_COMPUTE_STORAGE_WRITE)
.type(type).format(format).usage(usage)
.width(1).height(1).layer_count_or_depth(1).num_levels(1);
fallbackStorageTexture3D = SDL_CreateGPUTexture(device.getDevice(), ci);
if (fallbackStorageTexture3D == 0) {
LOG.error("Failed to create fallback storage 3D texture: {}", SDLError.SDL_GetError());
return 0;
}
trackTextureHandle(fallbackStorageTexture3D);
texture = SDL_CreateGPUTexture(device.getDevice(), ci);
}
seedOneTexel(fallbackStorageTexture3D, new byte[1]);
return fallbackStorageTexture3D;
if (texture == 0) {
LOG.error("Failed to create fallback texture (type={} format={} usage={}): {}", type, format, usage, SDLError.SDL_GetError());
return 0;
}
trackTextureHandle(texture);
seedOneTexel(texture, texel);
return texture;
}
private long fallbackStorageBuffer;
@@ -2279,6 +2355,7 @@ public final class ResourceManager {
.w(1).h(1).d(1);
SDL_UploadToGPUTexture(copyPass, src, dst, false);
SDL_EndGPUCopyPass(copyPass);
markTextureWritten(texture);
}
Submits.submit(cb);
}
@@ -2558,14 +2635,6 @@ public final class ResourceManager {
};
}
private static boolean isDepthFormat(int sdlFormat) {
return sdlFormat == SDL_GPU_TEXTUREFORMAT_D16_UNORM
|| sdlFormat == SDL_GPU_TEXTUREFORMAT_D24_UNORM
|| sdlFormat == SDL_GPU_TEXTUREFORMAT_D32_FLOAT
|| sdlFormat == SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT
|| sdlFormat == SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT;
}
static boolean isIntegerFormat(int sdlFormat) {
return sdlFormat == SDL_GPU_TEXTUREFORMAT_R8_UINT
|| sdlFormat == SDL_GPU_TEXTUREFORMAT_R8G8_UINT
@@ -2649,6 +2718,10 @@ public final class ResourceManager {
releaseTextureHandle(fallbackStorageTexture3D);
fallbackStorageTexture3D = 0;
}
if (fallbackStorageTexture2D != 0) {
releaseTextureHandle(fallbackStorageTexture2D);
fallbackStorageTexture2D = 0;
}
synchronized (computeStandInTextures) {
for (long handle : computeStandInTextures.values()) releaseTextureHandle(handle);
computeStandInTextures.clear();
@@ -198,15 +198,16 @@ public final class TextureOps {
final long cp = frameManager.ensureCopyPass();
if (cp == 0) return;
resourceManager.flushBatchedUploads(cp);
copyTexture(cp, srcTex, srcLevel, srcX, srcY, dstTex, dstLevel, dstX, dstY, w, h);
copyTexture(resourceManager, cp, srcTex, srcLevel, srcX, srcY, dstTex, dstLevel, dstX, dstY, w, h);
}
public static void copyTexture(long cp, long srcTex, int srcLevel, int srcX, int srcY, long dstTex, int dstLevel, int dstX, int dstY, int w, int h) {
public static void copyTexture(ResourceManager resourceManager, long cp, long srcTex, int srcLevel, int srcX, int srcY, long dstTex, int dstLevel, int dstX, int dstY, int w, int h) {
if (cp == 0) return;
try (var stack = MemoryStack.stackPush()) {
final var src = SDL_GPUTextureLocation.calloc(stack).texture(srcTex).mip_level(srcLevel).x(srcX).y(srcY);
final var dst = SDL_GPUTextureLocation.calloc(stack).texture(dstTex).mip_level(dstLevel).x(dstX).y(dstY);
SDL_CopyGPUTextureToTexture(cp, src, dst, w, h, 1, false);
resourceManager.markTextureWritten(dstTex);
}
}
@@ -228,6 +229,7 @@ public final class TextureOps {
info.flip_mode(flipMode);
frameManager.noteBlit();
SDL_BlitGPUTexture(cb, info);
resourceManager.markTextureWritten(dstTex);
}
}
@@ -262,6 +264,7 @@ public final class TextureOps {
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(glId);
if (meta == null || meta.levels() <= 1) continue;
SDL_GenerateMipmapsForGPUTexture(uploadCb, handle);
resourceManager.markTextureWritten(handle);
}
if (!Submits.submit(uploadCb)) {
@@ -0,0 +1,212 @@
package com.gtnewhorizons.angelica.sdlgpu.sampler;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager.FrameState;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.MovedSamplerConversion;
import com.gtnewhorizons.angelica.sdlgpu.resource.PixelOps;
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.GL11;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R32_FLOAT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREUSAGE_SAMPLER;
/**
* SDL reads texture slots in layout GENERAL, but a sampleable texture sits in SHADER_READ_ONLY_OPTIMAL
* and SDL won't give one texture both usages. So a moved sampler reads a GRAPHICS_STORAGE_READ copy of its texture,
* refreshed after the source is written. Formats Vulkan can't storage-read are converted to what texelFetch returns:
* depth to R32F, anything else (sRGB) to RGBA16F. D3D12 (one resource state for both usages) and Metal (no layouts)
* bind the texture directly.
*/
public final class MovedSamplerCopies {
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
private static final int UNUSED_FRAMES_BEFORE_RELEASE = 8;
private static final class Copy {
final long texture;
final ResourceManager.TextureMeta meta;
final int convertedFormat;
FrameState readFrame;
long readPass = -1;
long writeVersion = -1;
long pendingWriteVersion;
long lastUsedFrame;
Copy(long texture, ResourceManager.TextureMeta meta, int convertedFormat) {
this.texture = texture;
this.meta = meta;
this.convertedFormat = convertedFormat;
}
}
private final Device device;
private final FrameManager frameManager;
private final ResourceManager resourceManager;
private final MovedSamplerConversion conversion;
private final Long2ObjectOpenHashMap<Copy> bySource = new Long2ObjectOpenHashMap<>();
private final LongArrayList stale = new LongArrayList();
private final IntOpenHashSet unsupportedWarned = new IntOpenHashSet();
private long frameNumber;
public MovedSamplerCopies(Device device, FrameManager frameManager, ResourceManager resourceManager, MovedSamplerConversion conversion) {
this.device = device;
this.frameManager = frameManager;
this.resourceManager = resourceManager;
this.conversion = conversion;
}
public void prepare(ContextState st, FrameState f, boolean keepsOpenPass) {
if (!device.isVulkan()) return;
final ShaderManager.ProgramObject prog = st.boundProgramObj;
if (prog == null || !prog.linked || !prog.hasReadMovedSamplers) return;
final int[] vertexUnits = SamplerLookup.movedSamplerUnits(prog, false);
final int[] fragmentUnits = SamplerLookup.movedSamplerUnits(prog, true);
final long writeGen = resourceManager.textureWriteGeneration();
if (keepsOpenPass && st.lastPreparedMovedSamplerPass == f.renderPassGeneration
&& st.lastPreparedMovedSamplerProgram == prog
&& st.lastPreparedMovedSamplerUnitsGen == prog.samplerUnitsGeneration
&& st.lastPreparedMovedSamplerWriteGen == writeGen
&& bindingsMatch(st, vertexUnits, st.lastPreparedVertexMovedSamplerTextures)
&& bindingsMatch(st, fragmentUnits, st.lastPreparedFragmentMovedSamplerTextures)) return;
synchronized (this) {
if (!refresh(st, f, vertexUnits, fragmentUnits, keepsOpenPass)) return;
}
st.lastPreparedMovedSamplerProgram = prog;
snapshotBindings(st, vertexUnits, st.lastPreparedVertexMovedSamplerTextures);
snapshotBindings(st, fragmentUnits, st.lastPreparedFragmentMovedSamplerTextures);
st.lastPreparedMovedSamplerUnitsGen = prog.samplerUnitsGeneration;
st.lastPreparedMovedSamplerWriteGen = writeGen;
st.lastPreparedMovedSamplerPass = f.renderPassGeneration;
}
private static boolean bindingsMatch(ContextState st, int[] units, int[] textures) {
for (int i = 0; i < units.length; i++) {
if (textures[i] != st.boundTexture(units[i])) return false;
}
return true;
}
private static void snapshotBindings(ContextState st, int[] units, int[] textures) {
for (int i = 0; i < units.length; i++) textures[i] = st.boundTexture(units[i]);
}
private boolean refresh(ContextState st, FrameState f, int[] vertexUnits, int[] fragmentUnits, boolean keepsOpenPass) {
stale.clear();
collectStale(st, f, vertexUnits, keepsOpenPass);
collectStale(st, f, fragmentUnits, keepsOpenPass);
for (int i = 0; i < stale.size(); i++) {
final long source = stale.getLong(i);
final Copy copy = bySource.get(source);
if (copy == null) continue;
final ResourceManager.TextureMeta m = copy.meta;
final boolean refreshed = copy.convertedFormat == 0
? resourceManager.copyAllMips(source, copy.texture, m.glTarget(), m.width(), m.height(), m.depth(), m.levels())
: conversion.convert(st, source, copy.texture, copy.convertedFormat, m.width(), m.height(), m.levels());
if (!refreshed) return false;
copy.writeVersion = copy.pendingWriteVersion;
}
return true;
}
private void collectStale(ContextState st, FrameState f, int[] units, boolean keepsOpenPass) {
for (int unit : units) {
final int glTexId = st.boundTexture(unit);
if (glTexId == 0) continue;
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(glTexId);
if (!readThroughCopy(meta)) continue;
final long source = resourceManager.getTextureHandle(glTexId);
if (source == 0 || stale.contains(source)) continue;
final Copy copy = copyFor(source, meta);
if (copy == null) continue;
copy.lastUsedFrame = frameNumber;
final boolean readByOpenPass = keepsOpenPass && f.renderPass != 0 && copy.readFrame == f && copy.readPass == f.renderPassGeneration;
if (!readByOpenPass) {
final long writeVersion = resourceManager.textureWriteVersion(source);
if (copy.writeVersion != writeVersion) {
copy.pendingWriteVersion = writeVersion;
stale.add(source);
}
}
}
}
private static boolean readThroughCopy(ResourceManager.TextureMeta meta) {
return meta != null && (meta.usage() & SDL_GPU_TEXTUREUSAGE_SAMPLER) != 0;
}
private Copy copyFor(long source, ResourceManager.TextureMeta meta) {
Copy copy = bySource.get(source);
if (copy != null && copy.meta.equals(meta)) return copy;
if (copy != null) resourceManager.releaseTextureDeferred(copy.texture);
int convertedFormat = 0;
long texture = resourceManager.createStorageReadTexture(meta, meta.sdlFormat(), 0);
if (texture == 0 && meta.glTarget() == GL11.GL_TEXTURE_2D) {
convertedFormat = PixelOps.isDepthFormat(meta.sdlFormat()) ? SDL_GPU_TEXTUREFORMAT_R32_FLOAT : SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT;
texture = resourceManager.createStorageReadTexture(meta, convertedFormat, SDL_GPU_TEXTUREUSAGE_COLOR_TARGET);
}
if (texture == 0) {
bySource.remove(source);
resourceManager.stopTrackingTextureWrites(source);
if (unsupportedWarned.add(meta.sdlFormat())) {
LOG.error("SDL_GPU cannot make a read-only texture-slot copy of format 0x{} (GL target 0x{}); samplers moved off the 16 sampler slots read black from it",
Integer.toHexString(meta.sdlFormat()), Integer.toHexString(meta.glTarget()));
}
return null;
}
copy = new Copy(texture, meta, convertedFormat);
bySource.put(source, copy);
resourceManager.trackTextureWrites(source);
return copy;
}
public long textureFor(long source, int glTexId) {
if (!device.isVulkan()) return source;
synchronized (this) {
final Copy copy = bySource.get(source);
if (copy == null) return readThroughCopy(resourceManager.getTextureMeta(glTexId)) ? 0 : source;
if (copy.writeVersion == -1) return 0;
final FrameState f = frameManager.frame();
copy.readFrame = f;
copy.readPass = f.renderPassGeneration;
return copy.texture;
}
}
public synchronized void releaseCopyOf(long source) {
final Copy copy = bySource.remove(source);
if (copy != null) resourceManager.releaseTextureDeferred(copy.texture);
resourceManager.stopTrackingTextureWrites(source);
}
public synchronized void releaseUnused() {
frameNumber++;
if (bySource.isEmpty()) return;
for (var it = bySource.long2ObjectEntrySet().fastIterator(); it.hasNext(); ) {
final Long2ObjectMap.Entry<Copy> e = it.next();
if (frameNumber - e.getValue().lastUsedFrame > UNUSED_FRAMES_BEFORE_RELEASE) {
resourceManager.stopTrackingTextureWrites(e.getLongKey());
resourceManager.releaseTextureDeferred(e.getValue().texture);
it.remove();
}
}
}
public synchronized void releaseAll() {
for (var it = bySource.long2ObjectEntrySet().fastIterator(); it.hasNext(); ) {
final Long2ObjectMap.Entry<Copy> e = it.next();
resourceManager.stopTrackingTextureWrites(e.getLongKey());
resourceManager.releaseTextureHandle(e.getValue().texture);
}
bySource.clear();
}
}
@@ -149,7 +149,7 @@ public final class SamplerBinder {
continue;
}
final int glUnit = samplerUnits[i];
final int glTexId = (glUnit >= 0 && glUnit < st.boundTextures.length) ? st.boundTextures[glUnit] : 0;
final int glTexId = st.boundTexture(glUnit);
long texHandle = (glTexId != 0) ? resourceManager.getTextureHandle(glTexId) : 0;
if (texHandle == 0) {
texHandle = fallbackTexture;
@@ -6,19 +6,31 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap;
import java.util.List;
public final class SamplerLookup {
private static final int[] EMPTY_UNITS = new int[0];
private SamplerLookup() {}
public static int[] resolvedUnits(ShaderManager.ProgramObject prog, boolean fragment) {
if (prog.samplerUnitsDirty) {
prog.vertexSamplerUnits = resolveStage(prog.vertexSamplerNames, prog.samplerTextureUnits, prog.vertexResources.numSamplers());
prog.fragmentSamplerUnits = resolveStage(prog.fragmentSamplerNames, prog.samplerTextureUnits, prog.fragmentResources.numSamplers());
prog.samplerUnitsDirty = false;
}
refresh(prog);
return fragment ? prog.fragmentSamplerUnits : prog.vertexSamplerUnits;
}
public static int[] movedSamplerUnits(ShaderManager.ProgramObject prog, boolean fragment) {
refresh(prog);
return fragment ? prog.fragmentMovedSamplerUnits : prog.vertexMovedSamplerUnits;
}
private static void refresh(ShaderManager.ProgramObject prog) {
if (!prog.samplerUnitsDirty) return;
prog.vertexSamplerUnits = resolveStage(prog.vertexSamplerNames, prog.samplerTextureUnits, prog.vertexResources.numSamplers());
prog.fragmentSamplerUnits = resolveStage(prog.fragmentSamplerNames, prog.samplerTextureUnits, prog.fragmentResources.numSamplers());
prog.vertexMovedSamplerUnits = resolveMoved(prog.vertexImageNames, prog.vertexMovedSamplerRead, prog.samplerTextureUnits);
prog.fragmentMovedSamplerUnits = resolveMoved(prog.fragmentImageNames, prog.fragmentMovedSamplerRead, prog.samplerTextureUnits);
prog.samplerUnitsDirty = false;
prog.samplerUnitsGeneration++;
}
private static int[] resolveStage(List<String> samplerNames, Object2IntMap<String> textureUnits, int samplerCount) {
if (samplerCount <= 0) return new int[0];
if (samplerCount <= 0) return EMPTY_UNITS;
final int[] units = new int[samplerCount];
for (int i = 0; i < samplerCount; i++) {
units[i] = getSamplerTextureUnit(samplerNames, textureUnits, i);
@@ -26,17 +38,19 @@ public final class SamplerLookup {
return units;
}
private static int[] resolveMoved(List<String> imageNames, boolean[] read, Object2IntMap<String> textureUnits) {
if (read.length == 0) return EMPTY_UNITS;
final int[] units = new int[read.length];
for (int i = 0; i < units.length; i++) {
units[i] = read[i] ? getSamplerTextureUnit(imageNames, textureUnits, i) : -1;
}
return units;
}
public static int getSamplerTextureUnit(List<String> samplerNames, Object2IntMap<String> textureUnits, int bindingIndex) {
if (bindingIndex >= samplerNames.size()) return bindingIndex;
if (bindingIndex >= samplerNames.size()) return 0;
final String name = samplerNames.get(bindingIndex);
final int unit = textureUnits.getInt(name);
if (unit != -1) return unit;
return switch (name) {
case "tex", "texture", "gtexture" -> 0;
case "lightmap" -> 1;
case "normals" -> 2;
case "specular" -> 3;
default -> bindingIndex;
};
return unit != -1 ? unit : 0;
}
}
@@ -19,11 +19,13 @@ public final class StorageTextureBinder {
private final ResourceManager resourceManager;
private final ShaderManager shaderManager;
private final MovedSamplerCopies movedSamplerCopies;
private final IntOpenHashSet nullHandleWarned = new IntOpenHashSet();
public StorageTextureBinder(ResourceManager resourceManager, ShaderManager shaderManager) {
public StorageTextureBinder(ResourceManager resourceManager, ShaderManager shaderManager, MovedSamplerCopies movedSamplerCopies) {
this.resourceManager = resourceManager;
this.shaderManager = shaderManager;
this.movedSamplerCopies = movedSamplerCopies;
}
public void bindStorageTextures(long renderPass, ContextState st, long bindCb) {
@@ -63,18 +65,31 @@ public final class StorageTextureBinder {
final boolean programChanged = st.boundProgram != lastProg;
boolean anyChanged = programChanged;
final long fallback = resourceManager.getOrCreateFallbackStorageTexture3D();
final int[] movedSamplerUnits = SamplerLookup.movedSamplerUnits(prog, fragment);
final boolean[] movedSamplers = fragment ? prog.fragmentImageIsMovedSampler : prog.vertexImageIsMovedSampler;
for (int i = 0; i < count; i++) {
final String name = imgNames.get(i);
final int glUnit = prog.imageTextureUnits.getInt(name);
final int glTexId = (glUnit >= 0 && glUnit < ContextState.MAX_IMAGE_UNITS) ? st.boundStorageTextureByUnit[glUnit] : 0;
final boolean movedSampler = movedSamplers[i];
final int glUnit;
final int glTexId;
if (movedSampler) {
glUnit = movedSamplerUnits[i];
glTexId = st.boundTexture(glUnit);
} else {
glUnit = prog.imageTextureUnits.getInt(name);
glTexId = (glUnit >= 0 && glUnit < ContextState.MAX_IMAGE_UNITS) ? st.boundStorageTextureByUnit[glUnit] : 0;
}
long texHandle = (glTexId != 0) ? resourceManager.getTextureHandle(glTexId) : 0;
final boolean hasSource = texHandle != 0;
if (movedSampler && hasSource) texHandle = movedSamplerCopies.textureFor(texHandle, glTexId);
if (texHandle == 0) {
if (nullHandleWarned.add(st.boundProgram)) {
LOG.warn("StorageTextureBinder: program {} {} stage image '{}' has no bound texture (glUnit={} glTexId={}); substituting fallback", st.boundProgram, fragment ? "fragment" : "vertex", name, glUnit, glTexId);
final boolean unreadMovedSampler = movedSampler && glUnit < 0;
if (!unreadMovedSampler && nullHandleWarned.add(st.boundProgram)) {
LOG.warn("StorageTextureBinder: program {} {} stage image '{}' {} (glUnit={} glTexId={}); substituting fallback", st.boundProgram, fragment ? "fragment" : "vertex", name,
hasSource ? "has no usable texture-slot copy" : "has no bound texture", glUnit, glTexId);
}
if (fallback == 0) return;
texHandle = fallback;
texHandle = movedSampler ? resourceManager.getOrCreateFallbackStorageTexture2D() : resourceManager.getOrCreateFallbackStorageTexture3D();
if (texHandle == 0) return;
}
ptrs.put(i, texHandle);
if (texHandle != lastTex[i]) {
@@ -0,0 +1,133 @@
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 com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import org.antlr.v4.runtime.Token;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.taumc.glsl.grammar.GLSLParser;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* SDL_GPU binds 16 samplers per stage; GL drivers give 32. A sampler read only through texelFetch/textureSize never
* uses its sampler state, so it can move to one of the 8 read-only texture slots. Moved samplers get {@link #PREFIX};
* link strips it and binds them from their texture unit (through a copy on Vulkan, see {@code MovedSamplerCopies}).
*/
public final class SamplerOverflow {
private static final Logger LOGGER = LogManager.getLogger("Angelica-SDLGPU");
public static final int SAMPLER_SLOTS = ContextState.MAX_SAMPLERS;
public static final int TEXTURE_SLOTS = ContextState.MAX_IMAGE_UNITS;
public static final String PREFIX = "angelica_moved_sampler_";
private static final Set<String> SAMPLERLESS_CALLS = Set.of("texelFetch", "texelFetchOffset", "textureSize", "textureQueryLevels");
private SamplerOverflow() {}
public static boolean isMoved(String name) {
return name.startsWith(PREFIX);
}
public static String originalName(String name) {
return isMoved(name) ? name.substring(PREFIX.length()) : name;
}
private record SamplerDecl(String name, GLSLParser.Type_specifier_nonarrayContext type, boolean movable) {}
/**
* Returns true when it moved a sampler, which then needs {@link GlslVulkanPreprocess#SAMPLERLESS_EXTENSION}.
* Every declared texture, image and sampler {@link SamplerStripper} kept reaches SPIR-V and takes a slot, used or not.
*/
static boolean collectEdits(ShaderIndex index, List<TextureDeclarations.Declaration> declarations, Set<String> strippedSamplers, String debugName, List<Edit> edits) {
final List<SamplerDecl> samplers = new ArrayList<>();
int textureSlotsTaken = 0;
for (TextureDeclarations.Declaration decl : declarations) {
if (!decl.sampler()) {
textureSlotsTaken += decl.names().size();
continue;
}
final GLSLParser.Type_specifier_nonarrayContext type = decl.type();
final List<GLSLParser.Typeless_declarationContext> declarators = decl.declarators();
// Conversion and the missing-texture fallback both require a float 2D view.
final boolean movable = declarators.size() == 1 && !decl.array() && "sampler2D".equals(type.getText());
for (GLSLParser.Typeless_declarationContext td : declarators) {
if (td.IDENTIFIER() == null) continue;
final String name = td.IDENTIFIER().getText();
if (strippedSamplers.contains(name)) continue;
samplers.add(new SamplerDecl(name, type, movable && td.array_specifier() == null));
}
}
final int excess = samplers.size() - SAMPLER_SLOTS;
final int freeTextureSlots = TEXTURE_SLOTS - textureSlotsTaken;
if (excess <= 0 || freeTextureSlots <= 0) return false;
final int toMove = Math.min(excess, freeTextureSlots);
final ShaderIndex.Directives directives = index.directives();
final IntSet samplerlessArguments = samplerlessArgumentStarts(index);
final List<String> moved = new ArrayList<>();
for (SamplerDecl decl : samplers) {
if (moved.size() == toMove) break;
if (!decl.movable() || namedInDirectives(directives, decl.name()) || GlslVulkanPreprocess.renamesIdentifier(decl.name())
|| !onlyReadWithoutSampler(index, decl.name(), samplerlessArguments)) continue;
edits.add(new Edit(GlslVulkanPreprocess.startIdx(decl.type()), GlslVulkanPreprocess.stopIdx(decl.type()), "texture2D"));
for (Token tok : index.occurrences(decl.name())) {
edits.add(new Edit(tok.getStartIndex(), tok.getStartIndex() - 1, PREFIX));
}
moved.add(decl.name());
}
if (moved.isEmpty()) return false;
LOGGER.debug("'{}' uses {} samplers; moved {} that are only read with texelFetch/textureSize to texture slots: {}", debugName, samplers.size(), moved.size(), moved);
return true;
}
private static boolean onlyReadWithoutSampler(ShaderIndex index, String name, IntSet samplerlessArguments) {
int otherOccurrences = 0;
for (Token tok : index.occurrences(name)) {
if (samplerlessArguments.contains(tok.getStartIndex())) continue;
if (++otherOccurrences > 1) return false;
}
return true;
}
private static IntSet samplerlessArgumentStarts(ShaderIndex index) {
final IntSet starts = new IntOpenHashSet();
final Set<String> overloaded = userFunctionNames(index);
final ShaderIndex.Directives directives = index.directives();
for (String fn : SAMPLERLESS_CALLS) {
if (overloaded.contains(fn) || namedInDirectives(directives, fn)) continue;
for (GLSLParser.Postfix_expressionContext call : index.callsTo(fn)) {
final GLSLParser.Function_call_parametersContext params = call.function_call_parameters();
if (params == null || params.assignment_expression().isEmpty()) continue;
final GLSLParser.Assignment_expressionContext first = params.assignment_expression(0);
if (first.getStart() == first.getStop()) starts.add(first.getStart().getStartIndex());
}
}
return starts;
}
private static boolean namedInDirectives(ShaderIndex.Directives directives, String name) {
return directives.identifiers().contains(name) || directives.definedMacros().contains(name);
}
private static Set<String> userFunctionNames(ShaderIndex index) {
final Set<String> names = new HashSet<>();
for (GLSLParser.External_declarationContext external : index.root().external_declaration()) {
final GLSLParser.Function_definitionContext function = external.function_definition();
final GLSLParser.DeclarationContext declaration = function == null ? external.declaration() : null;
final GLSLParser.Function_prototypeContext prototype = function != null ? function.function_prototype() : declaration != null ? declaration.function_prototype() : null;
if (prototype != null && prototype.IDENTIFIER() != null) names.add(prototype.IDENTIFIER().getText());
}
return names;
}
}
@@ -2,86 +2,57 @@ package com.gtnewhorizons.angelica.sdlgpu.shader;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
import com.gtnewhorizons.angelica.glsm.shader.ShaderIndex;
import com.gtnewhorizons.angelica.sdlgpu.shader.TextureDeclarations.Declaration;
import org.antlr.v4.runtime.Token;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import org.taumc.glsl.grammar.GLSLParser;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class SamplerStripper {
private static final Logger LOGGER = LogManager.getLogger("Angelica-SDLGPU");
private SamplerStripper() {}
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<>();
for (GLSLParser.DeclarationContext ctx : index.globalDeclarations()) {
final SamplerDecl decl = samplerDeclaration(ctx);
if (decl != null) samplers.add(decl);
}
if (samplers.isEmpty()) return;
static Set<String> collectEdits(ShaderIndex index, List<Declaration> declarations, List<Edit> edits) {
Set<String> directiveRefs = null;
for (SamplerDecl s : samplers) {
if (!allUnreferenced(s, samplers, index)) continue;
final Set<String> removed = new HashSet<>();
for (Declaration s : declarations) {
if (!s.sampler() || !allUnreferenced(s, declarations, 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(), ""));
removed.addAll(s.names());
}
return removed;
}
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;
private static boolean insideAnySampler(List<Declaration> declarations, int pos) {
for (Declaration s : declarations) {
if (s.sampler() && pos >= s.start() && pos <= s.stop()) return true;
}
return false;
}
private static boolean allUnreferenced(SamplerDecl s, List<SamplerDecl> samplers, ShaderIndex index) {
private static boolean allUnreferenced(Declaration s, List<Declaration> declarations, ShaderIndex index) {
for (String name : s.names()) {
for (Token tok : index.occurrences(name)) {
if (!insideAny(samplers, tok.getStartIndex())) return false;
if (!insideAnySampler(declarations, tok.getStartIndex())) return false;
}
}
return true;
}
private static boolean anyIn(SamplerDecl s, Set<String> names) {
private static boolean anyIn(Declaration 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");
}
}
@@ -22,6 +22,7 @@ import com.gtnewhorizons.angelica.sdlgpu.shader.dxbc.DxbcCrossCompile;
import com.gtnewhorizons.angelica.sdlgpu.shader.msl.MslCrossCompile;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.sdl.SDL_GPUComputePipelineCreateInfo;
import org.lwjgl.sdl.SDL_GPUShaderCreateInfo;
import org.lwjgl.sdl.SDLError;
@@ -335,18 +336,22 @@ public final class ShaderManager {
if (glShaderType == GL20.GL_VERTEX_SHADER) {
ClipZRemap.collectEdits(tree, edits);
}
SamplerStripper.collectEdits(index, edits);
final List<TextureDeclarations.Declaration> textureDeclarations = TextureDeclarations.collect(index);
final Set<String> strippedSamplers = SamplerStripper.collectEdits(index, textureDeclarations, edits);
final boolean movedSamplers = isGraphics(glShaderType) && SamplerOverflow.collectEdits(index, textureDeclarations, strippedSamplers, debugName, edits);
if (glShaderType == GL20.GL_FRAGMENT_SHADER) {
DerivativeHoisting.collectEdits(index, edits);
}
final boolean needsSamplerless = meta.needsSamplerless() || movedSamplers;
if (headerLen == 0) {
if (movedSamplers && !meta.needsSamplerless()) GlslVulkanPreprocess.addSamplerlessExtension(index, edits);
return new PrewarmTransformResult(GlslVulkanPreprocess.applyEdits(source, edits), meta.boolUniforms());
}
if (edits.isEmpty() && !meta.needsSamplerless()) {
if (edits.isEmpty() && !needsSamplerless) {
return new PrewarmTransformResult(source, meta.boolUniforms());
}
String header = source.substring(0, headerLen);
if (meta.needsSamplerless()) {
if (needsSamplerless) {
final int nl = header.indexOf('\n');
header = header.substring(0, nl) + "\n" + GlslVulkanPreprocess.SAMPLERLESS_EXTENSION + header.substring(nl);
}
@@ -579,11 +584,10 @@ public final class ShaderManager {
prog.allSamplerNames.clear();
prog.allSamplerNames.addAll(prog.vertexSamplerNames);
prog.allSamplerNames.addAll(prog.fragmentSamplerNames);
prog.vertexImageNames = vs.reflection.storageImageNames();
prog.fragmentImageNames = fs.reflection.storageImageNames();
prog.allImageNames.clear();
prog.allImageNames.addAll(prog.vertexImageNames);
prog.allImageNames.addAll(prog.fragmentImageNames);
prog.hasReadMovedSamplers = false;
applyStageImages(prog, vs.reflection, true);
applyStageImages(prog, fs.reflection, false);
prog.imageTextureUnits.clear();
prog.boolUniforms.clear();
prog.boolUniforms.addAll(vs.boolUniforms);
@@ -601,6 +605,14 @@ public final class ShaderManager {
prog.infoLog = "graphics-stage RW storage image present";
return;
}
String overBudget = slotBudgetError("vertex", prog.vertexResources, prog.vertexImageIsMovedSampler);
if (overBudget == null) overBudget = slotBudgetError("fragment", prog.fragmentResources, prog.fragmentImageIsMovedSampler);
if (overBudget != null) {
LOG.error("Program {}: {}. Rejecting link.", program, overBudget);
prog.linked = false;
prog.infoLog = overBudget;
return;
}
prog.linked = true;
prog.infoLog = "";
@@ -614,11 +626,63 @@ public final class ShaderManager {
registerSamplerLocations(prog, vs.reflection, fs.reflection);
for (final String name : prog.allImageNames) ensureLocation(prog, name);
registerMovedSamplerLocations(prog, prog.vertexImageNames, prog.vertexMovedSamplerRead);
registerMovedSamplerLocations(prog, prog.fragmentImageNames, prog.fragmentMovedSamplerRead);
prog.buildUniformSlotArrays();
}
private static void applyStageImages(ProgramObject prog, StageReflection stage, boolean vertex) {
final List<String> reflected = stage.storageImageNames();
final List<String> names = new ArrayList<>(reflected.size());
final boolean[] moved = new boolean[reflected.size()];
final boolean[] read = new boolean[reflected.size()];
for (int i = 0; i < reflected.size(); i++) {
final String name = reflected.get(i);
if (SamplerOverflow.isMoved(name)) {
final String original = SamplerOverflow.originalName(name);
names.add(original);
moved[i] = true;
read[i] = !stage.unusedSamplerNames().contains(name);
prog.hasReadMovedSamplers |= read[i];
prog.allSamplerNames.add(original);
} else {
names.add(name);
prog.allImageNames.add(name);
}
}
if (vertex) {
prog.vertexImageNames = names;
prog.vertexImageIsMovedSampler = moved;
prog.vertexMovedSamplerRead = read;
} else {
prog.fragmentImageNames = names;
prog.fragmentImageIsMovedSampler = moved;
prog.fragmentMovedSamplerRead = read;
}
}
private static void registerMovedSamplerLocations(ProgramObject prog, List<String> imageNames, boolean[] read) {
for (int i = 0; i < read.length; i++) {
if (read[i]) ensureLocation(prog, imageNames.get(i));
}
}
private static @Nullable String slotBudgetError(String stage, ResourceCounts counts, boolean[] movedSamplers) {
if (counts.numSamplers() > SamplerOverflow.SAMPLER_SLOTS) {
int moved = 0;
for (boolean m : movedSamplers) if (m) moved++;
final String limit = "Too many " + stage + " shader texture samplers (" + counts.numSamplers() + ", maximum " + SamplerOverflow.SAMPLER_SLOTS + ")";
return moved == 0 ? limit : limit + ", even after moving " + moved + " that are only read through texelFetch/textureSize to texture slots";
}
if (counts.numStorageTextures() > SamplerOverflow.TEXTURE_SLOTS) {
return stage + " shader reads " + counts.numStorageTextures() + " images and textures without a sampler but SDL_GPU binds at most "
+ SamplerOverflow.TEXTURE_SLOTS + " per stage";
}
return null;
}
private void linkComputeProgram(int program, ProgramObject prog) {
final ShaderObject cs = shaderObjects.get(prog.computeShader);
if (cs == null || !cs.compiled || cs.spirv == null) {
@@ -647,9 +711,11 @@ public final class ShaderManager {
prog.allSamplerNames.clear();
prog.allSamplerNames.addAll(refl.samplerNames());
prog.vertexImageNames = List.of();
prog.fragmentImageNames = refl.storageImageNames();
prog.vertexImageIsMovedSampler = ProgramObject.NO_FLAGS;
prog.vertexMovedSamplerRead = ProgramObject.NO_FLAGS;
prog.allImageNames.clear();
prog.allImageNames.addAll(refl.storageImageNames());
prog.hasReadMovedSamplers = false;
applyStageImages(prog, refl, false);
prog.imageTextureUnits.clear();
prog.boolUniforms.clear();
prog.boolUniforms.addAll(cs.boolUniforms);
@@ -1902,12 +1968,18 @@ public final class ShaderManager {
}
final IntOpenHashSet active = new IntOpenHashSet();
forEachResource(pActiveResources.get(0), Spvc.SPVC_RESOURCE_TYPE_SAMPLED_IMAGE, stack, (i, r) -> active.add(r.id()));
forEachResource(pActiveResources.get(0), Spvc.SPVC_RESOURCE_TYPE_SEPARATE_IMAGE, stack, (i, r) -> active.add(r.id()));
final List<String> unused = new ArrayList<>();
forEachResource(resources, Spvc.SPVC_RESOURCE_TYPE_SAMPLED_IMAGE, stack, (i, r) -> {
if (active.contains(r.id())) return;
final String name = Spvc.spvc_compiler_get_name(compiler, r.id());
if (name != null && !name.isEmpty()) unused.add(name);
});
forEachResource(resources, Spvc.SPVC_RESOURCE_TYPE_SEPARATE_IMAGE, stack, (i, r) -> {
if (active.contains(r.id())) return;
final String name = Spvc.spvc_compiler_get_name(compiler, r.id());
if (name != null && SamplerOverflow.isMoved(name)) unused.add(name);
});
return unused;
}
@@ -1962,6 +2034,11 @@ public final class ShaderManager {
public List<String> vertexSamplerNames = List.of();
public List<String> fragmentImageNames = List.of();
public List<String> vertexImageNames = List.of();
public boolean[] fragmentImageIsMovedSampler = NO_FLAGS;
public boolean[] vertexImageIsMovedSampler = NO_FLAGS;
public boolean[] fragmentMovedSamplerRead = NO_FLAGS;
public boolean[] vertexMovedSamplerRead = NO_FLAGS;
public boolean hasReadMovedSamplers;
public final HashSet<String> allImageNames = new HashSet<>();
public ResourceCounts computeResources = ResourceCounts.EMPTY;
public ResourceCounts fragmentResources = ResourceCounts.EMPTY;
@@ -2009,7 +2086,10 @@ public final class ShaderManager {
public int[] vertexSamplerUnits = EMPTY_UNITS;
public int[] fragmentSamplerUnits = EMPTY_UNITS;
public int[] vertexMovedSamplerUnits = EMPTY_UNITS;
public int[] fragmentMovedSamplerUnits = EMPTY_UNITS;
public boolean samplerUnitsDirty = true;
public int samplerUnitsGeneration;
public static final byte LOCATION_KIND_PLAIN = 0;
public static final byte LOCATION_KIND_SAMPLER = 1;
@@ -2020,6 +2100,7 @@ public final class ShaderManager {
public int[] locationSamplerUnit = EMPTY_UNITS;
private static final int[] EMPTY_UNITS = new int[0];
private static final boolean[] NO_FLAGS = new boolean[0];
private static final byte[] EMPTY_KIND = new byte[0];
private static final String[] EMPTY_NAMES = new String[0];
@@ -0,0 +1,38 @@
package com.gtnewhorizons.angelica.sdlgpu.shader;
import com.gtnewhorizons.angelica.glsm.shader.ShaderIndex;
import org.taumc.glsl.grammar.GLSLParser;
import java.util.ArrayList;
import java.util.List;
final class TextureDeclarations {
private TextureDeclarations() {}
record Declaration(List<String> names, List<GLSLParser.Typeless_declarationContext> declarators,
GLSLParser.Type_specifier_nonarrayContext type, boolean sampler, boolean array, int start, int stop) {}
static List<Declaration> collect(ShaderIndex index) {
final List<Declaration> declarations = new ArrayList<>();
for (GLSLParser.DeclarationContext ctx : index.globalDeclarations()) {
final GLSLParser.Init_declarator_listContext list = ctx.init_declarator_list();
if (list == null || list.single_declaration() == null) continue;
final GLSLParser.Fully_specified_typeContext fst = list.single_declaration().fully_specified_type();
if (fst == null || fst.type_specifier() == null || !ShaderIndex.hasStorageQualifier(fst.type_qualifier(), "uniform")) continue;
final GLSLParser.Type_specifier_nonarrayContext type = fst.type_specifier().type_specifier_nonarray();
if (type == null) continue;
final String text = type.getText();
final boolean sampler = text.startsWith("sampler") || text.startsWith("isampler") || text.startsWith("usampler");
if (!sampler && !text.startsWith("image") && !text.startsWith("iimage") && !text.startsWith("uimage")
&& !text.startsWith("texture") && !text.startsWith("itexture") && !text.startsWith("utexture")) continue;
final List<GLSLParser.Typeless_declarationContext> declarators = ShaderIndex.declarators(list);
final List<String> names = new ArrayList<>(declarators.size());
for (GLSLParser.Typeless_declarationContext td : declarators) {
if (td.IDENTIFIER() != null) names.add(td.IDENTIFIER().getText());
}
if (!names.isEmpty()) declarations.add(new Declaration(names, declarators, type, sampler,
fst.type_specifier().array_specifier() != null, ctx.getStart().getStartIndex(), ctx.getStop().getStopIndex()));
}
return declarations;
}
}
@@ -0,0 +1,5 @@
#version 460 core
uniform sampler2D source;
uniform ivec4 sourceLevel;
layout(location = 0) out vec4 converted;
void main() { converted = texelFetch(source, ivec2(gl_FragCoord.xy), sourceLevel.x); }
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.sdlgpu;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
@@ -28,19 +29,21 @@ class SDLGPUBufferQueryTest {
}
@Test
void boundTextureReadsTheActiveUnitsSlot() {
final int[] bound = {11, 22, 33, 44};
assertEquals(11, SDLGPURenderBackend.boundTextureOf(0, bound));
assertEquals(44, SDLGPURenderBackend.boundTextureOf(3, bound));
void boundTextureReadsTheUnitsSlot() {
final ContextState st = new ContextState();
st.boundTextures[0] = 11;
st.boundTextures[3] = 44;
assertEquals(11, st.boundTexture(0));
assertEquals(44, st.boundTexture(3));
}
@Test
void outOfRangeUnitReadsZero() {
final int[] bound = {11, 22};
assertEquals(0, SDLGPURenderBackend.boundTextureOf(-1, bound));
assertEquals(0, SDLGPURenderBackend.boundTextureOf(2, bound));
assertEquals(0, SDLGPURenderBackend.boundTextureOf(-33984, bound));
assertEquals(0, SDLGPURenderBackend.boundTextureOf(0, new int[0]));
final ContextState st = new ContextState();
st.boundTextures[0] = 11;
assertEquals(0, st.boundTexture(-1));
assertEquals(0, st.boundTexture(st.boundTextures.length));
assertEquals(0, st.boundTexture(-33984));
}
@Test
@@ -29,7 +29,7 @@ class UniformNegativeLocationTest {
@BeforeEach
void setUp() {
applier = new PipelineApplier(null, null, null, null, null, null, null, null, null);
applier = new PipelineApplier(null, null, null, null, null, null, null, null, null, null);
st = new ContextState();
prog = new ShaderManager.ProgramObject();
prog.linked = true;
@@ -87,6 +87,21 @@ class Lwjgl3GLCapabilitiesShimTest {
assertThrows(IllegalStateException.class, GL::getCapabilities, "clearing on release must stop the thread answering yes to runningOnRenderThread()");
}
@Test
void functionProviderResolvesEveryNameToAbsent() {
final boolean preinstalled = GL.getFunctionProvider() != null;
Lwjgl3GLCapabilitiesShim.installFunctionProvider();
try {
assertNotNull(GL.getFunctionProvider());
if (!preinstalled) {
assertEquals(0L, GL.getFunctionProvider().getFunctionAddress("glBindVertexArrayAPPLE"));
}
Lwjgl3GLCapabilitiesShim.installFunctionProvider();
} finally {
if (!preinstalled) GL.destroy();
}
}
@Test
void advertisedSetHasNoDuplicatesOrBlanks() {
final Set<String> advertised = SDLGPURenderBackend.advertisedCapabilities();
@@ -2,7 +2,7 @@ package com.gtnewhorizons.angelica.sdlgpu.frame;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager.swapchainClearNeedsPassBreak;
import static com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager.fbo0PassStaysOpen;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -19,23 +19,23 @@ class SwapchainClearPassBreakTest {
void aPendingColorClearBreaksTheActivePass() {
final ContextState st = new ContextState();
st.pendingSwapchainClear = true;
assertTrue(swapchainClearNeedsPassBreak(st, activeFbo0Pass()), "otherwise the clear is deferred to the next pass and wipes what this one drew");
assertFalse(fbo0PassStaysOpen(st, activeFbo0Pass()), "otherwise the clear is deferred to the next pass and wipes what this one drew");
}
@Test
void aPendingDepthOrStencilClearBreaksTheActivePass() {
final ContextState depth = new ContextState();
depth.pendingSwapchainDepthClear = true;
assertTrue(swapchainClearNeedsPassBreak(depth, activeFbo0Pass()));
assertFalse(fbo0PassStaysOpen(depth, activeFbo0Pass()));
final ContextState stencil = new ContextState();
stencil.pendingSwapchainStencilClear = true;
assertTrue(swapchainClearNeedsPassBreak(stencil, activeFbo0Pass()));
assertFalse(fbo0PassStaysOpen(stencil, activeFbo0Pass()));
}
@Test
void nothingPendingLeavesThePassAlone() {
assertFalse(swapchainClearNeedsPassBreak(new ContextState(), activeFbo0Pass()));
assertTrue(fbo0PassStaysOpen(new ContextState(), activeFbo0Pass()));
}
@Test
@@ -45,7 +45,7 @@ class SwapchainClearPassBreakTest {
final FrameManager.FrameState f = activeFbo0Pass();
f.renderPass = 0;
assertFalse(swapchainClearNeedsPassBreak(st, f));
assertFalse(fbo0PassStaysOpen(st, f));
}
@Test
@@ -55,6 +55,6 @@ class SwapchainClearPassBreakTest {
final FrameManager.FrameState f = activeFbo0Pass();
f.activeLayoutHash = 0xF80L;
assertFalse(swapchainClearNeedsPassBreak(st, f));
assertFalse(fbo0PassStaysOpen(st, f));
}
}
@@ -0,0 +1,66 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.GlTestSupport;
import com.gtnewhorizons.angelica.glsm.ffp.CubeInstancedAttribs;
import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs;
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL20;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Tag("glsm-sdl")
@ExtendWith(GlsmSdlFrameExtension.class)
class GlsmSdlAttribBindingTest {
private static final String INSTANCED_VERTEX = """
#version 330 core
layout(location = 0) in vec3 a_Position;
layout(location = %d) in vec4 iris_InstEntity;
layout(location = %d) in vec4 iris_CubeTex;
void main() { gl_Position = vec4(a_Position, 1.0) + iris_InstEntity + iris_CubeTex; }
""".formatted(InstancedAttribs.LOC_ENTITY, CubeInstancedAttribs.LOC_CUBE_TEX);
private static final String IMMEDIATE_VERTEX = """
#version 330 core
layout(location = 0) in vec3 a_Position;
layout(location = %d) in vec4 mc_Entity;
layout(location = %d) in vec4 iris_Entity;
void main() { gl_Position = vec4(a_Position, 1.0) + mc_Entity + iris_Entity; }
""".formatted(ImmediateExtendedAttribHandler.LOC_MC_ENTITY, ImmediateExtendedAttribHandler.LOC_ENTITY_IDS);
@Test
void instancedEntityIdsStayAtTheInstanceSlot() {
final int program = linkWithIrisBindings(INSTANCED_VERTEX);
try {
assertEquals(InstancedAttribs.LOC_ENTITY, GLStateManager.glGetAttribLocation(program, "iris_InstEntity"));
assertEquals(CubeInstancedAttribs.LOC_CUBE_TEX, GLStateManager.glGetAttribLocation(program, "iris_CubeTex"));
} finally {
GLStateManager.glDeleteProgram(program);
}
}
@Test
void immediateEntityIdsDoNotMoveOntoMcEntity() {
final int program = linkWithIrisBindings(IMMEDIATE_VERTEX);
try {
assertEquals(ImmediateExtendedAttribHandler.LOC_MC_ENTITY, GLStateManager.glGetAttribLocation(program, "mc_Entity"));
assertEquals(ImmediateExtendedAttribHandler.LOC_ENTITY_IDS, GLStateManager.glGetAttribLocation(program, "iris_Entity"));
} finally {
GLStateManager.glDeleteProgram(program);
}
}
private static int linkWithIrisBindings(String vertexSource) {
final int program = GLStateManager.glCreateProgram();
GLStateManager.glAttachShader(program, GlTestSupport.compile(GL20.GL_VERTEX_SHADER, vertexSource));
GLStateManager.glAttachShader(program, GlTestSupport.compile(GL20.GL_FRAGMENT_SHADER, GlTestSupport.TRIVIAL_FS));
GLStateManager.glBindAttribLocation(program, InstancedAttribs.LOC_ENTITY, "iris_InstEntity");
GLStateManager.glBindAttribLocation(program, ImmediateExtendedAttribHandler.LOC_MC_ENTITY, "mc_Entity");
GLStateManager.glLinkProgram(program);
return program;
}
}
@@ -46,7 +46,7 @@ class GlsmSdlFfpTest {
GLStateManager.setFfpInstancing(Instancing.NONE);
CubeParityFixture.deleteResources();
ParticleParityFixture.deleteResources();
FfpFixture.IntegerInstances.delete();
FfpFixture.EntityIdInstances.delete();
}
private static void bindBaseTexture() {
@@ -172,11 +172,11 @@ class GlsmSdlFfpTest {
@Test
void entityAttribSelectsColorPerInstance() {
FfpFixture.IntegerInstances.build(0.6f);
FfpFixture.EntityIdInstances.build(0.6f);
GlsmSdlHeadlessRig.beginFrameAndClear();
FfpFixture.IntegerInstances.draw();
FfpFixture.EntityIdInstances.draw();
final int[] pixels = GlsmSdlHeadlessRig.readTarget();
@@ -29,6 +29,14 @@ public final class GlsmSdlHeadlessRig {
public static final int SIZE = 64;
public static final String FULLSCREEN_VERTEX = """
#version 330 core
void main() {
vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
}
""";
public static final int RED = 0xFFFF0000;
public static final int GREEN = 0xFF00FF00;
public static final int BLUE = 0xFF0000FF;
@@ -1,12 +1,15 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.GlTestSupport;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -49,6 +52,47 @@ class GlsmSdlSamplerBindingTest {
}
}
@Test
void pointingASamplerAtAnotherUnitRebindsItBeforeTheNextDraw() {
final int program = GlTestSupport.program(GlsmSdlHeadlessRig.FULLSCREEN_VERTEX, """
#version 330 core
uniform sampler2D s;
out vec4 fragColor;
void main() { fragColor = texture(s, vec2(0.5)); }
""");
final int red = GlsmSdlHeadlessRig.createSolidTexture(0xFFFF0000);
final int green = GlsmSdlHeadlessRig.createSolidTexture(0xFF00FF00);
final int vao = GLStateManager.glGenVertexArrays();
try {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, red);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, green);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glUseProgram(program);
final int location = GLStateManager.glGetUniformLocation(program, "s");
GLStateManager.glUniform1i(location, 0);
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
GLStateManager.glUniform1i(location, 1);
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
GLStateManager.glUseProgram(0);
final int center = GlsmSdlHeadlessRig.readTarget(GlsmSdlHeadlessRig.SIZE / 2, GlsmSdlHeadlessRig.SIZE / 2, 1, 1)[0];
assertEquals(Integer.toHexString(GlsmSdlHeadlessRig.GREEN), Integer.toHexString(center), "the second draw must sample unit 1");
} finally {
GLStateManager.glBindVertexArray(0);
GLStateManager.glDeleteVertexArrays(vao);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
GLStateManager.glDeleteTextures(red);
GLStateManager.glDeleteTextures(green);
GLStateManager.glDeleteProgram(program);
}
}
@Test
void unitCountMatchesTheSdlSamplerArray() {
GLStateManager.glBindSampler(0, 0);
@@ -0,0 +1,685 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.GlTestSupport;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import it.unimi.dsi.fastutil.longs.LongSet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL21;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL43;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import java.util.function.IntSupplier;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
/**
* A stage reading more samplers than SDL_GPU's 16 slots moves the texelFetch-only ones into texture slots,
* which bind from the sampler's texture unit rather than an image unit.
*/
@Tag("glsm-sdl")
@ExtendWith(GlsmSdlFrameExtension.class)
class GlsmSdlSamplerOverflowTest {
private static final int SAMPLERS = 18;
private static final int FETCH_RED = 16;
private static final int FETCH_GREEN = 17;
private static final int FILTERED_BLUE = 15;
private static final int EXPECTED = 0xFF4080C0;
private final int[] textures = new int[SAMPLERS];
private int program;
private int vao;
private int redLod;
private MovedSamplerProbe copyProbe;
@BeforeEach
void observeCopies() {
copyProbe = new MovedSamplerProbe();
}
private static String name(int sampler, String redName) {
return sampler == FETCH_RED ? redName : "t" + sampler;
}
private static String fragment(String redName, boolean lastTwoFetchOnly) {
final StringBuilder sb = new StringBuilder("#version 330 core\n");
for (int i = 0; i < SAMPLERS; i++) sb.append("uniform sampler2D ").append(name(i, redName)).append(";\n");
sb.append("uniform int redLod;\nout vec4 fragColor;\nvoid main() {\n vec4 unused = vec4(0.0);\n");
final int filtered = lastTwoFetchOnly ? FETCH_RED : SAMPLERS;
for (int i = 0; i < filtered; i++) sb.append(" unused += texture(").append(name(i, redName)).append(", vec2(0.5)) * 0.0;\n");
if (lastTwoFetchOnly) {
sb.append(" fragColor = vec4(texelFetch(").append(redName).append(", ivec2(0), redLod).r, texelFetch(t").append(FETCH_GREEN)
.append(", ivec2(0), 0).g, texture(t").append(FILTERED_BLUE).append(", vec2(0.5)).b, 1.0) + unused;\n");
} else {
sb.append(" fragColor = vec4(1.0) + unused;\n");
}
return sb.append("}\n").toString();
}
private static int colorFor(int sampler) {
return switch (sampler) {
case FETCH_RED -> 0xFF401122;
case FETCH_GREEN -> 0xFF338044;
case FILTERED_BLUE -> 0xFF5566C0;
default -> 0xFF000000;
};
}
private void setUp(String redName, int[] units) {
setUp(redName, units, () -> GlsmSdlHeadlessRig.createSolidTexture(colorFor(FETCH_RED)));
}
private void setUp(String redName, int[] units, IntSupplier redTexture) {
program = GlTestSupport.program(GlsmSdlHeadlessRig.FULLSCREEN_VERTEX, fragment(redName, true));
final ShaderManager.ProgramObject po = shaderManager().getProgram(program);
assertEquals(16, po.fragmentResources.numSamplers(), "fragment sampler slots");
assertEquals(2, po.fragmentResources.numStorageTextures(), "fetch-only samplers moved to texture slots");
for (int i = 0; i < SAMPLERS; i++) textures[i] = i == FETCH_RED ? redTexture.getAsInt() : GlsmSdlHeadlessRig.createSolidTexture(colorFor(i));
for (int i = 0; i < SAMPLERS; i++) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + units[i]);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, textures[i]);
}
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
vao = GLStateManager.glGenVertexArrays();
}
private int drawCenter(String redName, int[] units) {
bindDrawState(redName, units);
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
GLStateManager.glUseProgram(0);
assertMovedSamplersReadCopiesOnVulkan();
return GlsmSdlHeadlessRig.readTarget(GlsmSdlHeadlessRig.SIZE / 2, GlsmSdlHeadlessRig.SIZE / 2, 1, 1)[0];
}
private void bindDrawState(String redName, int[] units) {
GlsmSdlHeadlessRig.bindTarget();
GLStateManager.glUseProgram(program);
GLStateManager.glUniform1i(GLStateManager.glGetUniformLocation(program, "redLod"), redLod);
for (int i = 0; i < SAMPLERS; i++) {
final int location = GLStateManager.glGetUniformLocation(program, name(i, redName));
assertTrue(location >= 0, name(i, redName) + " has no location");
GLStateManager.glUniform1i(location, units[i]);
}
GLStateManager.glBindVertexArray(vao);
}
private void assertMovedSamplersReadCopiesOnVulkan() {
if (!SDLGPUGate.device().isVulkan()) return;
final ContextState st = SdlTestRig.contextState();
for (int slot = 0; slot < 2; slot++) {
final long bound = st.lastFragStorageTex[slot];
assertNotEquals(0L, bound, "texture slot " + slot + " unbound");
for (int texture : textures) {
assertNotEquals(GlsmSdlHeadlessRig.resourceManager().getTextureHandle(texture), bound, "texture slot " + slot + " bound a sampleable texture directly");
}
}
}
private static int[] shuffledUnits() {
final int[] units = new int[SAMPLERS];
for (int i = 0; i < SAMPLERS; i++) units[i] = (i + 5) % SAMPLERS;
return units;
}
@AfterEach
void cleanUp() {
copyProbe.close();
GLStateManager.glDisable(GL11.GL_COLOR_LOGIC_OP);
GLStateManager.glLogicOp(GL11.GL_COPY);
GLStateManager.glBindImageTexture(0, 0, 0, false, 0, GL15.GL_WRITE_ONLY, GL11.GL_RGBA8);
GLStateManager.glBindVertexArray(0);
if (vao != 0) GLStateManager.glDeleteVertexArrays(vao);
for (int unit = 0; unit < SAMPLERS; unit++) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + unit);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
}
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
for (int i = 0; i < SAMPLERS; i++) {
if (textures[i] != 0) GLStateManager.glDeleteTextures(textures[i]);
textures[i] = 0;
}
if (program != 0) GLStateManager.glDeleteProgram(program);
program = 0;
vao = 0;
}
@Test
void fetchOnlySamplersPastTheSlotLimitReadTheTexturesOnTheirUnits() {
final int[] units = shuffledUnits();
setUp("t" + FETCH_RED, units);
assertEquals(Integer.toHexString(EXPECTED), Integer.toHexString(drawCenter("t" + FETCH_RED, units)),
"red and green from the moved samplers, blue from a sampler slot");
}
@Test
void unboundMovedSamplerReadsTheFloat2DFallback() {
final int[] units = shuffledUnits();
setUp("t" + FETCH_RED, units);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + units[FETCH_RED]);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
assertEquals(0xFF0080C0, drawCenter("t" + FETCH_RED, units));
}
@ParameterizedTest
@ValueSource(strings = {"uniform", "texture binding"})
void changingAMovedSamplerPreparesAndBindsTheNewTexture(String change) {
final int[] units = shuffledUnits();
setUp("t" + FETCH_RED, units);
bindDrawState("t" + FETCH_RED, units);
for (int i = 0; i < 3; i++) GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
if (change.equals("uniform")) {
GLStateManager.glUniform1i(GLStateManager.glGetUniformLocation(program, "t" + FETCH_RED), units[0]);
} else {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + units[FETCH_RED]);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, textures[0]);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
}
final int refreshes = copyProbe.refreshCalls();
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
if (SDLGPUGate.device().isVulkan()) assertEquals(refreshes + 1, copyProbe.refreshCalls(), "a changed moved sampler must invalidate preparation");
GLStateManager.glUseProgram(0);
assertEquals(0xFF0080C0, GlsmSdlHeadlessRig.readTarget(32, 32, 1, 1)[0], "the new unit supplies black instead of red");
}
@Test
void movedSamplerSeesAClearIssuedBeforeTheDraw() {
final int[] units = shuffledUnits();
setUp("t" + FETCH_RED, units);
final int fbo = GlsmSdlHeadlessRig.fboWithColor(textures[FETCH_RED], 0);
try {
GLStateManager.glViewport(0, 0, 2, 2);
GLStateManager.glClearColor(0x60 / 255.0f, 0.0f, 0.0f, 1.0f);
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
assertEquals(Integer.toHexString(0xFF6080C0), Integer.toHexString(drawCenter("t" + FETCH_RED, units)),
"the moved sampler must read the cleared texture, not its uploaded texels");
} finally {
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLStateManager.glDeleteFramebuffers(fbo);
}
}
@Test
void movedSamplerReadsAnSrgbTextureDecoded() {
final int[] units = shuffledUnits();
setUp("t" + FETCH_RED, units, () -> {
final int texture = GlsmSdlHeadlessRig.newNearestTexture();
final ByteBuffer texels = MemoryUtil.memAlloc(2 * 2 * 4);
try {
for (int i = 0; i < 4; i++) GlsmSdlHeadlessRig.putArgb(texels, i, colorFor(FETCH_RED));
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL21.GL_SRGB8_ALPHA8, 2, 2, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, texels);
} finally {
MemoryUtil.memFree(texels);
}
return texture;
});
// sRGB 0x40 decodes to linear 0.0513, which the RGBA8 target stores as 13.
assertChannels(drawCenter("t" + FETCH_RED, units), 13, 0x80, 0xC0, "red decoded from the sRGB texture");
if (SDLGPUGate.device().isVulkan()) {
final long copy = watchCopy("t" + FETCH_RED);
final int writes = copyProbe.writesTo(copy);
GlsmSdlHeadlessRig.beginFrame();
assertChannels(drawCenter("t" + FETCH_RED, units), 13, 0x80, 0xC0, "cached conversion keeps the decoded red");
assertEquals(writes, copyProbe.writesTo(copy), "unchanged sRGB content must not be converted again");
}
}
@Test
void movedSamplerReadsADepthTexture() {
final int[] units = shuffledUnits();
setUp("t" + FETCH_RED, units, () -> {
final int texture = GlsmSdlHeadlessRig.newNearestTexture();
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL30.GL_DEPTH_COMPONENT32F, 2, 2, 0, GL11.GL_DEPTH_COMPONENT, GL11.GL_FLOAT, (ByteBuffer) null);
final int fbo = GlsmSdlHeadlessRig.fboWithColor(GlsmSdlHeadlessRig.createSolidTexture(0xFF000000), 0);
GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL11.GL_TEXTURE_2D, texture, 0);
GLStateManager.glViewport(0, 0, 2, 2);
GLStateManager.glDepthMask(true);
GLStateManager.glClearDepth(0.25);
GLStateManager.glClear(GL11.GL_DEPTH_BUFFER_BIT);
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLStateManager.glDeleteFramebuffers(fbo);
return texture;
});
assertChannels(drawCenter("t" + FETCH_RED, units), 64, 0x80, 0xC0, "red is the cleared depth 0.25");
}
private static void assertChannels(int argb, int r, int g, int b, String message) {
final int gotR = (argb >> 16) & 0xFF, gotG = (argb >> 8) & 0xFF, gotB = argb & 0xFF;
assertTrue(Math.abs(gotR - r) <= 1 && Math.abs(gotG - g) <= 1 && Math.abs(gotB - b) <= 1,
() -> message + ": expected rgb(" + r + "," + g + "," + b + ") but was rgb(" + gotR + "," + gotG + "," + gotB + ")");
}
@Test
void copiesOfDeletedTexturesAreReleased() {
assumeTrue(SDLGPUGate.device().isVulkan(), "copies exist only on Vulkan");
final int[] units = shuffledUnits();
setUp("t" + FETCH_RED, units);
drawCenter("t" + FETCH_RED, units);
final int beforeDelete = liveTextureCount();
GLStateManager.glDeleteTextures(textures[FETCH_RED]);
textures[FETCH_RED] = 0;
GlsmSdlHeadlessRig.beginFrame();
GlsmSdlHeadlessRig.endFrame();
assertEquals(beforeDelete - 1, liveTextureCount(), "the copy is released while GLSM retains a 1x1 placeholder for the deleted name");
}
@Test
void copiesNothingReadsAnyMoreAreReleasedAfterAFewFrames() {
assumeTrue(SDLGPUGate.device().isVulkan(), "copies exist only on Vulkan");
final int[] units = shuffledUnits();
setUp("t" + FETCH_RED, units);
drawCenter("t" + FETCH_RED, units);
final int beforeExpiry = liveTextureCount();
GLStateManager.glDeleteProgram(program);
program = 0;
for (int frame = 0; frame < 12; frame++) {
GlsmSdlHeadlessRig.beginFrame();
GlsmSdlHeadlessRig.endFrame();
}
assertEquals(beforeExpiry - 2, liveTextureCount(), "both unused copies must be released");
}
private static int liveTextureCount() {
final LongSet handles = Reflect.get(GlsmSdlHeadlessRig.resourceManager(), "liveTextureHandles");
return handles.size();
}
private long boundCopy(String name) {
return SdlTestRig.contextState().lastFragStorageTex[shaderManager().getProgram(program).fragmentImageNames.indexOf(name)];
}
private long watchCopy(String name) {
final long copy = boundCopy(name);
assertNotEquals(0L, copy);
copyProbe.watchWrites(copy);
return copy;
}
@Test
void unchangedSourcesReuseCopiesAcrossRenderPassesAndFrames() {
assumeTrue(SDLGPUGate.device().isVulkan(), "copies exist only on Vulkan");
final int[] units = shuffledUnits();
setUp("t" + FETCH_RED, units);
bindDrawState("t" + FETCH_RED, units);
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
final long copy = watchCopy("t" + FETCH_RED);
final int writes = copyProbe.writesTo(copy);
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
final int refreshes = copyProbe.refreshCalls();
assertTrue(refreshes > 0, "the probe must observe preparation before the cache can skip it");
final var frame = GlsmSdlHeadlessRig.frameManager().frame();
final long pass = frame.renderPassGeneration;
final int bindGen = SdlTestRig.contextState().samplerBindGen;
final long writeGen = GlsmSdlHeadlessRig.resourceManager().textureWriteGeneration();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + units[0]);
for (int i = 0; i < 4; i++) {
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, textures[(i + 1) % 2]);
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
}
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
assertEquals(pass, frame.renderPassGeneration, "the draws keep the same render pass");
assertNotEquals(bindGen, SdlTestRig.contextState().samplerBindGen, "the entity texture changes reached the backend");
assertEquals(writeGen, GlsmSdlHeadlessRig.resourceManager().textureWriteGeneration(), "observing copies must not change write tracking");
assertEquals(refreshes, copyProbe.refreshCalls(), "binding unrelated entity textures must skip preparation");
assertEquals(writes, copyProbe.writesTo(copy), "repeated draws must not copy unchanged content");
GlsmSdlHeadlessRig.frameManager().endRenderPassIfActive();
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
assertEquals(refreshes + 1, copyProbe.refreshCalls(), "a new pass must recheck its snapshots");
assertEquals(writes, copyProbe.writesTo(copy), "a new pass must reuse unchanged content");
GLStateManager.glUseProgram(0);
GlsmSdlHeadlessRig.endFrame();
GlsmSdlHeadlessRig.beginFrame();
assertEquals(EXPECTED, drawCenter("t" + FETCH_RED, units));
assertEquals(copy, boundCopy("t" + FETCH_RED), "the allocation is reused");
assertEquals(writes, copyProbe.writesTo(copy), "a new frame must reuse unchanged content");
}
@ParameterizedTest
@ValueSource(strings = {"upload", "copy", "blit", "clear", "mipmap", "compute", "copyTexSubImage"})
void writesRefreshOnlyTheChangedSource(String write) {
assumeTrue(SDLGPUGate.device().isVulkan(), "copies exist only on Vulkan");
final int[] units = shuffledUnits();
if (write.equals("mipmap")) {
redLod = 1;
setUp("t" + FETCH_RED, units, () -> {
final int texture = GlsmSdlHeadlessRig.createSolidTexture(0xFF000000);
GlsmSdlHeadlessRig.uploadSolidLevel(1, 1, colorFor(FETCH_RED));
return texture;
});
} else {
setUp("t" + FETCH_RED, units);
}
if (write.equals("compute")) GLStateManager.glBindImageTexture(0, textures[FETCH_RED], 0, false, 0, GL15.GL_WRITE_ONLY, GL11.GL_RGBA8);
final int fbo = GlsmSdlHeadlessRig.fboWithColor(textures[FETCH_RED], 0);
try {
assertEquals(EXPECTED, drawCenter("t" + FETCH_RED, units));
final long redCopy = watchCopy("t" + FETCH_RED);
final long greenCopy = watchCopy("t" + FETCH_GREEN);
final int redWrites = copyProbe.writesTo(redCopy);
final int greenWrites = copyProbe.writesTo(greenCopy);
GlsmSdlHeadlessRig.beginFrame();
switch (write) {
case "upload" -> {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + units[FETCH_RED]);
final ByteBuffer texels = MemoryUtil.memAlloc(16);
try {
for (int i = 0; i < 4; i++) GlsmSdlHeadlessRig.putArgb(texels, i, 0xFF000000);
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, texels);
} finally {
MemoryUtil.memFree(texels);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
}
}
case "copy" -> BackendManager.RENDER_BACKEND.copyImageSubData(textures[0], GL11.GL_TEXTURE_2D, 0, 0, 0, 0,
textures[FETCH_RED], GL11.GL_TEXTURE_2D, 0, 0, 0, 0, 2, 2, 1);
case "blit" -> {
final int srcFbo = GlsmSdlHeadlessRig.fboWithColor(textures[0], 0);
try {
GLStateManager.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, srcFbo);
GLStateManager.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo);
GLStateManager.glBlitFramebuffer(0, 0, 2, 2, 0, 0, 2, 2, GL11.GL_COLOR_BUFFER_BIT, GL11.GL_NEAREST);
} finally {
GLStateManager.glDeleteFramebuffers(srcFbo);
}
}
case "clear" -> {
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo);
GLStateManager.glClearColor(0, 0, 0, 1);
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
}
case "mipmap" -> BackendManager.RENDER_BACKEND.generateTextureMipmap(textures[FETCH_RED]);
case "compute" -> computeBlack();
case "copyTexSubImage" -> {
final int srcFbo = GlsmSdlHeadlessRig.fboWithColor(textures[0], 0);
try {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + units[FETCH_RED]);
GLStateManager.glCopyTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, 0, 0, 2, 2);
} finally {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glDeleteFramebuffers(srcFbo);
}
}
default -> throw new AssertionError(write);
}
assertEquals(0xFF0080C0, drawCenter("t" + FETCH_RED, units), write + " changes the sampled red");
assertEquals(redWrites + 1, copyProbe.writesTo(redCopy), "changed source copied once");
assertEquals(greenWrites, copyProbe.writesTo(greenCopy), "unchanged source not copied again");
} finally {
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLStateManager.glDeleteFramebuffers(fbo);
}
}
private static void computeBlack() {
final int shader = GlTestSupport.compile(GL43.GL_COMPUTE_SHADER, """
#version 430 core
layout(local_size_x = 1, local_size_y = 1) in;
layout(rgba8, binding = 0) uniform writeonly image2D target;
void main() { imageStore(target, ivec2(gl_GlobalInvocationID.xy), vec4(0.0, 0.0, 0.0, 1.0)); }
""");
final int compute = GLStateManager.glCreateProgram();
try {
GLStateManager.glAttachShader(compute, shader);
GLStateManager.glLinkProgram(compute);
assertEquals(1, GLStateManager.glGetProgrami(compute, GL20.GL_LINK_STATUS), GLStateManager.glGetProgramInfoLog(compute, 4096));
GLStateManager.glUseProgram(compute);
GLStateManager.glDispatchCompute(2, 2, 1);
} finally {
GLStateManager.glUseProgram(0);
GLStateManager.glDeleteProgram(compute);
GLStateManager.glDeleteShader(shader);
}
}
@Test
void partialClearDoesNotPrepareMovedSamplers() {
assumeTrue(SDLGPUGate.device().isVulkan(), "copies exist only on Vulkan");
final int[] units = shuffledUnits();
setUp("t" + FETCH_RED, units);
bindDrawState("t" + FETCH_RED, units);
final var frame = GlsmSdlHeadlessRig.frameManager().frame();
final long before = frame.renderPassGeneration;
GLStateManager.glEnable(GL11.GL_SCISSOR_TEST);
GLStateManager.glScissor(0, 0, 1, 1);
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
GLStateManager.glDisable(GL11.GL_SCISSOR_TEST);
assertEquals(before + 1, frame.renderPassGeneration, "only the clear's render pass is opened");
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
assertNotEquals(0L, boundCopy("t" + FETCH_RED), "the first draw prepares the copy");
GLStateManager.glUseProgram(0);
assertEquals(EXPECTED, GlsmSdlHeadlessRig.readTarget(32, 32, 1, 1)[0]);
}
@ParameterizedTest
@CsvSource({"sampler1D, int", "sampler3D, ivec3", "sampler2DArray, ivec3", "isampler2D, ivec2", "usampler2D, ivec2"})
void overflowRejectsTypesWithoutAMatchingFallback(String type, String coord) {
final String source = "#version 330 core\n" + declareFiltered(16) + "uniform " + type + " fetched;\nout vec4 fragColor;\n"
+ "void main() { vec4 c = vec4(0.0);\n" + readFiltered(16, "c", "")
+ "fragColor = c + vec4(texelFetch(fetched, " + coord + "(0), 0)); }\n";
final int vs = GlTestSupport.compile(GL20.GL_VERTEX_SHADER, GlsmSdlHeadlessRig.FULLSCREEN_VERTEX);
final int fs = GlTestSupport.compile(GL20.GL_FRAGMENT_SHADER, source);
program = GLStateManager.glCreateProgram();
try {
GLStateManager.glAttachShader(program, vs);
GLStateManager.glAttachShader(program, fs);
GLStateManager.glLinkProgram(program);
assertEquals(0, GLStateManager.glGetProgrami(program, GL20.GL_LINK_STATUS));
assertTrue(GLStateManager.glGetProgramInfoLog(program, 4096).contains("Too many fragment shader texture samplers"));
} finally {
GLStateManager.glDeleteShader(vs);
GLStateManager.glDeleteShader(fs);
}
}
private static String declareFiltered(int count) {
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) sb.append("uniform sampler2D f").append(i).append(";\n");
return sb.toString();
}
private static String readFiltered(int count, String into, String scale) {
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) sb.append(" ").append(into).append(" += texture(f").append(i).append(", vec2(0.5))").append(scale).append(";\n");
return sb.toString();
}
@Test
void unusedSamplerDeclaredWithAUsedOneStillTakesASlot() {
final StringBuilder sb = new StringBuilder("#version 330 core\n").append(declareFiltered(14));
sb.append("uniform sampler2D f14, neverRead;\n");
for (int i = 0; i < 3; i++) sb.append("uniform sampler2D x").append(i).append(";\n");
sb.append("out vec4 fragColor;\nvoid main() {\n vec4 c = vec4(0.0);\n").append(readFiltered(15, "c", ""));
for (int i = 0; i < 3; i++) sb.append(" c += texelFetch(x").append(i).append(", ivec2(0), 0);\n");
sb.append(" fragColor = c;\n}\n");
program = GlTestSupport.program(GlsmSdlHeadlessRig.FULLSCREEN_VERTEX, sb.toString());
final ShaderManager.ProgramObject po = shaderManager().getProgram(program);
assertEquals(16, po.fragmentResources.numSamplers(), "15 read samplers plus the unread one declared beside f14");
assertEquals(3, po.fragmentResources.numStorageTextures(), "all three fetch-only samplers moved");
}
@ParameterizedTest(name = "helper inside #if: {0}")
@ValueSource(booleans = {false, true})
void samplerPassedToTheShadersOwnTextureSizeStaysASampler(boolean helperInConditional) {
final StringBuilder sb = new StringBuilder("#version 330 core\n").append(declareFiltered(15));
sb.append("uniform sampler2D helperArgument;\nuniform sampler2D fetched;\nout vec4 fragColor;\n");
if (helperInConditional) sb.append("#if 1\n");
sb.append("ivec2 textureSize(sampler2D s) { return textureSize(s, 0); }\n");
if (helperInConditional) sb.append("#endif\n");
sb.append("void main() {\n vec4 c = vec4(0.0);\n").append(readFiltered(15, "c", ""));
sb.append(" c.xy += vec2(textureSize(helperArgument));\n c += texelFetch(fetched, ivec2(0), 0);\n fragColor = c;\n}\n");
program = GlTestSupport.program(GlsmSdlHeadlessRig.FULLSCREEN_VERTEX, sb.toString());
final ShaderManager.ProgramObject po = shaderManager().getProgram(program);
assertEquals(16, po.fragmentResources.numSamplers(), "helperArgument must stay a sampler; only 'fetched' moves");
assertEquals(1, po.fragmentResources.numStorageTextures(), "one sampler moved");
}
@Test
void samplerReadThroughATexelFetchMacroStaysASampler() {
final StringBuilder sb = new StringBuilder("#version 330 core\n");
sb.append("#define texelFetch(s, p, l) textureLod(s, vec2(p), float(l))\n").append(declareFiltered(15));
sb.append("uniform sampler2D throughMacro;\nuniform sampler2D sized0;\nuniform sampler2D sized1;\nout vec4 fragColor;\n");
sb.append("void main() {\n vec4 c = vec4(0.0);\n").append(readFiltered(15, "c", ""));
sb.append(" c += texelFetch(throughMacro, ivec2(0), 0);\n");
sb.append(" c.xy += vec2(textureSize(sized0, 0) + textureSize(sized1, 0));\n fragColor = c;\n}\n");
program = GlTestSupport.program(GlsmSdlHeadlessRig.FULLSCREEN_VERTEX, sb.toString());
final ShaderManager.ProgramObject po = shaderManager().getProgram(program);
assertEquals(16, po.fragmentResources.numSamplers(), "throughMacro must stay a sampler; the two textureSize-only ones move");
assertEquals(2, po.fragmentResources.numStorageTextures(), "two samplers moved");
}
@Test
void macroSharingAnUnusedSamplersNameDoesNotKeepTheSampler() {
final StringBuilder sb = new StringBuilder("#version 330 core\n#define helper(x) (x)\n").append(declareFiltered(16));
sb.append("uniform sampler2D helper;\nout vec4 fragColor;\nvoid main() {\n vec4 c = vec4(0.0);\n").append(readFiltered(16, "c", ""));
sb.append(" fragColor = c;\n}\n");
program = GlTestSupport.program(GlsmSdlHeadlessRig.FULLSCREEN_VERTEX, sb.toString());
assertEquals(16, shaderManager().getProgram(program).fragmentResources.numSamplers(), "the unread sampler 'helper' must still be stripped");
}
@Test
void samplerNamedThroughAMacroKeepsTheExpandedName() {
final StringBuilder sb = new StringBuilder("#version 330 core\n#define alias realTex\n").append(declareFiltered(15));
sb.append("uniform sampler2D alias;\nuniform sampler2D fetched;\nout vec4 fragColor;\n");
sb.append("void main() {\n vec4 c = vec4(0.0);\n").append(readFiltered(15, "c", ""));
sb.append(" c += texelFetch(alias, ivec2(0), 0) + texelFetch(fetched, ivec2(0), 0);\n fragColor = c;\n}\n");
program = GlTestSupport.program(GlsmSdlHeadlessRig.FULLSCREEN_VERTEX, sb.toString());
assertEquals(1, shaderManager().getProgram(program).fragmentResources.numStorageTextures(), "only 'fetched' moves");
assertTrue(GLStateManager.glGetUniformLocation(program, "realTex") >= 0, "the uniform keeps the name the macro expands to");
}
@ParameterizedTest
@ValueSource(booleans = {false, true})
void movedSamplerOnlyReadByAnUncalledFunctionUsesFallbackWithoutCopying(boolean fragment) {
final StringBuilder sb = new StringBuilder("#version 330 core\n").append(declareFiltered(16));
sb.append("uniform sampler2D shadowtex0;\n");
if (fragment) sb.append("out vec4 fragColor;\n");
sb.append("vec4 neverCalled() { return texelFetch(shadowtex0, ivec2(0), 0); }\n");
sb.append("void main() {\n vec4 c = vec4(0.0);\n").append(readFiltered(16, "c", " * 0.0"));
if (fragment) {
sb.append(" fragColor = vec4(0.0, 1.0, 0.0, 1.0) + c;\n}\n");
} else {
sb.append(" vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);\n gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0) + c;\n}\n");
}
program = GlTestSupport.program(fragment ? GlsmSdlHeadlessRig.FULLSCREEN_VERTEX : sb.toString(), fragment ? sb.toString() : """
#version 330 core
out vec4 fragColor;
void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
""");
final ShaderManager.ProgramObject po = shaderManager().getProgram(program);
assertEquals(1, (fragment ? po.fragmentResources : po.vertexResources).numStorageTextures(), "shadowtex0 moved");
assertEquals(-1, GLStateManager.glGetUniformLocation(program, "shadowtex0"), "an unread sampler must report -1, or Iris treats shadows as used");
assertTrue(GLStateManager.glGetUniformLocation(program, "f0") >= 0, "read samplers keep their location");
textures[0] = GlsmSdlHeadlessRig.createSolidTexture(GlsmSdlHeadlessRig.RED);
vao = GLStateManager.glGenVertexArrays();
GLStateManager.glBindVertexArray(vao);
GLStateManager.glUseProgram(program);
GlsmSdlHeadlessRig.bindTarget();
final var rm = GlsmSdlHeadlessRig.resourceManager();
final long fallback = rm.getOrCreateFallbackStorageTexture2D();
final long before = rm.textureWriteGeneration();
for (int i = 0; i < 3; i++) GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
final ContextState st = SdlTestRig.contextState();
assertEquals(fallback, (fragment ? st.lastFragStorageTex : st.lastVertStorageTex)[0], "the unread slot uses the 2D fallback");
assertEquals(before, rm.textureWriteGeneration(), "the unread sampler must not start tracking or copying unit 0");
GLStateManager.glUseProgram(0);
assertEquals(GlsmSdlHeadlessRig.GREEN, GlsmSdlHeadlessRig.readTarget(32, 32, 1, 1)[0]);
}
@Test
void copyIsRefreshedWhenTheDrawMovesToAnotherFramebuffer() {
final StringBuilder sb = new StringBuilder("#version 330 core\n").append(declareFiltered(16));
sb.append("uniform sampler2D written;\nuniform int readWritten;\nout vec4 fragColor;\nvoid main() {\n vec4 unused = vec4(0.0);\n");
sb.append(readFiltered(16, "unused", " * 0.0"));
sb.append(" float size = float(textureSize(written, 0).x) * 0.0;\n");
sb.append(" fragColor = (readWritten == 1 ? texelFetch(written, ivec2(0), 0) : vec4(0x40 / 255.0, 0.0, 0.0, 1.0)) + unused + size;\n}\n");
program = GlTestSupport.program(GlsmSdlHeadlessRig.FULLSCREEN_VERTEX, sb.toString());
assertEquals(1, shaderManager().getProgram(program).fragmentResources.numStorageTextures(), "'written' moved");
for (int i = 0; i < 16; i++) textures[i] = GlsmSdlHeadlessRig.createSolidTexture(0xFF000000);
textures[16] = GlsmSdlHeadlessRig.createSolidTexture(0xFF00FF00);
for (int i = 0; i <= 16; i++) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + i);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, textures[i]);
}
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
vao = GLStateManager.glGenVertexArrays();
final int fbo = GlsmSdlHeadlessRig.fboWithColor(textures[16], 0);
try {
GLStateManager.glUseProgram(program);
for (int i = 0; i < 16; i++) GLStateManager.glUniform1i(GLStateManager.glGetUniformLocation(program, "f" + i), i);
GLStateManager.glUniform1i(GLStateManager.glGetUniformLocation(program, "written"), 16);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glViewport(0, 0, 2, 2);
GLStateManager.glUniform1i(GLStateManager.glGetUniformLocation(program, "readWritten"), 0);
for (int i = 0; i < 3; i++) GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
GlsmSdlHeadlessRig.bindTarget();
GLStateManager.glUniform1i(GLStateManager.glGetUniformLocation(program, "readWritten"), 1);
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
GLStateManager.glUseProgram(0);
assertChannels(GlsmSdlHeadlessRig.readTarget(GlsmSdlHeadlessRig.SIZE / 2, GlsmSdlHeadlessRig.SIZE / 2, 1, 1)[0], 0x40, 0, 0,
"the second draw must see what the first draw wrote, not the texture's earlier green");
} finally {
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLStateManager.glDeleteFramebuffers(fbo);
}
}
@Test
void tooManyFilteredSamplersFailLinkInsteadOfDraw() {
final int vs = GlTestSupport.compile(GL20.GL_VERTEX_SHADER, GlsmSdlHeadlessRig.FULLSCREEN_VERTEX);
final int fs = GlTestSupport.compile(GL20.GL_FRAGMENT_SHADER, fragment("t" + FETCH_RED, false));
final int failed = GLStateManager.glCreateProgram();
try {
GLStateManager.glAttachShader(failed, vs);
GLStateManager.glAttachShader(failed, fs);
GLStateManager.glLinkProgram(failed);
assertEquals(0, GLStateManager.glGetProgrami(failed, GL20.GL_LINK_STATUS), "18 filtered samplers cannot fit 16 slots");
final String log = GLStateManager.glGetProgramInfoLog(failed, 4096);
assertTrue(log.contains("Too many fragment shader texture samplers (18, maximum 16)"), () -> "info log should name the count: " + log);
} finally {
GLStateManager.glDeleteShader(vs);
GLStateManager.glDeleteShader(fs);
GLStateManager.glDeleteProgram(failed);
}
}
private static ShaderManager shaderManager() {
return Reflect.get(BackendManager.RENDER_BACKEND, "shaderManager");
}
}
@@ -0,0 +1,60 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.EntityIdsOnlyHandler;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.GlTestSupport;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorEntityIdsFixture;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig.BLUE;
import static com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig.GREEN;
import static com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig.SIZE;
@Tag("glsm-sdl")
@ExtendWith(GlsmSdlFrameExtension.class)
class GlsmSdlTessellatorEntityIdsTest {
private ImmediateExtendedAttribHandler previousHandler;
private int program;
@BeforeEach
void setUp() {
previousHandler = EntityIdsOnlyHandler.install();
program = GlTestSupport.program(TessellatorEntityIdsFixture.VERTEX, TessellatorEntityIdsFixture.FRAGMENT);
}
@AfterEach
void tearDown() {
GLStateManager.glUseProgram(0);
GLStateManager.glDeleteProgram(program);
GLSMHooks.immediateExtendedHandler = previousHandler;
}
@Test
void idsChangedMidBatchApplyToTheVerticesAddedAfterTheChange() {
GlsmSdlHeadlessRig.beginFrameAndClear();
GLStateManager.glUseProgram(program);
TessellatorEntityIdsFixture.drawIdsChangedMidBatch();
final int[] pixels = GlsmSdlHeadlessRig.readTarget();
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, SIZE / 4, SIZE / 2, GREEN, "left quad was added under item 7");
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, 3 * SIZE / 4, SIZE / 2, BLUE, "right quad was added under item 50020");
}
@Test
void idsChangedAfterTheLastVertexLeaveTheBatchAlone() {
GlsmSdlHeadlessRig.beginFrameAndClear();
GLStateManager.glUseProgram(program);
TessellatorEntityIdsFixture.drawIdsChangedAfterTheLastVertex();
final int[] pixels = GlsmSdlHeadlessRig.readTarget();
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, SIZE / 4, SIZE / 2, GREEN, "every vertex was added under item 7");
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, 3 * SIZE / 4, SIZE / 2, GREEN, "every vertex was added under item 7");
}
}
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -20,6 +21,7 @@ class AttachmentClearTest {
private static SdlTestRig rig;
private static ShaderManager sm;
private static PipelineStore store;
private static FullscreenPass fullscreen;
private static AttachmentClear attachmentClear;
private static final int SIZE = 32;
@@ -34,7 +36,8 @@ class AttachmentClearTest {
rig = RealDeviceExtension.rig();
sm = new ShaderManager(rig.device);
store = new PipelineStore(rig.device);
attachmentClear = new AttachmentClear(rig.frameManager, store, sm);
fullscreen = new FullscreenPass(sm, rig.device);
attachmentClear = new AttachmentClear(rig.frameManager, store, fullscreen);
final RealDeviceExtension.Target color = RealDeviceExtension.colorTarget(9700, SIZE, SIZE);
colorTexture = color.texture();
@@ -61,6 +64,14 @@ class AttachmentClearTest {
return fbo;
}
@AfterAll
static void tearDown() {
if (attachmentClear != null) attachmentClear.shutdown();
if (store != null) store.shutdown();
if (fullscreen != null) fullscreen.shutdown();
if (sm != null) sm.shutdown();
}
@Test
void onePipelinePerLayoutModeAndMask() {
final FboState fbo = makeFbo();
@@ -44,6 +44,7 @@ class DepthStencilReadbackTest {
private static FrameManager frameManager;
private static ShaderManager sm;
private static PipelineStore store;
private static FullscreenPass fullscreen;
private static FBOClearTracker clearTracker;
private static TextureOps textureOps;
private static AttachmentClear attachmentClear;
@@ -60,8 +61,9 @@ class DepthStencilReadbackTest {
store = new PipelineStore(rig.device);
clearTracker = new FBOClearTracker(frameManager, rig.resourceManager, sm);
textureOps = new TextureOps(rig.device, frameManager, rig.resourceManager, clearTracker);
attachmentClear = new AttachmentClear(frameManager, store, sm);
readback = new DepthStencilReadback(frameManager, rig.resourceManager, store, sm, clearTracker);
fullscreen = new FullscreenPass(sm, rig.device);
attachmentClear = new AttachmentClear(frameManager, store, fullscreen);
readback = new DepthStencilReadback(frameManager, rig.resourceManager, store, fullscreen, clearTracker);
st = new ContextState();
final RealDeviceExtension.Target depth = RealDeviceExtension.depthStencilTarget(9801, W, H);
@@ -77,6 +79,8 @@ class DepthStencilReadbackTest {
readback.shutdown();
}
if (store != null) store.shutdown();
if (fullscreen != null) fullscreen.shutdown();
if (sm != null) sm.shutdown();
}
private static void clearRegion(float depth, int stencil) {
@@ -0,0 +1,39 @@
package com.gtnewhorizons.angelica.sdlgpu.pipeline;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
class FullscreenPassTest {
@Test
void failedFragmentsDoNotRetryOrLeakCompilerResources() {
final Device device = new Device();
final ShaderManager shaders = new ShaderManager(device);
final FullscreenPass fullscreen = new FullscreenPass(shaders, device);
final AtomicInteger loads = new AtomicInteger();
final Map<?, ?> programs = Reflect.get(shaders, "programObjects");
final Map<?, ?> shaderObjects = Reflect.get(shaders, "shaderObjects");
try {
final FullscreenPass.Fragment fragment = fullscreen.fragment("wrong-ubo-size", 16, () -> {
loads.incrementAndGet();
return "#version 330 core\nout vec4 color;\nvoid main() { color = vec4(1.0); }\n";
});
assertEquals(0, fragment.shader());
assertEquals(0, fragment.shader());
assertEquals(1, loads.get(), "a failed variant must only be attempted once");
assertEquals(0, programs.size(), "the temporary link program must be released even on failure");
assertEquals(1, shaderObjects.size(), "only the reusable vertex shader remains");
fullscreen.shutdown();
assertEquals(0, shaderObjects.size(), "shutdown releases the shared vertex shader");
} finally {
fullscreen.shutdown();
shaders.shutdown();
}
}
}
@@ -27,7 +27,7 @@ class PerFrameBlockDedupTest {
return prog;
}
private final PipelineApplier applier = new PipelineApplier(null, null, null, null, null, null, null, null, null);
private final PipelineApplier applier = new PipelineApplier(null, null, null, null, null, null, null, null, null, null);
private final ContextState st = new ContextState();
@Test
@@ -108,7 +108,7 @@ class PerFrameBlockDedupTest {
final int[] endCount = { 0 };
rig.frameManager.setBeforeEndCopyPassCallback(() -> endCount[0]++);
final PipelineApplier realApplier = new PipelineApplier(rig.frameManager, rig.resourceManager, null, null, null, null, null, null, null);
final PipelineApplier realApplier = new PipelineApplier(rig.frameManager, rig.resourceManager, null, null, null, null, null, null, null, null);
final ContextState cs = new ContextState();
cs.boundProgramObj = programWithBlockMemberAt(16, ShaderManager.BLOCK_PER_FRAME);
realApplier.putUniform(cs, 0, new float[]{0.5f});
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.sdlgpu.pipeline;
import com.gtnewhorizons.angelica.sdlgpu.RealDeviceExtension;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
@@ -39,14 +40,6 @@ class StencilMaskGpuTest {
private static long maskPipeline;
private static long fillPipeline;
private static final String VERTEX = String.join("\n",
"#version 460 core",
"void main() {",
" vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);",
" gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);",
"}",
"");
private static final String MASK_FRAGMENT = String.join("\n",
"#version 460 core",
"layout(location = 0) out vec4 fragColor;",
@@ -85,7 +78,7 @@ class StencilMaskGpuTest {
private static long createPipeline(String fragmentSource, boolean writesMask) {
final int vs = sm.createShader(GL20.GL_VERTEX_SHADER);
sm.shaderSource(vs, VERTEX);
sm.shaderSource(vs, GlsmSdlHeadlessRig.FULLSCREEN_VERTEX);
sm.compileShader(vs);
final int fs = sm.createShader(GL20.GL_FRAGMENT_SHADER);
sm.shaderSource(fs, fragmentSource);
@@ -0,0 +1,55 @@
package com.gtnewhorizons.angelica.sdlgpu.sampler;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
class SamplerLookupTest {
@ParameterizedTest
@ValueSource(booleans = {false, true})
void movedAndUnmovedSamplersShareDefaultsAndExplicitAssignments(boolean fragment) {
final ShaderManager.ProgramObject ordinary = program(fragment,
List.of("tex", "lightmap", "normals", "specular", "custom"), List.of(), new boolean[0]);
final ShaderManager.ProgramObject overflow = program(fragment, List.of("tex"),
List.of("storageImage", "custom", "specular", "lightmap", "normals"), new boolean[]{false, true, true, true, true});
assertArrayEquals(new int[]{0, 0, 0, 0, 0}, SamplerLookup.resolvedUnits(ordinary, fragment));
assertArrayEquals(new int[]{0}, SamplerLookup.resolvedUnits(overflow, fragment));
assertArrayEquals(new int[]{-1, 0, 0, 0, 0}, SamplerLookup.movedSamplerUnits(overflow, fragment),
"moving a sampler must preserve its default, while real storage images do not resolve sampler units");
for (ShaderManager.ProgramObject prog : List.of(ordinary, overflow)) {
prog.samplerTextureUnits.put("lightmap", 1);
prog.samplerTextureUnits.put("normals", 2);
prog.samplerTextureUnits.put("specular", 3);
prog.samplerTextureUnits.put("custom", 9);
prog.samplerUnitsDirty = true;
}
assertArrayEquals(new int[]{0, 1, 2, 3, 9}, SamplerLookup.resolvedUnits(ordinary, fragment));
assertArrayEquals(new int[]{-1, 9, 3, 1, 2}, SamplerLookup.movedSamplerUnits(overflow, fragment));
}
private static ShaderManager.ProgramObject program(boolean fragment, List<String> samplers, List<String> images, boolean[] moved) {
final ShaderManager.ProgramObject prog = new ShaderManager.ProgramObject();
final ShaderManager.ResourceCounts resources = new ShaderManager.ResourceCounts(samplers.size(), 0, 0, images.size());
if (fragment) {
prog.fragmentResources = resources;
prog.fragmentSamplerNames = samplers;
prog.fragmentImageNames = images;
prog.fragmentImageIsMovedSampler = moved;
prog.fragmentMovedSamplerRead = moved;
} else {
prog.vertexResources = resources;
prog.vertexSamplerNames = samplers;
prog.vertexImageNames = images;
prog.vertexImageIsMovedSampler = moved;
prog.vertexMovedSamplerRead = moved;
}
prog.samplerUnitsDirty = true;
return prog;
}
}
@@ -62,11 +62,13 @@ class FontFilterVertexSamplerTest {
}
@Test
void samplerNamedLightmapResolvesToTextureUnitOne() {
void lightmapStartsOnUnitZeroAndHonorsTheFontsExplicitAssignment() {
final List<String> samplers = reflect(runSdlPipeline(FONT_FILTER_VSH)).samplerNames();
final Object2IntOpenHashMap<String> noExplicitUnits = new Object2IntOpenHashMap<>();
noExplicitUnits.defaultReturnValue(-1);
final Object2IntOpenHashMap<String> units = new Object2IntOpenHashMap<>();
units.defaultReturnValue(-1);
assertEquals(1, SamplerLookup.getSamplerTextureUnit(samplers, noExplicitUnits, 0), "even without a glUniform1i the SDL fallback must put a sampler named 'lightmap' on unit 1");
assertEquals(0, SamplerLookup.getSamplerTextureUnit(samplers, units, 0), "all samplers start on unit 0");
units.put("lightmap", 1);
assertEquals(1, SamplerLookup.getSamplerTextureUnit(samplers, units, 0), "the font renderer assigns lightmap explicitly");
}
}
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.sdlgpu.shader;
import com.gtnewhorizons.angelica.sdlgpu.RealDeviceExtension;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -47,14 +48,6 @@ class FragmentStorageTextureReadGpuTest {
sm = new ShaderManager(rig.device);
}
private static final String VERTEX = String.join("\n",
"#version 460 core",
"void main() {",
" vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);",
" gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);",
"}",
"");
private static String fragment(int samplerCount) {
final StringBuilder sb = new StringBuilder();
sb.append("#version 460 core\n");
@@ -103,7 +96,7 @@ class FragmentStorageTextureReadGpuTest {
private int linkProgram(int samplerCount) {
final int vs = sm.createShader(GL20.GL_VERTEX_SHADER);
sm.shaderSource(vs, VERTEX);
sm.shaderSource(vs, GlsmSdlHeadlessRig.FULLSCREEN_VERTEX);
sm.compileShader(vs);
final int fs = sm.createShader(GL20.GL_FRAGMENT_SHADER);
sm.shaderSource(fs, fragment(samplerCount));
@@ -23,7 +23,7 @@ class SDLGPUUniformDedupTest {
@BeforeEach
void setUp() {
applier = new PipelineApplier(null, null, null, null, null, null, null, null, null);
applier = new PipelineApplier(null, null, null, null, null, null, null, null, null, null);
st = new ContextState();
prog = new ShaderManager.ProgramObject();
prog.linked = true;
@@ -36,7 +36,7 @@ final class ShaderTransformChain {
static String stripUnused(String source) {
if (!source.contains("sampler")) return source;
return edit(source, SamplerStripper::collectEdits);
return edit(source, (index, edits) -> SamplerStripper.collectEdits(index, TextureDeclarations.collect(index), edits));
}
static String inject(String source, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
@@ -38,6 +38,7 @@ public final class ImmediateExtendedAttribs implements ImmediateExtendedAttribHa
private static final int WANTS_MID = 1;
private static final int WANTS_TANGENT = 2;
private static final int WANTS_ENTITY_IDS = 4;
private static final int NEUTRAL_TANGENT = NormI8.pack(1.0f, 0.0f, 0.0f, 1.0f);
@@ -57,6 +58,7 @@ public final class ImmediateExtendedAttribs implements ImmediateExtendedAttribHa
wants = 0;
if (GLStateManager.glGetAttribLocation(program, "mc_midTexCoord") >= 0) wants |= WANTS_MID;
if (GLStateManager.glGetAttribLocation(program, "at_tangent") >= 0) wants |= WANTS_TANGENT;
if (GLStateManager.glGetAttribLocation(program, "iris_Entity") == LOC_ENTITY_IDS) wants |= WANTS_ENTITY_IDS;
programWantsExtCache.put(program, wants);
}
currentProgramWants = wants;
@@ -83,8 +85,7 @@ public final class ImmediateExtendedAttribs implements ImmediateExtendedAttribHa
@Override
public boolean wantsExtended() {
GLSMHooks.resolvePendingProgram();
return Iris.enabled && currentProgramWants != 0 && GLStateManager.getActiveProgram() != 0;
return programWants(WANTS_MID | WANTS_TANGENT);
}
@Override
@@ -92,6 +93,17 @@ public final class ImmediateExtendedAttribs implements ImmediateExtendedAttribHa
return (currentProgramWants & WANTS_TANGENT) != 0;
}
@Override
public boolean wantsEntityIds() {
return programWants(WANTS_ENTITY_IDS);
}
private static boolean programWants(int mask) {
if (!Iris.enabled) return false;
GLSMHooks.resolvePendingProgram();
return (currentProgramWants & mask) != 0 && GLStateManager.getActiveProgram() != 0;
}
@Override
public boolean wantsExtendedCapture() {
return Iris.enabled && Iris.getCurrentPack().isPresent();
@@ -26,6 +26,7 @@ import net.coderbot.iris.pipeline.DeferredWorldRenderingPipeline;
import net.coderbot.iris.pipeline.WorldRenderingPipeline;
import net.coderbot.iris.samplers.IrisSamplers;
import net.coderbot.iris.texture.pbr.PBRTextureManager;
import net.coderbot.iris.uniforms.CapturedRenderingState;
import net.coderbot.iris.uniforms.SystemTimeUniforms;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import org.lwjgl.opengl.GL11;
@@ -65,6 +66,9 @@ public class IrisGLSMBridge {
public static void installImmediateExtendedHandler() {
GLSMHooks.immediateExtendedHandler = new ImmediateExtendedAttribs();
final CapturedRenderingState state = CapturedRenderingState.INSTANCE;
state.setEntityIdsListener(() -> GLStateManager.setEntityIds(
state.getCurrentRenderedEntity(), state.getCurrentRenderedBlockEntity(), state.getCurrentRenderedItem()));
}
public static void installPostTransformHook() {
@@ -72,8 +72,7 @@ public final class AngelicaClientTweaker implements IFMLLoadingPlugin, IEarlyMix
// Angelica Config
ConfigurationManager.registerConfig(AngelicaConfig.class);
TracyOptions.latch(AngelicaConfig.enableTracy, AngelicaConfig.tracyAllowRemote, AngelicaConfig.tracyFineZones, AngelicaConfig.tracyMaxSrcLocs);
if (BackendStartGuard.begin(new File(Launch.minecraftHome, "angelica" + File.separator + "sdlgpu-starting"), AngelicaConfig.sdlGpuConfigured(), SystemProperties.SDL_GPU_OVERRIDE != null)) {
LOGGER.warn("The last start with SDL GPU did not reach the main menu; resetting the render backend to OpenGL");
if (BackendStartGuard.begin(new File(Launch.minecraftHome, "angelica"), AngelicaConfig.sdlGpuConfigured(), SystemProperties.SDL_GPU_OVERRIDE != null, SDLGPUGate.REQUIREMENTS_REVISION, BackendOptions.sdlGpuDriver(AngelicaConfig.sdlGpuDriverName()))) {
AngelicaConfig.renderBackend = RenderBackendChoice.OPENGL;
ConfigurationManager.save(AngelicaConfig.class);
}
@@ -199,6 +199,7 @@ public enum Mixins implements IMixins {
"angelica.entity.MixinRendererLivingEntity_GlintClock",
"angelica.entity.MixinRendererLivingEntity_ModelPassDraws",
"angelica.entity.MixinRender_BatchEligibility",
"angelica.entity.MixinRender_NameTagDraws",
"angelica.entity.MixinRender_ShadowBatch",
"angelica.entity.MixinTextureManager",
"angelica.tesr.MixinTileEntitySpecialRenderer_BatchEligibility"
@@ -61,6 +61,7 @@ import com.gtnewhorizons.angelica.debug.TPSGraph;
import com.gtnewhorizons.angelica.dynamiclights.DynamicLights;
import com.gtnewhorizons.angelica.dynamiclights.config.EntityLightConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.BackendStartGuard;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
@@ -383,7 +384,7 @@ public final class ClientProxy extends CommonProxy {
@SubscribeEvent(priority = EventPriority.LOWEST)
public void onGuiOpen(GuiOpenEvent event) {
BackendStartGuard.finish();
if (BackendManager.RENDER_BACKEND.isSDLGPU()) BackendStartGuard.latch();
if (!event.isCanceled()) {
if (event.gui instanceof GuiMainMenu && gameStartTime == -1) {
gameStartTime = ManagementFactory.getRuntimeMXBean().getUptime() / 1000f;
@@ -10,7 +10,6 @@ import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import com.gtnewhorizons.angelica.mixins.interfaces.ModelBoxQuads;
import com.gtnewhorizons.angelica.rendering.tesr.PassRebindGate;
import lombok.Getter;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gl.image.GlImage;
@@ -211,8 +210,8 @@ public final class PlayerReflectionCapture {
final float[] xyzuv = vertexScratch;
if (xyzuv == null || xyzuv.length < VERTEX_COUNT * FLOATS_PER_VERTEX) return;
PassRebindGate.rebindIfDirty();
if (!(Iris.getPipelineManager().getPipelineNullable() instanceof DeferredWorldRenderingPipeline pipeline)) return;
pipeline.rebindCurrentPass();
if (GLStateManager.getActiveProgram() != pipeline.getActivePassProgramId()) return;
if (Tracy.ENABLED) Tracy.beginZone(Z_PLAYER_REFLECTION_FLUSH);
@@ -41,6 +41,12 @@ public final class GpuDrivenChunkCuller {
private int metaCapacityBytes;
private int savedProgram;
private int savedVisibleSsbo;
private int savedMetaSsbo;
private int savedIndirectSsbo;
private int savedFrustumUbo;
public boolean ensureReady() {
if (ready) return true;
if (disabled) return false;
@@ -88,28 +94,31 @@ public final class GpuDrivenChunkCuller {
GLStateManager.glBufferSubData(GL31.GL_UNIFORM_BUFFER, 0L, frustumUboBytes);
}
public void dispatch(int visibleSsboGlId, int indirectSsboGlId, int visibleCount) {
if (!ready || visibleCount <= 0) return;
final int previousProgram = GLStateManager.getActiveProgram();
final int prevSsbo0 = RenderSystem.getIndexedBufferBinding(GL43.GL_SHADER_STORAGE_BUFFER, VISIBLE_SSBO_BINDING);
final int prevSsbo1 = RenderSystem.getIndexedBufferBinding(GL43.GL_SHADER_STORAGE_BUFFER, META_SSBO_BINDING);
final int prevSsbo2 = RenderSystem.getIndexedBufferBinding(GL43.GL_SHADER_STORAGE_BUFFER, INDIRECT_SSBO_BINDING);
final int prevUbo = RenderSystem.getIndexedBufferBinding(GL31.GL_UNIFORM_BUFFER, FRUSTUM_UBO_BINDING);
void beginDispatches(int visibleSsboGlId, int indirectSsboGlId) {
savedProgram = GLStateManager.getActiveProgram();
savedVisibleSsbo = RenderSystem.getIndexedBufferBinding(GL43.GL_SHADER_STORAGE_BUFFER, VISIBLE_SSBO_BINDING);
savedMetaSsbo = RenderSystem.getIndexedBufferBinding(GL43.GL_SHADER_STORAGE_BUFFER, META_SSBO_BINDING);
savedIndirectSsbo = RenderSystem.getIndexedBufferBinding(GL43.GL_SHADER_STORAGE_BUFFER, INDIRECT_SSBO_BINDING);
savedFrustumUbo = RenderSystem.getIndexedBufferBinding(GL31.GL_UNIFORM_BUFFER, FRUSTUM_UBO_BINDING);
GLStateManager.glUseProgram(program.handle());
RenderSystem.bindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, VISIBLE_SSBO_BINDING, visibleSsboGlId);
RenderSystem.bindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, META_SSBO_BINDING, metaSsboGlId);
RenderSystem.bindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, INDIRECT_SSBO_BINDING, indirectSsboGlId);
RenderSystem.bindBufferBase(GL31.GL_UNIFORM_BUFFER, FRUSTUM_UBO_BINDING, frustumUboGlId);
}
void dispatch(int visibleCount) {
final int groups = (visibleCount + LOCAL_SIZE_X - 1) / LOCAL_SIZE_X;
GLStateManager.glDispatchCompute(groups, 1, 1);
GLStateManager.glMemoryBarrier(GL43.GL_SHADER_STORAGE_BARRIER_BIT | GL42.GL_COMMAND_BARRIER_BIT);
}
RenderSystem.bindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, VISIBLE_SSBO_BINDING, prevSsbo0);
RenderSystem.bindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, META_SSBO_BINDING, prevSsbo1);
RenderSystem.bindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, INDIRECT_SSBO_BINDING, prevSsbo2);
RenderSystem.bindBufferBase(GL31.GL_UNIFORM_BUFFER, FRUSTUM_UBO_BINDING, prevUbo);
GLStateManager.glUseProgram(previousProgram);
void endDispatches() {
GLStateManager.glMemoryBarrier(GL43.GL_SHADER_STORAGE_BARRIER_BIT | GL42.GL_COMMAND_BARRIER_BIT);
RenderSystem.bindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, VISIBLE_SSBO_BINDING, savedVisibleSsbo);
RenderSystem.bindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, META_SSBO_BINDING, savedMetaSsbo);
RenderSystem.bindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, INDIRECT_SSBO_BINDING, savedIndirectSsbo);
RenderSystem.bindBufferBase(GL31.GL_UNIFORM_BUFFER, FRUSTUM_UBO_BINDING, savedFrustumUbo);
GLStateManager.glUseProgram(savedProgram);
}
public synchronized void shutdown() {
@@ -261,12 +261,14 @@ public final class GpuTerrainCuller {
GLStateManager.glBindBuffer(GL43.GL_SHADER_STORAGE_BUFFER, visibleSsboGlId);
GLStateManager.glBufferSubData(GL43.GL_SHADER_STORAGE_BUFFER, 0L, visibleStagingView);
culler.beginDispatches(visibleSsboGlId, indirectSsboGlId);
BackendManager.RENDER_BACKEND.beginComputeDispatchBatch();
try {
dispatchPass(primaryPass);
if (secondPrepared) dispatchPass(secondPass);
if (translucentReady) dispatchPass(sortedPass);
} finally {
culler.endDispatches();
BackendManager.RENDER_BACKEND.endComputeDispatchBatch();
}
}
@@ -280,7 +282,7 @@ public final class GpuTerrainCuller {
FrustumExtractor.patchControl(r.entryCount, r.indexPointerMask, frustumUboBytes);
FrustumExtractor.patchBatchEntryBase(r.entryBase, frustumUboBytes);
culler.uploadFrustum(frustumUboBytes);
culler.dispatch(visibleSsboGlId, indirectSsboGlId, r.entryCount);
culler.dispatch(r.entryCount);
r.dispatched = true;
}
@@ -16,6 +16,8 @@ import com.gtnewhorizons.angelica.glsm.ffp.CubeParams;
import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorEntityIds;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
import com.gtnewhorizons.angelica.glsm.states.AlphaState;
import com.gtnewhorizons.angelica.glsm.states.BlendState;
import com.gtnewhorizons.angelica.glsm.states.Color4;
@@ -624,6 +626,53 @@ public final class ModelPartBatcher {
return true;
}
private enum TessellatorRoute { DRAW_NOW, DRAW_NOW_EXPECTED, QUEUE }
public static int drawTessellator(Tessellator tess) {
switch (INSTANCE.routeTessellatorDraw(tess)) {
case QUEUE -> {
tess.isDrawing = false;
final int result = tess.rawBufferIndex * 4;
tess.reset();
return result;
}
case DRAW_NOW_EXPECTED -> {
BatchEligibility.beginExpectedDraws(GLStateManager.drawCalls);
try {
return TessellatorStreamingDrawer.draw(tess);
} finally {
BatchEligibility.endExpectedDraws(GLStateManager.drawCalls);
}
}
default -> {
return TessellatorStreamingDrawer.draw(tess);
}
}
}
private TessellatorRoute routeTessellatorDraw(Tessellator tess) {
if (!active || mode != Mode.BLOCK_ENTITIES || !GLStateManager.isMainThread() || !BatchEligibility.insideRenderer()) return TessellatorRoute.DRAW_NOW;
if (!tess.isDrawing || tess.drawMode != GL11.GL_QUADS || tess.vertexCount == 0 || (tess.vertexCount & 3) != 0) return TessellatorRoute.DRAW_NOW;
if (shaderPipeline != null && TessellatorEntityIds.hasIdChanges(tess)) return TessellatorRoute.DRAW_NOW;
if (DisplayListManager.isRecording() || !GLStateManager.glIsEnabled(GL11.GL_DEPTH_TEST) || !unitTexIdentity()) return TessellatorRoute.DRAW_NOW;
if (GLStateManager.getActiveProgram() != (shaderPipeline != null ? shaderPipeline.getActivePassProgramId() : 0)) return TessellatorRoute.DRAW_NOW;
final TesrMaterial material = EntityMaterials.fromCurrentState(true);
if (material == null || material == EntityMaterials.OVERLAY) return TessellatorRoute.DRAW_NOW;
if (lastBoundLocation == null || GLStateManager.getBoundTextureForServerState() != lastBoundGlId) return TessellatorRoute.DRAW_NOW;
if (!BatchEligibility.batchingAllowed()) return TessellatorRoute.DRAW_NOW_EXPECTED;
final PassOverride pass = shaderPipeline != null ? PassOverride.capture() : PassOverride.NONE;
final boolean offset = GLStateManager.glIsEnabled(GL11.GL_POLYGON_OFFSET_FILL);
final PolygonState polygon = GLStateManager.getPolygonState();
final int cullCode = material.isNoCull() ? DrawState.DISABLED : DrawState.liveCull();
final RenderLayer layer = layerFor(lastBoundLocation, material, pass, offset ? polygon.getOffsetFactor() : 0.0f, offset ? polygon.getOffsetUnits() : 0.0f,
ShaderGlint.NO_TINT, cullCode, DrawState.liveLit(material), DrawState.liveLightmap());
final int groupId = groupId(shaderPipeline != null, pass.isEntityPhase(), CapturedRenderingState.INSTANCE.getCurrentRenderedEntity(), CapturedRenderingState.INSTANCE.getCurrentRenderedBlockEntity());
TessellatorStreamingDrawer.fillMissingAttributes(tess);
activeGroups.streamVertices(layer, groupId, tess.rawBuffer, tess.vertexCount, currentModelView());
return TessellatorRoute.QUEUE;
}
private final ReferenceOpenHashSet<Class<?>> loggedMaterialBails = new ReferenceOpenHashSet<>();
private final BlendState bailBlend = new BlendState();
private final AlphaState bailAlpha = new AlphaState();
@@ -470,6 +470,12 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
promote(groupFor(layer, material, blockEntityId));
}
void streamVertices(RenderLayer layer, int id, int[] packed, int vertexCount, Matrix4fc mv) {
if (glintCapture != null) glintCapture.invalidate();
source.getBuffer(layer, id).addPackedVertices(packed, vertexCount, mv, scratchVec);
streamedInstances++;
}
void queue(TemplateBuffer template, RenderLayer layer, TesrMaterial material, Matrix4f currentMV, int packedLight, int colorABGR, int overlayABGR, long entityInfo, int id, Matrix4f texMatrix) {
queue(template, null, 1.0f, layer, material, currentMV, packedLight, colorABGR, overlayABGR, entityInfo, id, texMatrix);
}
@@ -32,30 +32,47 @@ public class InterpolatedIcon extends TextureAtlasSprite {
protected void updateAnimationInterpolated() throws IllegalArgumentException, IllegalAccessException {
double d0 = 1.0D - tickCounter / (double) animationMetadata.getFrameTimeSingle(frameCounter);
int i = animationMetadata.getFrameIndex(frameCounter);
int j = animationMetadata.getFrameCount() == 0 ? framesTextureData.size() : animationMetadata.getFrameCount();
int k = animationMetadata.getFrameIndex((frameCounter + 1) % j);
final double dn = tickCounter / (double) animationMetadata.getFrameTimeSingle(frameCounter);
final double dc = 1.0D - dn;
final int cFrame = animationMetadata.getFrameIndex(frameCounter);
final int endFrame = animationMetadata.getFrameCount() == 0 ? framesTextureData.size() : animationMetadata.getFrameCount();
final int nFrame = animationMetadata.getFrameIndex((frameCounter + 1) % endFrame);
if (i != k && k >= 0 && k < framesTextureData.size()) {
int[][] aint = (int[][]) framesTextureData.get(i);
int[][] aint1 = (int[][]) framesTextureData.get(k);
if (cFrame != nFrame && nFrame >= 0 && nFrame < framesTextureData.size()) {
final int[][] cARGB = framesTextureData.get(cFrame);
final int[][] nARGB = framesTextureData.get(nFrame);
if (interpolatedFrameData == null || interpolatedFrameData.length != aint.length)
interpolatedFrameData = new int[aint.length][];
if (interpolatedFrameData == null || interpolatedFrameData.length != cARGB.length)
interpolatedFrameData = new int[cARGB.length][];
for (int l = 0; l < aint.length; l++) {
if (interpolatedFrameData[l] == null)
interpolatedFrameData[l] = new int[aint[l].length];
for (int i = 0; i < cARGB.length; i++) {
if (interpolatedFrameData[i] == null)
interpolatedFrameData[i] = new int[cARGB[i].length];
if (l < aint1.length && aint1[l].length == aint[l].length)
for (int i1 = 0; i1 < aint[l].length; ++i1) {
int j1 = aint[l][i1];
int k1 = aint1[l][i1];
int l1 = (int) (((j1 & 16711680) >> 16) * d0 + ((k1 & 16711680) >> 16) * (1.0D - d0));
int i2 = (int) (((j1 & 65280) >> 8) * d0 + ((k1 & 65280) >> 8) * (1.0D - d0));
int j2 = (int) ((j1 & 255) * d0 + (k1 & 255) * (1.0D - d0));
interpolatedFrameData[l][i1] = j1 & -16777216 | l1 << 16 | i2 << 8 | j2;
if (i < nARGB.length && nARGB[i].length == cARGB[i].length)
for (int j = 0; j < cARGB[i].length; ++j) {
final int c = cARGB[i][j];
final int n = nARGB[i][j];
final double ca = (c >>> 24) * dc;
final int cr = (c >>> 16) & 0xFF;
final int cg = (c >>> 8) & 0xFF;
final int cb = c & 0xFF;
final double na = (n >>> 24) * dn;
final int nr = (n >>> 16) & 0xFF;
final int ng = (n >>> 8) & 0xFF;
final int nb = n & 0xFF;
final double ad = ca + na;
final double wc = ad == 0 ? dc : ca / ad;
final double wn = ad == 0 ? dn : na / ad;
final int a = (int) Math.round(ad);
final int r = (int) Math.round(cr * wc + nr * wn);
final int g = (int) Math.round(cg * wc + ng * wn);
final int b = (int) Math.round(cb * wc + nb * wn);
interpolatedFrameData[i][j] = a << 24 | r << 16 | g << 8 | b;
}
}
@@ -5,6 +5,7 @@ package net.coderbot.iris.gl.shader;
import com.gtnewhorizons.angelica.glsm.GLDebug;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs;
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
import com.gtnewhorizons.angelica.glsm.shader.ProgramBinaryCache;
import org.apache.logging.log4j.LogManager;
@@ -24,8 +25,8 @@ public class ProgramCreator {
public static final int AT_MIDBLOCK = ImmediateExtendedAttribHandler.LOC_MIDBLOCK;
// TODO: This is *really* hardcoded, we need to refactor this to support external calls to glBindAttribLocation
private static final String[] ATTRIBUTE_NAMES = { "iris_Entity", "mc_Entity", "mc_midTexCoord", "iris_CubeTangent", "at_tangent", "at_midBlock" };
private static final int[] ATTRIBUTE_LOCATIONS = { MC_ENTITY, MC_ENTITY, MC_MID_TEX_COORD, AT_TANGENT, AT_TANGENT, AT_MIDBLOCK };
private static final String[] ATTRIBUTE_NAMES = { "iris_InstEntity", "mc_Entity", "mc_midTexCoord", "iris_CubeTangent", "at_tangent", "at_midBlock" };
private static final int[] ATTRIBUTE_LOCATIONS = { InstancedAttribs.LOC_ENTITY, MC_ENTITY, MC_MID_TEX_COORD, AT_TANGENT, AT_TANGENT, AT_MIDBLOCK };
@Nullable
public static ProgramBinaryCache.Key cacheKey() {
@@ -617,8 +617,8 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
() -> usesSampler(IrisSamplers.USAGE_SHADOWTEX1));
Program shadowProgram = table.match(RenderCondition.SHADOW, SHADOW_INPUTS).getProgram();
Program shadowWaterProgram = table.match(RenderCondition.SHADOW_TRANSLUCENT, SHADOW_INPUTS).getProgram();
shadowRenderer.setUsesImages((shadowProgram != null && shadowProgram.getActiveImages() > 0) || (shadowWaterProgram != null && shadowWaterProgram.getActiveImages() > 0));
shadowRenderer.setPlayerReflectionCaptureEnabled((shadowProgram != null && GLStateManager.glGetUniformLocation(shadowProgram.getProgramId(), "playerAtlas_img") != -1) || (shadowWaterProgram != null && GLStateManager.glGetUniformLocation(shadowWaterProgram.getProgramId(), "playerAtlas_img") != -1));
shadowRenderer.setUsesImages(usesImages(shadowProgram) || usesImages(shadowWaterProgram));
shadowRenderer.setPlayerReflectionCaptureEnabled(usesImage(shadowProgram, "playerAtlas_img") || usesImage(shadowWaterProgram, "playerAtlas_img"));
} else {
shadowRenderer = null;
}
@@ -1591,6 +1591,14 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
return supportsInstancing[kind.ordinal()];
}
private boolean usesImages(@Nullable Program program) {
return program != null && (program.getActiveImages() > 0 || vertexWriteReplays.writesImages(program));
}
private boolean usesImage(@Nullable Program program, String image) {
return program != null && (GLStateManager.glGetUniformLocation(program.getProgramId(), image) != -1 || vertexWriteReplays.writesImage(program, image));
}
@Override
public boolean hasInstancedVariant(Instancing kind) {
return current != null && current.instancedVariant(kind) != null;
@@ -5,7 +5,9 @@ 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 it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntIterator;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gl.image.ImageHolder;
import net.coderbot.iris.gl.program.ComputeProgram;
@@ -33,10 +35,20 @@ import java.util.function.IntSupplier;
final class VertexWriteReplays {
private final List<ComputeProgram> twins = new ArrayList<>();
private final IntArrayList attachedPrograms = new IntArrayList();
private final Int2ObjectMap<List<String>> writtenImagesByProgram = new Int2ObjectOpenHashMap<>();
boolean writesImage(Program program, String image) {
final List<String> images = writtenImagesByProgram.get(program.getProgramId());
return images != null && images.contains(image);
}
boolean writesImages(Program program) {
return writtenImagesByProgram.containsKey(program.getProgramId());
}
void attach(String name, Program program, Map<PatchShaderType, String> transformed, BiConsumer<SamplerHolder, ImageHolder> wireResources) {
BackendManager.RENDER_BACKEND.setVertexWriteReplay(program.getProgramId(), null);
writtenImagesByProgram.remove(program.getProgramId());
final String source = transformed.get(PatchShaderType.COMPUTE);
if (RwImageStoreExtractor.parseSentinel(source) != RwImageStoreExtractor.RwExtractMode.VERTEX_REPLAY) return;
@@ -76,13 +88,13 @@ final class VertexWriteReplays {
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());
writtenImagesByProgram.put(program.getProgramId(), writtenImages);
twins.add(twin);
}
void destroy() {
for (int i = 0; i < attachedPrograms.size(); i++) BackendManager.RENDER_BACKEND.setVertexWriteReplay(attachedPrograms.getInt(i), null);
attachedPrograms.clear();
for (IntIterator it = writtenImagesByProgram.keySet().iterator(); it.hasNext(); ) BackendManager.RENDER_BACKEND.setVertexWriteReplay(it.nextInt(), null);
writtenImagesByProgram.clear();
for (ComputeProgram twin : twins) twin.destroy();
twins.clear();
}
@@ -1,24 +1,21 @@
package net.coderbot.iris.pipeline.transform;
import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs;
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
import com.gtnewhorizons.angelica.glsm.shader.ShaderIndex;
import com.gtnewhorizons.angelica.glsm.shader.ShaderType;
import net.coderbot.iris.pipeline.transform.parameter.AttributeParameters;
import org.antlr.v4.runtime.CommonToken;
import org.antlr.v4.runtime.ParserRuleContext;
import org.taumc.glsl.Transformer;
import org.taumc.glsl.grammar.GLSLParser;
class EntityPatcher {
private static final String[] ID_NAMES = { "entityId", "blockEntityId", "currentRenderedItemId" };
private static final String[] ID_COMPONENTS = { ".x", ".y", ".z" };
public static void patchEntityId(Transformer transformer, AttributeParameters parameters) {
if (!hasInstanceAttributes(parameters)) {
injectUniformIfUndeclared(transformer, "entityId", "uniform int entityId;");
injectUniformIfUndeclared(transformer, "blockEntityId", "uniform int blockEntityId;");
injectUniformIfUndeclared(transformer, "currentRenderedItemId", "uniform int currentRenderedItemId;");
return;
}
removeIfDeclared(transformer, "entityId");
removeIfDeclared(transformer, "blockEntityId");
removeIfDeclared(transformer, "currentRenderedItemId");
final String geometryIn = parameters.hasTesselation ? "iris_entityInfoTES" : "iris_entityInfo";
final String info = switch (parameters.type) {
case GEOMETRY -> geometryIn + "[0]";
@@ -26,17 +23,37 @@ class EntityPatcher {
case TESSELATION_EVAL -> "iris_entityInfoTCS[0]";
default -> "iris_entityInfo";
};
transformer.replaceExpression("entityId", info + ".x", GLSLParser::postfix_expression);
transformer.replaceExpression("blockEntityId", info + ".y", GLSLParser::postfix_expression);
transformer.replaceExpression("currentRenderedItemId", info + ".z", GLSLParser::postfix_expression);
final StringBuilder assignments = new StringBuilder();
for (int i = 0; i < ID_NAMES.length; i++) {
final String name = ID_NAMES[i];
if (!transformer.containsCall(name)) continue;
final GLSLParser.Typeless_declarationContext global = topLevelDeclarator(transformer, name);
if (global != null) {
if (!isUniform(global)) continue;
// removeVariable takes the last declaration it finds by name, which can be a pack local
final String removed = "iris_removed_" + name;
((CommonToken) global.IDENTIFIER().getSymbol()).setText(removed);
transformer.removeVariable(removed);
}
transformer.injectVariable("int " + name + ";");
assignments.append(name).append(" = ").append(info).append(ID_COMPONENTS[i]).append("; ");
}
if (parameters.type != ShaderType.VERTEX && assignments.length() > 0) {
transformer.prependMain("{ " + assignments + "}");
}
switch (parameters.type) {
case VERTEX -> {
final boolean instanced = hasInstanceAttributes(parameters);
final String input = instanced ? "iris_InstEntity" : "iris_Entity";
final int location = instanced ? InstancedAttribs.LOC_ENTITY : ImmediateExtendedAttribHandler.LOC_ENTITY_IDS;
removeIfDeclared(transformer, "iris_Entity");
transformer.injectVariable("layout(location = " + InstancedAttribs.LOC_ENTITY + ") in uvec4 iris_Entity;");
removeIfDeclared(transformer, "iris_InstEntity");
transformer.injectVariable("layout(location = " + location + ") in vec4 " + input + ";");
transformer.injectVariable("flat out ivec3 iris_entityInfo;");
transformer.prependMain("iris_entityInfo = ivec3(iris_Entity.x == 65535u ? -1 : int(iris_Entity.x), "
+ "iris_Entity.y == 65535u ? -1 : int(iris_Entity.y), iris_Entity.z == 65535u ? -1 : int(iris_Entity.z));");
transformer.prependMain("{ ivec3 iris_ids = ivec3(round(" + input + ".xyz * 65535.0)); "
+ "iris_entityInfo = ivec3(iris_ids.x == 65535 ? -1 : iris_ids.x, iris_ids.y == 65535 ? -1 : iris_ids.y, "
+ "iris_ids.z == 65535 ? -1 : iris_ids.z); " + assignments + "}");
}
case TESSELATION_CONTROL -> {
transformer.injectVariable("flat out ivec3 iris_entityInfoTCS[];");
@@ -113,6 +130,27 @@ class EntityPatcher {
}
}
private static GLSLParser.Typeless_declarationContext topLevelDeclarator(Transformer transformer, String name) {
final GLSLParser.Typeless_declarationContext[] found = new GLSLParser.Typeless_declarationContext[1];
transformer.mutateTree(root -> {
for (GLSLParser.External_declarationContext external : root.external_declaration()) {
final GLSLParser.DeclarationContext declaration = external.declaration();
if (declaration == null || declaration.init_declarator_list() == null) continue;
for (GLSLParser.Typeless_declarationContext declarator : ShaderIndex.declarators(declaration.init_declarator_list())) {
if (declarator.IDENTIFIER().getText().equals(name)) found[0] = declarator;
}
}
});
return found[0];
}
private static boolean isUniform(GLSLParser.Typeless_declarationContext declarator) {
final ParserRuleContext parent = declarator.getParent();
final GLSLParser.Init_declarator_listContext list = (GLSLParser.Init_declarator_listContext)
(parent instanceof GLSLParser.Single_declarationContext ? parent.getParent() : parent);
return ShaderIndex.hasStorageQualifier(list.single_declaration().fully_specified_type().type_qualifier(), "uniform");
}
private static boolean hasInstanceAttributes(AttributeParameters parameters) {
return parameters.instancing.hasInstanceHead();
}
@@ -11,7 +11,7 @@ final class SamplerAliasDeduplicator {
private record AliasGroup(String canonical, List<String> aliases) {}
private static final List<AliasGroup> GROUPS = List.of(
new AliasGroup("tex", List.of("texture", "gtexture", "textureAtlas")),
new AliasGroup("tex", List.of("texture", "gtexture")),
new AliasGroup("depthtex0", List.of("gdepthtex")),
new AliasGroup("dhDepthTex0", List.of("dhDepthTex"))
);
@@ -35,6 +35,7 @@ public class CapturedRenderingState {
@Getter
private int currentRenderedItem = -1;
private Runnable itemIdListener = null;
private Runnable entityIdsListener = null;
private final IntArrayList itemStack = new IntArrayList();
private final IntArrayList entityStack = new IntArrayList();
private final IntArrayList blockEntityStack = new IntArrayList();
@@ -88,15 +89,25 @@ public class CapturedRenderingState {
if (this.blockEntityIdListener != null) {
this.blockEntityIdListener.run();
}
notifyEntityIdsChanged();
}
private void setCurrentEntity(int entity) {
if (currentRenderedEntity == entity) return;
this.currentRenderedEntity = entity;
if (this.entityIdListener != null) {
this.entityIdListener.run();
private void applyCurrentEntityAndItem(int entity, int item) {
final boolean entityChanged = currentRenderedEntity != entity;
final boolean itemChanged = currentRenderedItem != item;
if (entityChanged) {
this.currentRenderedEntity = entity;
if (this.entityIdListener != null) {
this.entityIdListener.run();
}
}
if (itemChanged) {
this.currentRenderedItem = item;
if (this.itemIdListener != null) {
this.itemIdListener.run();
}
}
if (entityChanged || itemChanged) notifyEntityIdsChanged();
}
/**
@@ -104,15 +115,13 @@ public class CapturedRenderingState {
*/
public void setCurrentEntityAndItem(int entity, int item) {
IrisDisplayListState.recordEntityAndItem(entity, item);
setCurrentEntity(entity);
applyCurrentRenderedItem(item);
applyCurrentEntityAndItem(entity, item);
}
public void setCurrentNamedEntity(NamespacedId entity) {
IrisDisplayListState.recordNamedEntity(entity);
final var ids = BlockRenderingSettings.INSTANCE.getEntityIds();
setCurrentEntity(ids == null ? -1 : ids.applyAsInt(entity));
applyCurrentRenderedItem(0);
applyCurrentEntityAndItem(ids == null ? -1 : ids.applyAsInt(entity), 0);
}
public void pushCurrentEntityAndItem() {
@@ -123,15 +132,13 @@ public class CapturedRenderingState {
public void setCurrentRenderedEntity(Entity entity) {
IrisDisplayListState.recordEntity(entity);
setCurrentEntity(EntityIdHelper.getEntityId(entity));
applyCurrentRenderedItem(0);
applyCurrentEntityAndItem(EntityIdHelper.getEntityId(entity), 0);
}
public void popCurrentEntityAndItem() {
IrisDisplayListState.recordEntityScope(false);
// Restore the playback caller, never the IDs seen while compiling the list.
setCurrentEntity(entityStack.popInt());
applyCurrentRenderedItem(itemStack.popInt());
applyCurrentEntityAndItem(entityStack.popInt(), itemStack.popInt());
}
public void setCurrentEntityColor(float r, float g, float b, float a) {
@@ -209,6 +216,18 @@ public class CapturedRenderingState {
if (this.itemIdListener != null) {
this.itemIdListener.run();
}
notifyEntityIdsChanged();
}
public void setEntityIdsListener(Runnable listener) {
this.entityIdsListener = listener;
notifyEntityIdsChanged();
}
private void notifyEntityIdsChanged() {
if (this.entityIdsListener != null) {
this.entityIdsListener.run();
}
}
public ValueUpdateNotifier getEntityIdNotifier() {
@@ -0,0 +1,22 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.entity;
import com.gtnewhorizons.angelica.experimental.surround.Surround;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility;
import net.minecraft.client.renderer.entity.Render;
import org.spongepowered.asm.mixin.Mixin;
// The label sits above the model and its text is deferred, so its background quad does not make the batcher demote it.
@Mixin(Render.class)
public abstract class MixinRender_NameTagDraws {
@Surround(method = "func_147906_a")
private void angelica$countNameTagDraws() {
BatchEligibility.beginExpectedDraws(GLStateManager.drawCalls);
}
@Surround.Finally
private void angelica$endNameTagDraws() {
BatchEligibility.endExpectedDraws(GLStateManager.drawCalls);
}
}
@@ -1,11 +1,13 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.ffp;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorEntityIds;
import com.gtnewhorizons.angelica.rendering.tesr.ModelPartBatcher;
import net.minecraft.client.renderer.Tessellator;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
@@ -17,6 +19,11 @@ public class MixinTessellator_CoreProfile {
@Inject(method = "draw", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/Tessellator;isDrawing:Z", opcode = Opcodes.GETFIELD), cancellable = true)
private void angelica$coreProfileDraw(CallbackInfoReturnable<Integer> cir) {
cir.setReturnValue(TessellatorStreamingDrawer.draw((Tessellator) (Object) this));
cir.setReturnValue(ModelPartBatcher.drawTessellator((Tessellator) (Object) this));
}
@Inject(method = "reset", at = @At("HEAD"))
private void angelica$forgetEntityIds(CallbackInfo ci) {
TessellatorEntityIds.onReset((Tessellator) (Object) this);
}
}
@@ -8,6 +8,7 @@ import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.compat.DriverCompatabilityCheck;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.BackendStartGuard;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
@@ -47,6 +48,7 @@ public class MixinInitGLStateManager {
.enableDSA(AngelicaConfig.enableDSA)
.postInitCallback(SelectionBoxRenderer::init)
.build());
BackendStartGuard.disarm();
GLSMConfig.extendedAttribsExpected = AngelicaConfig.enableIris;
@@ -1,5 +1,6 @@
package net.coderbot.iris.pipeline;
import com.gtnewhorizons.angelica.glsm.EntityIdsOnlyHandler;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.GlTestSupport;
@@ -7,6 +8,7 @@ import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs;
import com.gtnewhorizons.angelica.glsm.ffp.FfpFixture;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
import com.gtnewhorizons.angelica.glsm.shader.ShaderType;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility;
@@ -403,15 +405,19 @@ class IrisInstancingGLTest {
""";
final String fragmentSource = """
#version 120
uniform int entityId;
uniform int blockEntityId;
uniform int entityId, blockEntityId;
uniform int currentRenderedItemId;
uniform int expectedId;
// a parameter and a local sharing the id names keep their own values
bool isFortyTwo(int entityId) { return entityId == 42; }
void main() {
bool matches = entityId == expectedId && blockEntityId == expectedId && currentRenderedItemId == expectedId;
gl_FragData[0] = matches ? vec4(0, 1, 0, 1) : vec4(1, 0, 0, 1);
bool shadowed = isFortyTwo(42);
{ int blockEntityId = 7; shadowed = shadowed && blockEntityId == 7; }
gl_FragData[0] = matches && shadowed ? vec4(0, 1, 0, 1) : vec4(1, 0, 0, 1);
}
""";
final int[] ids = { -1, 0, 32767, 32768, 45020, 50020, 50072, 65534 };
final int vao = GLStateManager.glGenVertexArrays();
final int buffer = GLStateManager.glGenBuffers();
GLStateManager.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
@@ -432,7 +438,7 @@ class IrisInstancingGLTest {
GLStateManager.glEnableVertexAttribArray(InstancedAttribs.LOC_ENTITY);
GLStateManager.glVertexAttribDivisor(InstancedAttribs.LOC_ENTITY, 1);
final ByteBuffer data = BufferUtils.createByteBuffer(InstancedAttribs.HEAD_SIZE);
for (int id : new int[] { -1, 0, 32767, 32768, 45020, 50020, 50072, 65534 }) {
for (int id : ids) {
data.putLong(InstancedAttribs.OFFSET_ENTITY, InstancedAttribs.packEntityInfo(id, id, id));
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STREAM_DRAW);
GLStateManager.glUniform1i(GLStateManager.glGetUniformLocation(program, "expectedId"), id);
@@ -449,6 +455,30 @@ class IrisInstancingGLTest {
GLStateManager.glDeleteProgram(program);
}
}
final int program = linkProgram("highIdsImmediate", TransformPatcher.patchAttributes(vertexSource, null, null, null, fragmentSource, TEX_LM, false));
final ImmediateExtendedAttribHandler previousHandler = GLSMHooks.immediateExtendedHandler;
try {
EntityIdsOnlyHandler.install();
GLStateManager.glUseProgram(program);
GLStateManager.glUniform1i(GLStateManager.glGetUniformLocation(program, "iris_currentAlphaFunc"), 7);
GLStateManager.glBindVertexArray(vao);
for (int id : ids) {
GLStateManager.setEntityIds(id, id, id);
GLStateManager.glUniform1i(GLStateManager.glGetUniformLocation(program, "expectedId"), id);
GLStateManager.glClearColor(0, 0, 0, 1);
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "draw error");
final int[] pixel = FfpFixture.readPixel(0, 0);
assertEquals(255, pixel[1], "immediate material ID " + id + " pixel " + Arrays.toString(pixel));
}
} finally {
GLStateManager.glUseProgram(0);
GLStateManager.glDeleteProgram(program);
GLSMHooks.immediateExtendedHandler = previousHandler;
GLStateManager.setEntityIds(-1, 0, -1);
}
} finally {
GLStateManager.glBindVertexArray(0);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.glsm.ffp.CubeInstancedAttribs;
import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs;
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
import com.gtnewhorizons.angelica.glsm.ffp.ParticleInstancedAttribs;
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability;
import net.coderbot.iris.pipeline.transform.parameter.AttributeParameters;
import org.junit.jupiter.api.AfterAll;
@@ -259,9 +260,10 @@ class AttributeTransformerInstancedTest {
assertLacks(v, "uniform int entityId;");
assertLacks(v, "uniform int blockEntityId;");
assertLacks(v, "uniform int currentRenderedItemId;");
assertHas(v, "layout(location = " + InstancedAttribs.LOC_ENTITY + ") in uvec4 iris_Entity;");
assertHas(v, "layout(location = " + InstancedAttribs.LOC_ENTITY + ") in vec4 iris_InstEntity;");
assertLacks(v, "in vec4 iris_Entity;");
assertHas(v, "flat out ivec3 iris_entityInfo;");
assertHas(v, "iris_entityInfo = ivec3(iris_Entity.x == 65535u ? -1 : int(iris_Entity.x), iris_Entity.y == 65535u ? -1 : int(iris_Entity.y), iris_Entity.z == 65535u ? -1 : int(iris_Entity.z));");
assertHas(v, "ivec3 iris_ids = ivec3(round(iris_InstEntity.xyz * 65535.0));");
assertLacks(f, "uniform int entityId;");
assertHas(f, "flat in ivec3 iris_entityInfo;");
@@ -362,6 +364,8 @@ class AttributeTransformerInstancedTest {
assertHas(v, "layout(location = 3) in vec4 iris_CubeDelta");
assertHas(v, "layout(location = " + CubeInstancedAttribs.LOC_CUBE_TEX + ") in vec4 iris_CubeTex");
assertHas(v, "layout(location = " + CubeInstancedAttribs.LOC_LIGHTMAP_SCALE + ") in vec4 iris_InstLightmapScale");
assertHas(v, "layout(location = " + InstancedAttribs.LOC_ENTITY + ") in vec4 iris_InstEntity;");
assertLacks(v, "in vec4 iris_Entity;");
assertLacks(v, "layout(location = 2) in vec4 iris_MultiTexCoord0");
assertHas(v, "vec4 iris_MultiTexCoord0;");
assertHas(v, "iris_MultiTexCoord1 = vec4(iris_InstLightmapScale.xy, 0.0, 1.0)");
@@ -417,10 +421,11 @@ class AttributeTransformerInstancedTest {
assertEquals(ParticleInstancedAttribs.LOC_UV, locationOf(byLocation, "iris_InstUv"));
assertEquals(InstancedAttribs.LOC_COLOR, locationOf(byLocation, "iris_InstColor"));
assertEquals(InstancedAttribs.LOC_LIGHTMAP, locationOf(byLocation, "iris_InstLightmap"));
assertEquals(ImmediateExtendedAttribHandler.LOC_ENTITY_IDS, locationOf(byLocation, "iris_Entity"));
assertFalse(byLocation.containsKey(7), "location 7 is unused");
for (final int loc : byLocation.keySet()) {
assertTrue(loc < 11, "unexpected attribute location " + loc + " declared for the particle variant");
assertTrue(loc < 11 || loc == ImmediateExtendedAttribHandler.LOC_ENTITY_IDS, "unexpected attribute location " + loc + " declared for the particle variant");
}
}
@@ -431,7 +436,7 @@ class AttributeTransformerInstancedTest {
}
@Test
void particleEntityIdAndOverlayColorStayAsPlainUniformsLikeTheNonePath() {
void particleEntityIdsAndOverlayColorFollowTheNonePath() {
final Map<PatchShaderType, String> result = TransformPatcher.patchAttributesInstanced(VERTEX_ENTITY_AND_COLOR, null, null, null, FRAGMENT_ENTITY_AND_COLOR, TEX_LM, false, Instancing.PARTICLE);
assertNotNull(result);
final String v = result.get(PatchShaderType.VERTEX);
@@ -441,16 +446,16 @@ class AttributeTransformerInstancedTest {
ShaderParser.parseShader(v);
ShaderParser.parseShader(f);
assertHas(v, "uniform int entityId;");
assertLacks(v, "uniform int entityId;");
assertHas(v, "layout(location = " + ImmediateExtendedAttribHandler.LOC_ENTITY_IDS + ") in vec4 iris_Entity;");
assertHas(v, "uniform vec4 entityColor;");
assertLacks(v, "iris_entityInfo");
assertLacks(v, "iris_InstOverlay");
assertLacks(v, "out vec4 entityColor;");
assertHas(f, "uniform int entityId;");
assertLacks(f, "uniform int entityId;");
assertHas(f, "flat in ivec3 iris_entityInfo;");
assertHas(f, "uniform vec4 entityColor;");
assertLacks(f, "in vec4 entityColor;");
assertLacks(f, "flat in ivec3 iris_entityInfo;");
}
@Test
@@ -465,6 +470,6 @@ class AttributeTransformerInstancedTest {
final Map<PatchShaderType, String> entityResult = TransformPatcher.patchAttributes(VERTEX_ENTITY, null, null, null, FRAGMENT_ENTITY, TEX_LM, false);
assertNotNull(entityResult);
assertHas(entityResult.get(PatchShaderType.VERTEX), "uniform int entityId;");
assertHas(entityResult.get(PatchShaderType.VERTEX), "layout(location = " + ImmediateExtendedAttribHandler.LOC_ENTITY_IDS + ") in vec4 iris_Entity;");
}
}
@@ -28,16 +28,16 @@ class SamplerAliasDeduplicatorTest {
String src =
"#version 330 core\n" +
"uniform sampler2D tex;\n" +
"uniform sampler2D textureAtlas;\n" +
"uniform sampler2D gtexture;\n" +
"out vec4 fragColor;\n" +
"void main() {\n" +
" fragColor = texture(textureAtlas, vec2(0.5)) + texture(tex, vec2(0.5));\n" +
" fragColor = texture(gtexture, vec2(0.5)) + texture(tex, vec2(0.5));\n" +
"}\n";
String out = transformFragment(src);
assertTrue(out.contains("uniform sampler2D tex"), "canonical declaration must remain");
assertFalse(out.contains("uniform sampler2D textureAtlas"), "alias declaration must be removed: " + out);
assertFalse(out.contains("textureAtlas"), "all references to alias must be rewritten: " + out);
assertFalse(out.contains("uniform sampler2D gtexture"), "alias declaration must be removed: " + out);
assertFalse(out.contains("gtexture"), "all references to alias must be rewritten: " + out);
assertEquals(2, occurrences(out, "texture(tex"), "both references should now read from tex");
}
@@ -45,27 +45,27 @@ class SamplerAliasDeduplicatorTest {
void onlyAliasDeclared_renamedToCanonical() {
String src =
"#version 330 core\n" +
"uniform sampler2D textureAtlas;\n" +
"uniform sampler2D gtexture;\n" +
"out vec4 fragColor;\n" +
"void main() { fragColor = texture(textureAtlas, vec2(0.5)); }\n";
"void main() { fragColor = texture(gtexture, vec2(0.5)); }\n";
String out = transformFragment(src);
assertTrue(out.contains("uniform sampler2D tex"), "alias should be promoted to canonical: " + out);
assertFalse(out.contains("textureAtlas"), "no alias should remain: " + out);
assertFalse(out.contains("gtexture"), "no alias should remain: " + out);
}
@Test
void crossStage_consistentCanonical() {
String vs =
"#version 330 core\n" +
"uniform sampler2D textureAtlas;\n" +
"void main() { gl_Position = vec4(texture(textureAtlas, vec2(0.0)).xyz, 1.0); }\n";
"uniform sampler2D gtexture;\n" +
"void main() { gl_Position = vec4(texture(gtexture, vec2(0.0)).xyz, 1.0); }\n";
String fs =
"#version 330 core\n" +
"uniform sampler2D tex;\n" +
"uniform sampler2D textureAtlas;\n" +
"uniform sampler2D gtexture;\n" +
"out vec4 fragColor;\n" +
"void main() { fragColor = texture(textureAtlas, vec2(0.5)) + texture(tex, vec2(0.5)); }\n";
"void main() { fragColor = texture(gtexture, vec2(0.5)) + texture(tex, vec2(0.5)); }\n";
Transformer vt = parse(vs);
Transformer ft = parse(fs);
@@ -78,10 +78,24 @@ class SamplerAliasDeduplicatorTest {
String foutS = format(ft);
assertTrue(voutS.contains("uniform sampler2D tex"), "vertex must declare tex: " + voutS);
assertFalse(voutS.contains("textureAtlas"), "vertex must not reference alias: " + voutS);
assertFalse(voutS.contains("gtexture"), "vertex must not reference alias: " + voutS);
assertTrue(foutS.contains("uniform sampler2D tex"), "fragment must declare tex: " + foutS);
assertFalse(foutS.contains("uniform sampler2D textureAtlas"), "fragment alias decl must be gone: " + foutS);
assertFalse(foutS.contains("textureAtlas"), "fragment must not reference alias: " + foutS);
assertFalse(foutS.contains("uniform sampler2D gtexture"), "fragment alias decl must be gone: " + foutS);
assertFalse(foutS.contains("gtexture"), "fragment must not reference alias: " + foutS);
}
@Test
void packCustomSamplerIsNotAnAlias() {
String src =
"#version 330 core\n" +
"uniform sampler2D tex;\n" +
"uniform sampler2D textureAtlas;\n" +
"out vec4 fragColor;\n" +
"void main() { fragColor = texture(textureAtlas, vec2(0.5)) + texture(tex, vec2(0.5)); }\n";
String out = transformFragment(src);
assertTrue(out.contains("uniform sampler2D textureAtlas"), "a pack-defined sampler keeps its own binding: " + out);
assertTrue(out.contains("texture(textureAtlas"), "its reads must not be redirected to tex: " + out);
}
@Test
@@ -118,10 +118,10 @@ class VertexReplayExtractionTest {
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");
final VertexInput entity = input(inputs, "iris_InstEntity");
assertEquals(InstancedAttribs.LOC_ENTITY, entity.location());
assertEquals("uvec4", entity.glslType());
assertTrue(result.computeSource().contains("iris_Entity = uvec4(_vg_uint("), result.computeSource());
assertEquals("vec4", entity.glslType());
assertTrue(result.computeSource().contains("iris_InstEntity = vec4(_vg_float("), result.computeSource());
assertFalse(result.computeSource().contains("location"), "a location on a former interface variable does not compile in compute");
}