mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-15 01:34:48 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49db8e111d | ||
|
|
bd9a3c3c5c | ||
|
|
01ff612d1d |
No files matched your search
+25
-6
@@ -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) {
|
||||
|
||||
+53
-47
@@ -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);
|
||||
|
||||
+4
-2
@@ -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;
|
||||
|
||||
+73
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+12
-26
@@ -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;
|
||||
|
||||
+11
-69
@@ -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;
|
||||
|
||||
+110
-32
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+86
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-3
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -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;
|
||||
|
||||
+114
-41
@@ -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)) {
|
||||
|
||||
+212
@@ -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;
|
||||
|
||||
+29
-15
@@ -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;
|
||||
}
|
||||
}
|
||||
+23
-8
@@ -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;
|
||||
}
|
||||
}
|
||||
+14
-43
@@ -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];
|
||||
|
||||
|
||||
+38
@@ -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
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+7
-7
@@ -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;
|
||||
|
||||
+44
@@ -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);
|
||||
|
||||
+685
@@ -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");
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -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();
|
||||
|
||||
+6
-2
@@ -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) {
|
||||
|
||||
+39
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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});
|
||||
|
||||
+2
-9
@@ -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);
|
||||
|
||||
+55
@@ -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;
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -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
-9
@@ -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));
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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"))
|
||||
);
|
||||
|
||||
+22
@@ -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);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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
-14
@@ -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
|
||||
|
||||
Reference in new issue
Block a user