Compare commits

..
3 Commits
Author SHA1 Message Date
Eclipse-Sol 49db8e111d Allow Iris to use "more" Samplers on SDL-GPU (#2256)
* Initial commit for sdl-over-16-samplers

* Fix WSR not working on SDL
2026-10-14 16:10:54 -07:00
Eclipse-Sol bd9a3c3c5c initial commit for witchery-cauldron-not-drawing-base (#2260) 2026-10-14 16:09:57 -07:00
Ruling-0 01ff612d1d Make animation interpolation respect alpha (#2258) 2026-10-14 15:44:18 -07:00
52 changed files with 2159 additions and 412 deletions

No files matched your search

@@ -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) {
@@ -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;
@@ -206,6 +160,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.
*/
@@ -443,6 +448,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);
@@ -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();
}
}
@@ -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);
@@ -534,8 +542,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 +681,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 +2644,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 +3748,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 +4031,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 +4051,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 +4085,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 +4331,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);
}
}
@@ -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;
@@ -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));
}
}
@@ -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");
}
}
@@ -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) {
@@ -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"
@@ -16,6 +16,7 @@ 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.TessellatorStreamingDrawer;
import com.gtnewhorizons.angelica.glsm.states.AlphaState;
import com.gtnewhorizons.angelica.glsm.states.BlendState;
import com.gtnewhorizons.angelica.glsm.states.Color4;
@@ -624,6 +625,52 @@ 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 (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;
}
}
@@ -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"))
);
@@ -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,6 +1,6 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.ffp;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
import com.gtnewhorizons.angelica.rendering.tesr.ModelPartBatcher;
import net.minecraft.client.renderer.Tessellator;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
@@ -17,6 +17,6 @@ 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));
}
}
@@ -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