mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-08 12:35:01 +00:00
vsync adjustments: on/mailbox/off + windows input lag fix + glsl parsing optimizations (#2009)
This commit is contained in:
1 parent
3659b59db9
commit
b17bb42c49
67 files changed
+2947
-762
No files matched your search
@@ -40,5 +40,6 @@ addon.late.local.gradle
|
||||
addon.late.local.gradle.kts
|
||||
local/
|
||||
tracy-client/src/main/resources/natives/
|
||||
tracy-client/angelica/
|
||||
dist/tracy-natives/
|
||||
*.tracy
|
||||
@@ -35,6 +35,8 @@ public final class SystemProperties {
|
||||
public static final boolean SDL_ENCODER_ASSERTIONS_FATAL = ENCODER_ASSERTIONS == SdlAssertionMode.FATAL;
|
||||
public static final boolean SDL_VERIFY_STATE_SYNC = Boolean.getBoolean("angelica.sdlgpu.verifyStateSync");
|
||||
public static final boolean SDL_VERIFY_PER_FRAME_UNIFORM_BLOCK = Boolean.getBoolean("angelica.sdlgpu.verifyPerFrameUniformBlock");
|
||||
public static final int SDL_FRAMES_IN_FLIGHT = Integer.getInteger("angelica.sdlgpu.framesInFlight", 2);
|
||||
public static final boolean DISABLE_SDL_PRESENTER_THREAD = Boolean.getBoolean("angelica.sdlgpu.disablePresenterThread");
|
||||
|
||||
// Tracy
|
||||
public static final boolean TRACY = Boolean.getBoolean("angelica.tracy");
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.gtnewhorizons.angelica.glsm.DisplayListManager.RecordMode;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.GLDebugMessageListener;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
|
||||
@@ -3383,14 +3382,11 @@ public class GLStateManager {
|
||||
}
|
||||
|
||||
public static boolean isCurrent() {
|
||||
if (BackendManager.RENDER_BACKEND == null) {
|
||||
return Thread.currentThread() == MainThread;
|
||||
}
|
||||
return BackendManager.RENDER_BACKEND.isCurrent();
|
||||
return RENDER_BACKEND.isCurrent();
|
||||
}
|
||||
|
||||
public static void swapBuffers() throws LWJGLException {
|
||||
if (BackendManager.RENDER_BACKEND != null && BackendManager.RENDER_BACKEND.handleSwapBuffers()) {
|
||||
if (RENDER_BACKEND.handleSwapBuffers()) {
|
||||
return;
|
||||
}
|
||||
Display.swapBuffers();
|
||||
@@ -3406,7 +3402,14 @@ public class GLStateManager {
|
||||
if (processMessages) Display.processMessages();
|
||||
return;
|
||||
}
|
||||
if (!RENDER_BACKEND.wantsDisplayUpdateGateTiming()) {
|
||||
Display.update(processMessages);
|
||||
return;
|
||||
}
|
||||
final long start = System.nanoTime();
|
||||
Display.update(processMessages);
|
||||
final long end = System.nanoTime();
|
||||
RENDER_BACKEND.recordFrameGate(end - start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5048,41 +5051,57 @@ public class GLStateManager {
|
||||
}
|
||||
|
||||
public static void setVSyncEnabled(boolean enabled) {
|
||||
setVSyncMode(RENDER_BACKEND.getPreferredVSyncMode(), enabled);
|
||||
RENDER_BACKEND.setVSyncEnabled(enabled);
|
||||
}
|
||||
|
||||
public static void setVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
|
||||
RENDER_BACKEND.setVSyncMode(preferred, vsyncEnabled);
|
||||
public static void setVSyncMode(VSyncMode mode) {
|
||||
RENDER_BACKEND.setVSyncMode(mode);
|
||||
}
|
||||
|
||||
public static void setVSyncPreference(VSyncMode mode) {
|
||||
RENDER_BACKEND.setVSyncPreference(mode);
|
||||
}
|
||||
|
||||
public static boolean supportsVSyncMode(VSyncMode mode) {
|
||||
return RENDER_BACKEND.supportsVSyncMode(mode);
|
||||
}
|
||||
|
||||
public static VSyncMode preferredTearFreeMode() {
|
||||
return RENDER_BACKEND.preferredTearFreeMode();
|
||||
}
|
||||
|
||||
public static String getRenderBackendName() {
|
||||
return RENDER_BACKEND.getName();
|
||||
}
|
||||
|
||||
public static VSyncMode getEffectiveVSyncMode() {
|
||||
return RENDER_BACKEND.getEffectiveVSyncMode();
|
||||
}
|
||||
|
||||
public static List<VSyncMode> getSelectableVSyncModes() {
|
||||
return VSyncMode.selectable(RENDER_BACKEND::supportsVSyncMode);
|
||||
}
|
||||
|
||||
public static int getMinRenderAhead() {
|
||||
return RENDER_BACKEND.getMinRenderAhead();
|
||||
}
|
||||
|
||||
public static int getMaxRenderAhead() {
|
||||
return RENDER_BACKEND.getMaxRenderAhead();
|
||||
}
|
||||
|
||||
public static int renderAheadLimit(int configured) {
|
||||
return RenderBackend.clampRenderAhead(configured, RENDER_BACKEND.getMinRenderAhead(), RENDER_BACKEND.getMaxRenderAhead());
|
||||
}
|
||||
|
||||
public static void applyRenderAheadLimit(int configured) {
|
||||
RENDER_BACKEND.setRenderAheadLimit(renderAheadLimit(configured));
|
||||
}
|
||||
|
||||
public static int getDisplayRefreshRateHz() {
|
||||
return RENDER_BACKEND.getDisplayRefreshRateHz();
|
||||
}
|
||||
|
||||
public static boolean gateAnchorsNextFrameStart() {
|
||||
return RENDER_BACKEND.gateAnchorsNextFrameStart();
|
||||
}
|
||||
|
||||
public static long lastFrameGateNanos() {
|
||||
return RENDER_BACKEND.lastFrameGateNanos();
|
||||
}
|
||||
|
||||
public static long lastFrameGateEndNanos() {
|
||||
return RENDER_BACKEND.lastFrameGateEndNanos();
|
||||
}
|
||||
|
||||
public static boolean hasSwapchainBackpressure() {
|
||||
return RENDER_BACKEND.hasSwapchainBackpressure();
|
||||
}
|
||||
|
||||
public static void pumpDisplayMessages() {
|
||||
Display.processMessages();
|
||||
}
|
||||
|
||||
public static int glGetError() {
|
||||
if (!RENDER_BACKEND.hasContext()) return 0;
|
||||
if (initConfig.isNoErrorChecks()) return 0;
|
||||
|
||||
@@ -2,8 +2,10 @@ package com.gtnewhorizons.angelica.glsm;
|
||||
|
||||
import org.antlr.v4.runtime.BufferedTokenStream;
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.CommonToken;
|
||||
import org.antlr.v4.runtime.CommonTokenStream;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
import org.antlr.v4.runtime.WritableToken;
|
||||
import org.antlr.v4.runtime.tree.ParseTree;
|
||||
import org.antlr.v4.runtime.tree.TerminalNode;
|
||||
import org.taumc.glsl.grammar.GLSLLexer;
|
||||
@@ -88,6 +90,23 @@ public class GlslTransformUtils {
|
||||
return source;
|
||||
}
|
||||
|
||||
public static void restoreReservedWordsInTree(ParseTree tree) {
|
||||
if (tree instanceof TerminalNode terminal) {
|
||||
final Token token = terminal.getSymbol();
|
||||
final String text = token.getText();
|
||||
if (text != null && text.startsWith(RENAMED_PREFIX) && token instanceof WritableToken writable) {
|
||||
final String rest = text.substring(RENAMED_PREFIX.length());
|
||||
if (rest.startsWith("texture") || rest.startsWith("sample")) {
|
||||
writable.setText(rest);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < tree.getChildCount(); i++) {
|
||||
restoreReservedWordsInTree(tree.getChild(i));
|
||||
}
|
||||
}
|
||||
|
||||
public record QuietParse(GLSLParser.Translation_unitContext full, GLSLPreParser.Translation_unitContext pre) {}
|
||||
|
||||
public static GLSLParser.Translation_unitContext parseFullQuiet(String source) {
|
||||
@@ -162,21 +181,32 @@ public class GlslTransformUtils {
|
||||
public static String getFormattedShader(ParseTree tree, String header) {
|
||||
final StringBuilder sb = new StringBuilder(header + "\n");
|
||||
final String[] tabHolder = {""};
|
||||
getFormattedShader(tree, sb, tabHolder);
|
||||
getFormattedShader(tree, sb, tabHolder, false);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static void getFormattedShader(ParseTree tree, StringBuilder stringBuilder, String[] tabHolder) {
|
||||
if (tree instanceof TerminalNode) {
|
||||
public static String getFormattedShaderRebased(ParseTree tree, String header) {
|
||||
final StringBuilder sb = new StringBuilder(header + "\n");
|
||||
final String[] tabHolder = {""};
|
||||
getFormattedShader(tree, sb, tabHolder, true);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static void getFormattedShader(ParseTree tree, StringBuilder stringBuilder, String[] tabHolder, boolean rebaseOffsets) {
|
||||
if (tree instanceof TerminalNode terminal) {
|
||||
final String text = tree.getText();
|
||||
if (text.equals("<EOF>")) {
|
||||
return;
|
||||
}
|
||||
if (text.equals("#")) {
|
||||
stringBuilder.append("\n#");
|
||||
rebaseAt(terminal, stringBuilder.length() - 1, text, rebaseOffsets);
|
||||
return;
|
||||
}
|
||||
stringBuilder.append(text);
|
||||
if (!text.equals("}")) {
|
||||
rebaseAt(terminal, stringBuilder.length() - text.length(), text, rebaseOffsets);
|
||||
}
|
||||
if (text.equals("{")) {
|
||||
stringBuilder.append(" \n\t");
|
||||
tabHolder[0] = "\t";
|
||||
@@ -186,6 +216,7 @@ public class GlslTransformUtils {
|
||||
if (stringBuilder.length() >= 2) {
|
||||
stringBuilder.deleteCharAt(stringBuilder.length() - 2);
|
||||
}
|
||||
rebaseAt(terminal, stringBuilder.length() - 1, text, rebaseOffsets);
|
||||
tabHolder[0] = "";
|
||||
stringBuilder.append(" \n");
|
||||
} else {
|
||||
@@ -193,8 +224,17 @@ public class GlslTransformUtils {
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < tree.getChildCount(); ++i) {
|
||||
getFormattedShader(tree.getChild(i), stringBuilder, tabHolder);
|
||||
getFormattedShader(tree.getChild(i), stringBuilder, tabHolder, rebaseOffsets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void rebaseAt(TerminalNode terminal, int startIndex, String text, boolean rebaseOffsets) {
|
||||
if (!rebaseOffsets) return;
|
||||
if (terminal.getSymbol() instanceof CommonToken common) {
|
||||
common.setText(text);
|
||||
common.setStartIndex(startIndex);
|
||||
common.setStopIndex(startIndex + text.length() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.gtnewhorizons.angelica.glsm.dsa.DSAEXT;
|
||||
import com.gtnewhorizons.angelica.glsm.dsa.DSAUnsupported;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
|
||||
import com.mitchej123.lwjgl.LWJGLServiceProvider;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
@@ -111,6 +112,13 @@ public class RenderSystem {
|
||||
|
||||
BackendManager.init();
|
||||
|
||||
try {
|
||||
// Force init on client thread
|
||||
GLStateManager.LOGGER.info("Celeritas LWJGL service: {}", LWJGLServiceProvider.LWJGL.getClass().getName());
|
||||
} catch (Throwable t) {
|
||||
GLStateManager.LOGGER.debug("Could not resolve the celeritas LWJGL service: {}", t.toString());
|
||||
}
|
||||
|
||||
supportsCompute = supportsCompute();
|
||||
supportsTesselation = GLStateManager.capabilities.GL_ARB_tessellation_shader || GLStateManager.capabilities.OpenGL40;
|
||||
|
||||
|
||||
+6
-2
@@ -115,10 +115,14 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
|
||||
public int getMinGLSLVersion() {return 330;}
|
||||
|
||||
@Override
|
||||
protected void setSwapInterval(boolean vsync) {Display.setVSyncEnabled(vsync);}
|
||||
protected VSyncMode applyVSyncMode(VSyncMode preferred) {
|
||||
final boolean enabled = preferred.tearFree();
|
||||
Display.setVSyncEnabled(enabled);
|
||||
return enabled ? VSyncMode.ON : VSyncMode.OFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDisplayRefreshRateHz() {
|
||||
protected int queryDisplayRefreshRateHz() {
|
||||
final DisplayMode mode = Display.getDisplayMode();
|
||||
if (mode != null && mode.getFrequency() > 0) return mode.getFrequency();
|
||||
final DisplayMode desktop = Display.getDesktopDisplayMode();
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.nio.IntBuffer;
|
||||
import java.nio.ShortBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Abstract rendering backend.
|
||||
@@ -62,47 +63,77 @@ public abstract class RenderBackend {
|
||||
public abstract void flush();
|
||||
public abstract void finish();
|
||||
|
||||
protected abstract void setSwapInterval(boolean vsync);
|
||||
|
||||
private VSyncMode preferredVSyncMode = VSyncMode.AUTO;
|
||||
private VSyncMode effectiveVSyncMode = VSyncMode.ON;
|
||||
private VSyncMode preferredTearFreeMode;
|
||||
|
||||
public final void setVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
|
||||
preferredVSyncMode = preferred == null ? VSyncMode.AUTO : preferred;
|
||||
effectiveVSyncMode = applyVSyncMode(preferredVSyncMode, vsyncEnabled);
|
||||
public final void setVSyncEnabled(boolean enabled) {
|
||||
setVSyncMode(enabled ? preferredTearFreeMode() : VSyncMode.OFF);
|
||||
}
|
||||
|
||||
protected VSyncMode applyVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
|
||||
final VSyncMode mode = preferred.resolve(vsyncEnabled).swapIntervalEquivalent();
|
||||
setSwapInterval(mode.usesVSyncSwapInterval());
|
||||
return mode;
|
||||
public final void setVSyncMode(VSyncMode preferred) {
|
||||
setVSyncPreference(preferred);
|
||||
publishVSyncMode(applyVSyncMode(preferred));
|
||||
}
|
||||
|
||||
public final void setVSyncPreference(VSyncMode preferred) {
|
||||
if (preferred.tearFree()) preferredTearFreeMode = preferred;
|
||||
}
|
||||
|
||||
public final VSyncMode preferredTearFreeMode() {
|
||||
return resolveTearFreeMode(preferredTearFreeMode, this::supportsVSyncMode);
|
||||
}
|
||||
|
||||
static VSyncMode resolveTearFreeMode(VSyncMode preferred, Predicate<VSyncMode> supported) {
|
||||
if (preferred != null && preferred.tearFree() && supported.test(preferred)) return preferred;
|
||||
return supported.test(VSyncMode.MAILBOX) ? VSyncMode.MAILBOX : VSyncMode.ON;
|
||||
}
|
||||
|
||||
protected abstract VSyncMode applyVSyncMode(VSyncMode preferred);
|
||||
|
||||
public boolean supportsVSyncMode(VSyncMode mode) { return mode != VSyncMode.MAILBOX; }
|
||||
|
||||
public int getMinRenderAhead() { return 0; }
|
||||
|
||||
public int getMaxRenderAhead() { return 9; }
|
||||
|
||||
public void setRenderAheadLimit(int frames) {}
|
||||
|
||||
public static int clampRenderAhead(int value, int min, int max) {
|
||||
if (value <= 0) return min <= 0 ? 0 : max;
|
||||
return Math.min(Math.max(value, Math.max(min, 1)), max);
|
||||
}
|
||||
|
||||
public final VSyncMode getPreferredVSyncMode() { return preferredVSyncMode; }
|
||||
protected final void publishVSyncMode(VSyncMode mode) { effectiveVSyncMode = mode; }
|
||||
|
||||
public final VSyncMode getEffectiveVSyncMode() { return effectiveVSyncMode; }
|
||||
|
||||
public int getDisplayRefreshRateHz() { return 0; }
|
||||
private static final long REFRESH_POLL_NANOS = 1_000_000_000L;
|
||||
|
||||
private long refreshPollNanos;
|
||||
private int cachedRefreshHz;
|
||||
|
||||
public final int getDisplayRefreshRateHz() {
|
||||
final long now = System.nanoTime();
|
||||
if (refreshPollNanos == 0L || now - refreshPollNanos >= REFRESH_POLL_NANOS) {
|
||||
refreshPollNanos = now;
|
||||
cachedRefreshHz = queryDisplayRefreshRateHz();
|
||||
}
|
||||
return cachedRefreshHz;
|
||||
}
|
||||
|
||||
protected int queryDisplayRefreshRateHz() { return 0; }
|
||||
|
||||
public static int refreshHzFrom(int numerator, int denominator, float fallbackHz) {
|
||||
if (numerator > 0 && denominator > 0) return (numerator + denominator - 1) / denominator;
|
||||
return fallbackHz > 0.0f ? (int) Math.ceil(fallbackHz) : 0;
|
||||
}
|
||||
|
||||
public String getPresentDebugInfo() { return null; }
|
||||
public boolean hasSwapchainBackpressure() { return false; }
|
||||
|
||||
private long frameGateNanos;
|
||||
private long frameGateEndNanos;
|
||||
|
||||
public boolean gateAnchorsNextFrameStart() { return false; }
|
||||
|
||||
public boolean wantsDisplayUpdateGateTiming() { return true; }
|
||||
|
||||
public void recordFrameGate(long durationNanos, long endNanos) {
|
||||
frameGateNanos = durationNanos;
|
||||
frameGateEndNanos = endNanos;
|
||||
}
|
||||
|
||||
public long lastFrameGateNanos() { return frameGateNanos; }
|
||||
|
||||
public long lastFrameGateEndNanos() { return frameGateEndNanos; }
|
||||
|
||||
public void onFrameBegin() {
|
||||
if (GLStateManager.takeStateSeedPending()) {
|
||||
|
||||
@@ -1,54 +1,11 @@
|
||||
package com.gtnewhorizons.angelica.glsm.backend;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public enum VSyncMode {
|
||||
AUTO,
|
||||
OFF,
|
||||
ON,
|
||||
MAILBOX,
|
||||
OFF;
|
||||
|
||||
private static final List<VSyncMode> OFF_ORDER = List.of(OFF, MAILBOX, ON);
|
||||
private static final List<VSyncMode> MAILBOX_ORDER = List.of(MAILBOX, ON);
|
||||
private static final List<VSyncMode> ON_ORDER = List.of(ON);
|
||||
|
||||
private static final List<VSyncMode> DISPLAY_ORDER = List.of(OFF, ON, MAILBOX);
|
||||
|
||||
public boolean blocksOnVBlank() {
|
||||
return this == ON;
|
||||
}
|
||||
|
||||
public boolean usesVSyncSwapInterval() {
|
||||
return this == ON || this == MAILBOX;
|
||||
}
|
||||
|
||||
public VSyncMode swapIntervalEquivalent() {
|
||||
return this == MAILBOX ? ON : this;
|
||||
}
|
||||
|
||||
public VSyncMode resolve(boolean vsyncEnabled) {
|
||||
return this != AUTO ? this : (vsyncEnabled ? ON : OFF);
|
||||
}
|
||||
|
||||
public List<VSyncMode> fallbackOrder() {
|
||||
return switch (this) {
|
||||
case OFF -> OFF_ORDER;
|
||||
case MAILBOX -> MAILBOX_ORDER;
|
||||
default -> ON_ORDER;
|
||||
};
|
||||
}
|
||||
MAILBOX;
|
||||
|
||||
public boolean tearFree() {
|
||||
return this != OFF;
|
||||
}
|
||||
|
||||
public static List<VSyncMode> selectable(Predicate<VSyncMode> supported) {
|
||||
final List<VSyncMode> modes = new ArrayList<>(DISPLAY_ORDER.size());
|
||||
for (VSyncMode mode : DISPLAY_ORDER) {
|
||||
if (supported.test(mode)) modes.add(mode);
|
||||
}
|
||||
return modes;
|
||||
}
|
||||
}
|
||||
+5
@@ -1,8 +1,13 @@
|
||||
package com.gtnewhorizons.angelica.glsm.hooks;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderType;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ShaderTransformPostProcessor {
|
||||
void onTransformed(String transformedSource, ShaderType shaderType);
|
||||
|
||||
default void onTransformed(String transformedSource, GLSLParser.Translation_unitContext bodyTree, int headerLen, ShaderType shaderType) {
|
||||
onTransformed(transformedSource, shaderType);
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,7 @@ public class GLSMRedirector {
|
||||
private static final String EXTGpuShader4 = "org/lwjgl/opengl/EXTGpuShader4";
|
||||
private static final String EXTGPUShader4 = "org/lwjgl/opengl/EXTGPUShader4";
|
||||
private static final String ARBTimerQuery = "org/lwjgl/opengl/ARBTimerQuery";
|
||||
private static final String EXTTimerQuery = "org/lwjgl/opengl/EXTTimerQuery";
|
||||
|
||||
// Redirect VAO related calls from NHLib
|
||||
private static final String UniversalVAO = "com/gtnewhorizon/gtnhlib/client/opengl/UniversalVAO";
|
||||
@@ -632,6 +633,10 @@ public class GLSMRedirector {
|
||||
.add("glQueryCounter")
|
||||
.add("glGetQueryObjectui64")
|
||||
);
|
||||
methodRedirects.put(EXTTimerQuery, RedirectMap.newMap()
|
||||
.add("glGetQueryObjectuEXT", "glGetQueryObjectui64")
|
||||
.add("glGetQueryObjectui64EXT", "glGetQueryObjectui64")
|
||||
);
|
||||
|
||||
glDescRedirects.put("org/lwjgl/opengl/GL32C glFenceSync(II)J", "glFenceSync");
|
||||
glDescRedirects.put("org/lwjgl/opengl/GL32C glClientWaitSync(JIJ)I", "glClientWaitSync");
|
||||
|
||||
@@ -100,7 +100,7 @@ public final class GlslVulkanPreprocess {
|
||||
return out;
|
||||
}
|
||||
|
||||
public record Metadata(Set<String> boolUniforms, Set<String> explicitVsInputs) {}
|
||||
public record Metadata(Set<String> boolUniforms, Set<String> explicitVsInputs, boolean needsSamplerless) {}
|
||||
|
||||
public static Metadata collectEdits(String source, GLSLParser.Translation_unitContext root, int glShaderType, String debugName, boolean separateReadOnlyImages, List<Edit> edits) {
|
||||
final boolean isVertex = glShaderType == GL20.GL_VERTEX_SHADER;
|
||||
@@ -216,7 +216,7 @@ public final class GlslVulkanPreprocess {
|
||||
}
|
||||
}, root);
|
||||
|
||||
if (needsSamplerless[0]) {
|
||||
if (needsSamplerless[0] && source != null) {
|
||||
final int at = versionDirectiveEnd(source);
|
||||
if (at >= 0) edits.add(new Edit(at, at - 1, "\n" + SAMPLERLESS_EXTENSION));
|
||||
}
|
||||
@@ -234,7 +234,7 @@ public final class GlslVulkanPreprocess {
|
||||
}
|
||||
}
|
||||
|
||||
return new Metadata(bools, explicitInputs);
|
||||
return new Metadata(bools, explicitInputs, needsSamplerless[0]);
|
||||
}
|
||||
|
||||
public static void clearCache() {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.gtnewhorizons.angelica.glsm;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ReservedWordRestoreTest {
|
||||
|
||||
private static void assertTreeRestoreMatchesStringRestore(String label, String renamedSource) {
|
||||
final GLSLParser.Translation_unitContext treeA = GlslTransformUtils.parseFullQuiet(renamedSource);
|
||||
final String legacy = GlslTransformUtils.restoreReservedWords(GlslTransformUtils.getFormattedShader(treeA, ""));
|
||||
|
||||
final GLSLParser.Translation_unitContext treeB = GlslTransformUtils.parseFullQuiet(renamedSource);
|
||||
GlslTransformUtils.restoreReservedWordsInTree(treeB);
|
||||
final String inTree = GlslTransformUtils.getFormattedShader(treeB, "");
|
||||
|
||||
assertEquals(legacy, inTree, "in-tree restore diverged from string restore for " + label);
|
||||
}
|
||||
|
||||
@Test
|
||||
void allThreeRenamesRestoreIdentically() {
|
||||
final String src = "float angelica_renamed_sample = 1.0;\n"
|
||||
+ "uniform sampler2D angelica_renamed_sampler;\n"
|
||||
+ "vec4 angelica_renamed_texture = vec4(0.0);\n"
|
||||
+ "void main() {\n"
|
||||
+ " vec4 c = angelica_renamed_texture + vec4(angelica_renamed_sample);\n"
|
||||
+ "}\n";
|
||||
assertTreeRestoreMatchesStringRestore("all-three", src);
|
||||
}
|
||||
|
||||
@Test
|
||||
void renamedNewStaysRenamed() {
|
||||
final String src = "float angelica_renamed_new = 2.0;\n"
|
||||
+ "void main() { float x = angelica_renamed_new; }\n";
|
||||
assertTreeRestoreMatchesStringRestore("new-identifier", src);
|
||||
|
||||
final GLSLParser.Translation_unitContext tree = GlslTransformUtils.parseFullQuiet(src);
|
||||
GlslTransformUtils.restoreReservedWordsInTree(tree);
|
||||
final String out = GlslTransformUtils.getFormattedShader(tree, "");
|
||||
assertTrue(out.contains("angelica_renamed_new"), "new is reserved in every GLSL version and must stay renamed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void renamedTextureFunctionCallRestores() {
|
||||
final String src = "uniform sampler2D tex;\n"
|
||||
+ "void main() {\n"
|
||||
+ " vec4 c = angelica_renamed_texture2D(tex, vec2(0.5));\n"
|
||||
+ "}\n";
|
||||
assertTreeRestoreMatchesStringRestore("texture2D-call", src);
|
||||
|
||||
final GLSLParser.Translation_unitContext tree = GlslTransformUtils.parseFullQuiet(src);
|
||||
GlslTransformUtils.restoreReservedWordsInTree(tree);
|
||||
final String out = GlslTransformUtils.getFormattedShader(tree, "");
|
||||
assertTrue(out.contains("texture2D"), "suffixed rename must strip only the prefix");
|
||||
assertFalse(out.contains("angelica_renamed_"), "no renamed remnants expected");
|
||||
}
|
||||
|
||||
@Test
|
||||
void untouchedSourcePrintsIdentically() {
|
||||
final String src = "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = vec4(1.0); }\n";
|
||||
assertTreeRestoreMatchesStringRestore("no-renames", src);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package com.gtnewhorizons.angelica.glsm.backend;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@@ -11,63 +11,36 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
class BackendPolicyTest {
|
||||
|
||||
@Test
|
||||
void autoFollowsTheVsyncToggle() {
|
||||
assertEquals(VSyncMode.ON, VSyncMode.AUTO.resolve(true));
|
||||
assertEquals(VSyncMode.OFF, VSyncMode.AUTO.resolve(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void anExplicitModeIgnoresTheVsyncToggle() {
|
||||
for (VSyncMode mode : new VSyncMode[]{ VSyncMode.ON, VSyncMode.MAILBOX, VSyncMode.OFF }) {
|
||||
assertEquals(mode, mode.resolve(true));
|
||||
assertEquals(mode, mode.resolve(false));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyOnBlocks() {
|
||||
assertTrue(VSyncMode.ON.blocksOnVBlank());
|
||||
assertFalse(VSyncMode.MAILBOX.blocksOnVBlank());
|
||||
assertFalse(VSyncMode.OFF.blocksOnVBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mailboxRidesOnSwapIntervalOne() {
|
||||
assertTrue(VSyncMode.ON.usesVSyncSwapInterval());
|
||||
assertTrue(VSyncMode.MAILBOX.usesVSyncSwapInterval());
|
||||
assertFalse(VSyncMode.OFF.usesVSyncSwapInterval());
|
||||
}
|
||||
|
||||
@Test
|
||||
void swapIntervalBackendsReportWhatTheyActuallyGet() {
|
||||
assertEquals(VSyncMode.ON, VSyncMode.MAILBOX.swapIntervalEquivalent());
|
||||
assertEquals(VSyncMode.ON, VSyncMode.ON.swapIntervalEquivalent());
|
||||
assertEquals(VSyncMode.OFF, VSyncMode.OFF.swapIntervalEquivalent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallbackNeverWidensTearing() {
|
||||
assertEquals(List.of(VSyncMode.ON), VSyncMode.ON.fallbackOrder());
|
||||
assertEquals(List.of(VSyncMode.MAILBOX, VSyncMode.ON), VSyncMode.MAILBOX.fallbackOrder());
|
||||
assertEquals(List.of(VSyncMode.OFF, VSyncMode.MAILBOX, VSyncMode.ON), VSyncMode.OFF.fallbackOrder());
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyOffCanTear() {
|
||||
void onlyImmediateCanTear() {
|
||||
assertTrue(VSyncMode.ON.tearFree());
|
||||
assertTrue(VSyncMode.MAILBOX.tearFree());
|
||||
assertFalse(VSyncMode.OFF.tearFree());
|
||||
}
|
||||
|
||||
private static final Predicate<VSyncMode> NO_MAILBOX = mode -> mode != VSyncMode.MAILBOX;
|
||||
private static final Predicate<VSyncMode> EVERYTHING = mode -> true;
|
||||
|
||||
@Test
|
||||
void onlyBackendSupportedModesAreSelectable() {
|
||||
assertEquals(List.of(VSyncMode.OFF, VSyncMode.ON), VSyncMode.selectable(m -> m != VSyncMode.MAILBOX));
|
||||
assertEquals(List.of(VSyncMode.OFF, VSyncMode.ON, VSyncMode.MAILBOX), VSyncMode.selectable(m -> true));
|
||||
void mailboxIsTakenWhenTheBackendOffersIt() {
|
||||
assertEquals(VSyncMode.MAILBOX, RenderBackend.resolveTearFreeMode(null, EVERYTHING));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoIsNeverSelectable() {
|
||||
assertFalse(VSyncMode.selectable(m -> true).contains(VSyncMode.AUTO));
|
||||
void vsyncIsTheFallbackWhenMailboxIsUnsupported() {
|
||||
assertEquals(VSyncMode.ON, RenderBackend.resolveTearFreeMode(null, NO_MAILBOX));
|
||||
assertEquals(VSyncMode.ON, RenderBackend.resolveTearFreeMode(VSyncMode.MAILBOX, NO_MAILBOX));
|
||||
}
|
||||
|
||||
@Test
|
||||
void anExplicitSupportedPreferenceWins() {
|
||||
assertEquals(VSyncMode.ON, RenderBackend.resolveTearFreeMode(VSyncMode.ON, EVERYTHING));
|
||||
assertEquals(VSyncMode.MAILBOX, RenderBackend.resolveTearFreeMode(VSyncMode.MAILBOX, EVERYTHING));
|
||||
}
|
||||
|
||||
@Test
|
||||
void offIsNeverATearFreePreference() {
|
||||
assertEquals(VSyncMode.MAILBOX, RenderBackend.resolveTearFreeMode(VSyncMode.OFF, EVERYTHING));
|
||||
assertEquals(VSyncMode.ON, RenderBackend.resolveTearFreeMode(VSyncMode.OFF, NO_MAILBOX));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -89,32 +62,4 @@ class BackendPolicyTest {
|
||||
assertEquals(0, RenderBackend.refreshHzFrom(-1, -1, -1.0f));
|
||||
assertEquals(0, RenderBackend.refreshHzFrom(144, 0, 0.0f));
|
||||
}
|
||||
|
||||
@Test
|
||||
void glRenderAheadKeepsZeroAsOff() {
|
||||
assertEquals(0, RenderBackend.clampRenderAhead(0, 0, 9));
|
||||
assertEquals(0, RenderBackend.clampRenderAhead(-1, 0, 9));
|
||||
}
|
||||
|
||||
@Test
|
||||
void glRenderAheadClampsToItsRange() {
|
||||
assertEquals(1, RenderBackend.clampRenderAhead(1, 0, 9));
|
||||
assertEquals(3, RenderBackend.clampRenderAhead(3, 0, 9));
|
||||
assertEquals(9, RenderBackend.clampRenderAhead(9, 0, 9));
|
||||
assertEquals(9, RenderBackend.clampRenderAhead(20, 0, 9));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sdlRenderAheadHasNoOffSwitch() {
|
||||
assertEquals(3, RenderBackend.clampRenderAhead(0, 1, 3));
|
||||
assertEquals(3, RenderBackend.clampRenderAhead(-1, 1, 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sdlRenderAheadClampsToItsRange() {
|
||||
assertEquals(1, RenderBackend.clampRenderAhead(1, 1, 3));
|
||||
assertEquals(2, RenderBackend.clampRenderAhead(2, 1, 3));
|
||||
assertEquals(3, RenderBackend.clampRenderAhead(3, 1, 3));
|
||||
assertEquals(3, RenderBackend.clampRenderAhead(6, 1, 3));
|
||||
}
|
||||
}
|
||||
@@ -331,6 +331,9 @@ class GLSMRedirectorTest {
|
||||
void phase2NameKeyedGapsRedirect() {
|
||||
assertRedirected("org/lwjgl/opengl/GL33C", "glQueryCounter", "(II)V", "glQueryCounter");
|
||||
assertRedirected("org/lwjgl/opengl/GL33C", "glGetQueryObjectui64", "(II)J", "glGetQueryObjectui64");
|
||||
assertRedirected("org/lwjgl/opengl/ARBTimerQuery", "glGetQueryObjectui64", "(II)J", "glGetQueryObjectui64");
|
||||
assertRedirected("org/lwjgl/opengl/EXTTimerQuery", "glGetQueryObjectuEXT", "(II)J", "glGetQueryObjectui64");
|
||||
assertRedirected("org/lwjgl/opengl/EXTTimerQuery", "glGetQueryObjectui64EXT", "(II)J", "glGetQueryObjectui64");
|
||||
assertRedirected("org/lwjgl/opengl/GL30C", "glBindFragDataLocation", "(IILjava/lang/CharSequence;)V", "glBindFragDataLocation");
|
||||
assertRedirected("org/lwjgl/opengl/GL11C", "glGetIntegerv", "(I[I)V", "glGetInteger");
|
||||
assertRedirected("org/lwjgl/opengl/GL20C", "glUniform4fv", "(I[F)V", "glUniform4");
|
||||
|
||||
@@ -9,7 +9,7 @@ asmCompile = "5.0.3"
|
||||
lwjgl3 = "3.4.2-20260602.093430-9"
|
||||
lwjgl3ify = "3.0.31"
|
||||
|
||||
celeritas = "2.5.9-GTNH"
|
||||
celeritas = "2.5.10-GTNH"
|
||||
gtnhlib = "0.11.36"
|
||||
eventbus = "7.0-beta.15"
|
||||
glslTransformLib = "0.2.0-33.g3ec381e"
|
||||
|
||||
+7
-2
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.glsm.GLESCaps;
|
||||
import com.gtnewhorizons.angelica.glsm.RenderSystem;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.DebugMessageHandler;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
|
||||
import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -171,10 +172,14 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
|
||||
public int getMinGLSLVersion() {return 330;}
|
||||
|
||||
@Override
|
||||
protected void setSwapInterval(boolean vsync) {SDLVideo.SDL_GL_SetSwapInterval(vsync ? 1 : 0);}
|
||||
protected VSyncMode applyVSyncMode(VSyncMode preferred) {
|
||||
final boolean enabled = preferred.tearFree();
|
||||
SDLVideo.SDL_GL_SetSwapInterval(enabled ? 1 : 0);
|
||||
return enabled ? VSyncMode.ON : VSyncMode.OFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDisplayRefreshRateHz() {
|
||||
protected int queryDisplayRefreshRateHz() {
|
||||
final long window = Display.getWindow();
|
||||
final int displayId = window == 0 ? SDLVideo.SDL_GetPrimaryDisplay() : SDLVideo.SDL_GetDisplayForWindow(window);
|
||||
if (displayId == 0) return 0;
|
||||
|
||||
@@ -59,6 +59,7 @@ dependencies {
|
||||
api(project(":glsm"))
|
||||
|
||||
compileOnly(libs.lwjgl3ify) { artifact { classifier = "dev" }; isTransitive = false }
|
||||
compileOnly(libs.retrofuturabootstrap) { isTransitive = false }
|
||||
compileOnly(libs.celeritas.common) { isTransitive = false }
|
||||
|
||||
// GL enum constants (compileOnly -- no runtime GL dependency)
|
||||
@@ -76,6 +77,7 @@ dependencies {
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testImplementation(libs.junit.jupiter)
|
||||
testImplementation(testFixtures(project(":glsm")))
|
||||
testImplementation(libs.celeritas.common) { isTransitive = false }
|
||||
testImplementation(libs.lwjgl3.opengl)
|
||||
// GL.<clinit> loads liblwjgl_opengl.so. Test scope only: a root-level runtimeOnly collides with lwjgl3Bindings.
|
||||
testRuntimeOnly(libs.lwjgl3.opengl) { artifact { classifier = lwjglNatives } }
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.lwjgl.sdl.SDLLog;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
import org.lwjgl.sdl.SDLProperties;
|
||||
import org.lwjgl.system.Platform;
|
||||
import org.lwjglx.Sys;
|
||||
@@ -100,6 +101,11 @@ public final class SDLGPUGate {
|
||||
ShaderManager.prewarmSpirv(transformedSource, glShaderType);
|
||||
}
|
||||
|
||||
public static void prewarmSpirv(String transformedSource, GLSLParser.Translation_unitContext bodyTree, int headerLen, int glShaderType) {
|
||||
if (!isActive()) return;
|
||||
ShaderManager.prewarmSpirv(transformedSource, bodyTree, headerLen, glShaderType);
|
||||
}
|
||||
|
||||
public static void clearShaderPrewarmCache() {
|
||||
if (!isActive()) return;
|
||||
ShaderManager.clearPrewarmCache();
|
||||
@@ -152,6 +158,9 @@ public final class SDLGPUGate {
|
||||
}
|
||||
|
||||
public static void fallBackToGL() {
|
||||
if (SDLGPULWJGLService.isOfferedAsAvailable()) {
|
||||
throw new IllegalStateException("Celeritas already selected the SDL GPU LWJGL service; falling back to OpenGL would leave it bound to a dead backend");
|
||||
}
|
||||
disarmed = true;
|
||||
engaged = false;
|
||||
deviceReady = false;
|
||||
|
||||
@@ -19,11 +19,17 @@ import java.util.Arrays;
|
||||
|
||||
/** SDL GPU implementation of {@link LWJGLService} for Celeritas */
|
||||
public final class SDLGPULWJGLService extends LWJGLService {
|
||||
|
||||
private static volatile boolean offeredAsAvailable;
|
||||
|
||||
public static boolean isOfferedAsAvailable() { return offeredAsAvailable; }
|
||||
|
||||
@Override public int getPriority() {
|
||||
if (SystemProperties.USE_SDL_GPU && SDLGPUGate.isSDLGPUAvailable() && SDLGPUGate.isEngaged()) {
|
||||
return 200;
|
||||
if (!SystemProperties.USE_SDL_GPU || !SDLGPUGate.isSDLGPUAvailable() || !SDLGPUGate.isEngaged()) {
|
||||
return PRIORITY_UNAVAILABLE;
|
||||
}
|
||||
return Integer.MIN_VALUE;
|
||||
offeredAsAvailable = true;
|
||||
return 200;
|
||||
}
|
||||
|
||||
@Override public boolean isOpenGLVersionSupported(int major, int minor) {
|
||||
@@ -148,22 +154,30 @@ public final class SDLGPULWJGLService extends LWJGLService {
|
||||
}
|
||||
@Override public void glDeleteSync(long sync) { BackendManager.RENDER_BACKEND.deleteSync(sync); }
|
||||
|
||||
private long[] queryTimestamps = new long[64];
|
||||
private long[] queryElapsed = new long[64];
|
||||
private int activeQueryId = -1;
|
||||
private long activeQueryStart;
|
||||
|
||||
private void ensureQueryCapacity(int id) {
|
||||
if (id >= queryTimestamps.length) {
|
||||
queryTimestamps = Arrays.copyOf(queryTimestamps, Math.max(id + 1, queryTimestamps.length * 2));
|
||||
if (id >= queryElapsed.length) {
|
||||
queryElapsed = Arrays.copyOf(queryElapsed, Math.max(id + 1, queryElapsed.length * 2));
|
||||
}
|
||||
}
|
||||
|
||||
@Override public int glGenQueries() { return BackendManager.RENDER_BACKEND.genQueries(); }
|
||||
@Override public void glDeleteQueries(int query) { }
|
||||
@Override public void glQueryCounter(int id, int target) {
|
||||
@Override public void glBeginQuery(int target, int id) {
|
||||
ensureQueryCapacity(id);
|
||||
queryTimestamps[id] = System.nanoTime();
|
||||
activeQueryId = id;
|
||||
activeQueryStart = System.nanoTime();
|
||||
}
|
||||
@Override public void glEndQuery(int target) {
|
||||
if (activeQueryId < 0) return;
|
||||
queryElapsed[activeQueryId] = System.nanoTime() - activeQueryStart;
|
||||
activeQueryId = -1;
|
||||
}
|
||||
@Override public long glGetQueryObjectui64(int id, int pname) {
|
||||
if (id >= 0 && id < queryTimestamps.length) return queryTimestamps[id];
|
||||
if (id >= 0 && id < queryElapsed.length) return queryElapsed[id];
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.FenceTracker;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager.FrameState;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.Presenter;
|
||||
import com.gtnewhorizons.retrofuturabootstrap.MainStartOnFirstThread;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.pipeline.DrawDispatch;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineApplier;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineCache;
|
||||
@@ -41,9 +43,10 @@ import com.gtnewhorizons.angelica.sdlgpu.sampler.StorageBufferBinder;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.sampler.StorageTextureBinder;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.splash.SplashDispatcher;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.splash.SplashOffscreenTarget;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.OffscreenTarget;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.util.DebugLabels;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.util.DebugMessageRelay;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.util.ThreadRegistry;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
@@ -80,8 +83,8 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.lwjgl.sdl.SDLGPU;
|
||||
@@ -108,6 +111,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
|
||||
private final Device device = SDLGPUGate.device();
|
||||
private final FrameManager frameManager = new FrameManager(device);
|
||||
private Presenter presenter;
|
||||
private final ResourceManager resourceManager = new ResourceManager(device, frameManager);
|
||||
private final Image3DClear image3DClear = new Image3DClear();
|
||||
private final ShaderManager shaderManager = new ShaderManager(device);
|
||||
@@ -151,33 +155,30 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
|
||||
private volatile boolean shutdown;
|
||||
|
||||
private SplashOffscreenTarget splashTarget;
|
||||
private OffscreenTarget splashTarget;
|
||||
|
||||
private static final CopyOnWriteArrayList<ContextState> registeredStates = new CopyOnWriteArrayList<>();
|
||||
private static final ThreadLocal<ContextState> tlState = ThreadLocal.withInitial(() -> {
|
||||
private static final ThreadRegistry<ContextState> registeredStates = new ThreadRegistry<>(new ContextState[0]);
|
||||
private static final ThreadLocal<ContextState> tlState = new ThreadLocal<>();
|
||||
|
||||
private static ContextState s() {
|
||||
final ContextState sole = registeredStates.sole();
|
||||
if (sole != null && sole.owner == Thread.currentThread()) return sole;
|
||||
final ContextState st = tlState.get();
|
||||
return st != null ? st : registerState();
|
||||
}
|
||||
|
||||
private static ContextState registerState() {
|
||||
final ContextState st = new ContextState();
|
||||
for (int i = 0; i < ContextState.MAX_VERTEX_ATTRIBS; i++) st.attribDefaults[i * 4 + 3] = 1.0f;
|
||||
st.attribDefaults[4] = 1.0f; st.attribDefaults[5] = 1.0f;
|
||||
st.attribDefaults[6] = 1.0f;
|
||||
tlState.set(st);
|
||||
registeredStates.add(st);
|
||||
refreshSoleState();
|
||||
return st;
|
||||
});
|
||||
|
||||
private static volatile ContextState soleState;
|
||||
|
||||
private static void refreshSoleState() {
|
||||
soleState = registeredStates.size() == 1 ? registeredStates.get(0) : null;
|
||||
}
|
||||
|
||||
private static ContextState s() {
|
||||
final ContextState st = soleState;
|
||||
if (st != null && st.owner == Thread.currentThread()) return st;
|
||||
return tlState.get();
|
||||
}
|
||||
|
||||
private void markAllPipelineInputDirty() {
|
||||
for (ContextState st : registeredStates) st.pipeline.markInputDirty();
|
||||
for (ContextState st : registeredStates.snapshot()) st.pipeline.markInputDirty();
|
||||
}
|
||||
|
||||
public static final int CTR_SLOT_WRITES = 0;
|
||||
@@ -189,7 +190,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
|
||||
public static void takeContextCounters(int[] out) {
|
||||
Arrays.fill(out, 0);
|
||||
for (ContextState st : registeredStates) {
|
||||
for (ContextState st : registeredStates.snapshot()) {
|
||||
out[CTR_SLOT_WRITES] += st.slotWrites;
|
||||
out[CTR_SLOT_WRITES_ELIDED] += st.slotWritesElided;
|
||||
out[CTR_SSBO_BINDS] += st.ssboBinds;
|
||||
@@ -204,7 +205,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
}
|
||||
|
||||
private void releaseUniformStagingAllThreads(ShaderManager.ProgramObject prog) {
|
||||
for (ContextState st : registeredStates) st.releaseUniformStaging(prog);
|
||||
for (ContextState st : registeredStates.snapshot()) st.releaseUniformStaging(prog);
|
||||
}
|
||||
|
||||
|
||||
@@ -290,13 +291,19 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
resourceManager.setBufferLivenessListener(this::markAllPipelineInputDirty);
|
||||
shaderManager.setUniformReleaseListener(this::releaseUniformStagingAllThreads);
|
||||
fenceTracker.setUnresolvedFenceFlush(this::midFrameFenceFlush);
|
||||
frameManager.setMainThread(GLStateManager.getMainThread());
|
||||
if (!SystemProperties.DISABLE_SDL_PRESENTER_THREAD) {
|
||||
presenter = new Presenter(frameManager, MainStartOnFirstThread.instance());
|
||||
frameManager.setPresenter(presenter);
|
||||
LOG.info("SDL presenter thread enabled: presents run on '{}'", device.getWindowThread() != null ? device.getWindowThread().getName() : "window thread");
|
||||
}
|
||||
|
||||
final int splashW = Math.max(1, Display.getWidth());
|
||||
final int splashH = Math.max(1, Display.getHeight());
|
||||
final int[] maxDesktop = device.getMaxDesktopSizePixels();
|
||||
final int splashTexW = Math.max(splashW, maxDesktop[0]);
|
||||
final int splashTexH = Math.max(splashH, maxDesktop[1]);
|
||||
splashTarget = new SplashOffscreenTarget();
|
||||
splashTarget = new OffscreenTarget();
|
||||
splashTarget.create(device, resourceManager, splashTexW, splashTexH, device.getSwapchainTextureFormat());
|
||||
SplashDispatcher.seedDrawnSize(splashW, splashH);
|
||||
LOG.info("Splash offscreen target {}x{} (window {}x{}, max desktop {}x{})", splashTexW, splashTexH, splashW, splashH, maxDesktop[0], maxDesktop[1]);
|
||||
@@ -376,31 +383,50 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
LOG.info("Populated GL capabilities for SDL GPU backend");
|
||||
}
|
||||
|
||||
@Override protected void setSwapInterval(boolean vsync) {
|
||||
device.setVSyncMode(VSyncMode.AUTO, vsync);
|
||||
}
|
||||
private final AtomicReference<VSyncMode> pendingVSync = new AtomicReference<>();
|
||||
|
||||
@Override protected VSyncMode applyVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
|
||||
return device.setVSyncMode(preferred, vsyncEnabled);
|
||||
@Override protected VSyncMode applyVSyncMode(VSyncMode preferred) {
|
||||
pendingVSync.set(preferred);
|
||||
return device.chooseVSyncMode(preferred);
|
||||
}
|
||||
|
||||
@Override public boolean supportsVSyncMode(VSyncMode mode) {
|
||||
return mode == VSyncMode.AUTO || device.supportsVSyncMode(mode);
|
||||
return device.supportsVSyncMode(mode);
|
||||
}
|
||||
|
||||
@Override public int getMinRenderAhead() { return Device.MIN_FRAMES_IN_FLIGHT; }
|
||||
private void applyPendingVSync() {
|
||||
final VSyncMode preferred = pendingVSync.getAndSet(null);
|
||||
if (preferred != null) {
|
||||
if (presenter != null) presenter.drain();
|
||||
publishVSyncMode(device.applyVSync(preferred));
|
||||
}
|
||||
}
|
||||
|
||||
@Override public int getMaxRenderAhead() { return Device.MAX_FRAMES_IN_FLIGHT; }
|
||||
|
||||
@Override public void setRenderAheadLimit(int frames) { device.setFramesInFlight(frames); }
|
||||
|
||||
@Override public int getDisplayRefreshRateHz() {
|
||||
@Override protected int queryDisplayRefreshRateHz() {
|
||||
return device.getDisplayRefreshRateHz();
|
||||
}
|
||||
|
||||
@Override public boolean gateAnchorsNextFrameStart() { return true; }
|
||||
|
||||
@Override public boolean wantsDisplayUpdateGateTiming() { return false; }
|
||||
|
||||
@Override public long lastFrameGateNanos() {
|
||||
if (presenter != null && presenter.isEngaged()) return frameManager.windowFrame().lastFrameGateNanos;
|
||||
return frameManager.lastFrameGateNanos();
|
||||
}
|
||||
|
||||
@Override public long lastFrameGateEndNanos() {
|
||||
if (presenter != null && presenter.isEngaged()) return frameManager.windowFrame().lastFrameGateEndNanos;
|
||||
return frameManager.lastFrameGateEndNanos();
|
||||
}
|
||||
|
||||
@Override public boolean hasSwapchainBackpressure() { return true; }
|
||||
|
||||
@Override public void shutdown() {
|
||||
if (shutdown) return;
|
||||
shutdown = true;
|
||||
if (presenter != null) presenter.drain();
|
||||
frameManager.destroyFinalTarget();
|
||||
if (splashTarget != null) {
|
||||
splashTarget.destroy(resourceManager);
|
||||
splashTarget = null;
|
||||
@@ -423,7 +449,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
fenceTracker.dispose();
|
||||
frameManager.releaseAllRegisteredFrames();
|
||||
resourceManager.shutdownTexSamplerStates();
|
||||
for (ContextState st : registeredStates) {
|
||||
for (ContextState st : registeredStates.snapshot()) {
|
||||
if (st.fanIndexBuffer != 0) {
|
||||
resourceManager.releaseBufferHandle(st.fanIndexBuffer);
|
||||
st.fanIndexBuffer = 0;
|
||||
@@ -441,7 +467,6 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
clearMappedState(st);
|
||||
}
|
||||
registeredStates.clear();
|
||||
soleState = null;
|
||||
|
||||
drawDispatch.clearWarnedState();
|
||||
pipelineStore.shutdown();
|
||||
@@ -480,13 +505,16 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
}
|
||||
|
||||
@Override public void onRenderThreadReleased(Thread t) {
|
||||
if (shutdown || t == GLStateManager.getMainThread()) return;
|
||||
if (frameManager.isFrameActive()) {
|
||||
frameManager.endFrame();
|
||||
}
|
||||
frameManager.releaseThreadState();
|
||||
registeredStates.remove(tlState.get());
|
||||
tlState.remove();
|
||||
refreshSoleState();
|
||||
final ContextState st = tlState.get();
|
||||
if (st != null) {
|
||||
tlState.remove();
|
||||
registeredStates.remove(st);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public boolean handleMakeCurrent(Object drawable) {
|
||||
@@ -500,12 +528,11 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
}
|
||||
|
||||
@Override public boolean handleReleaseContext(Object drawable) {
|
||||
if (isSDLManagedDrawable(drawable)) {
|
||||
Lwjgl3GLCapabilitiesShim.clearOnCurrentThread();
|
||||
onRenderThreadReleased(Thread.currentThread());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (!isSDLManagedDrawable(drawable)) return false;
|
||||
if (Thread.currentThread() == GLStateManager.getMainThread()) return true;
|
||||
Lwjgl3GLCapabilitiesShim.clearOnCurrentThread();
|
||||
onRenderThreadReleased(Thread.currentThread());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isSDLManagedDrawable(Object drawable) {
|
||||
@@ -528,33 +555,19 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void dispatchSplashBlit(SplashOffscreenTarget target) {
|
||||
public void dispatchSplashBlit(OffscreenTarget target) {
|
||||
if (shutdown || target == null) return;
|
||||
onFrameBegin();
|
||||
final FrameManager.FrameState f = frameManager.frame();
|
||||
if (!frameManager.ensureSwapchainAcquired(f)) {
|
||||
onFrameEnd();
|
||||
return;
|
||||
}
|
||||
frameManager.endCopyPassIfActive();
|
||||
frameManager.endRenderPassIfActive();
|
||||
final long drawnSize = SplashDispatcher.getDrawnSize();
|
||||
final int srcW = Math.max(1, Math.min((int) (drawnSize >> 32), target.width()));
|
||||
final int srcH = Math.max(1, Math.min((int) drawnSize, target.height()));
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
final var info = SDL_GPUBlitInfo.calloc(stack);
|
||||
info.source(s -> s.texture(target.colorTexture()).x(0).y(0).w(srcW).h(srcH));
|
||||
info.destination(d -> d.texture(f.swapchainTexture).x(0).y(0).w(f.swapchainWidth).h(f.swapchainHeight));
|
||||
info.filter(SDL_GPU_FILTER_LINEAR);
|
||||
info.flip_mode(SDLSurface.SDL_FLIP_VERTICAL);
|
||||
SDL_BlitGPUTexture(f.commandBuffer, info);
|
||||
}
|
||||
f.swapchainUsedThisFrame = true;
|
||||
onFrameEnd();
|
||||
final FrameState f = frameManager.frame();
|
||||
frameManager.presentBlit(f, target.colorTexture(), srcW, srcH, SDLSurface.SDL_FLIP_VERTICAL);
|
||||
f.presentedThisFrame = false;
|
||||
}
|
||||
|
||||
@Override public void onFrameBegin() {
|
||||
if (shutdown) return;
|
||||
if (GLStateManager.isSplashComplete()) applyPendingVSync();
|
||||
frameManager.beginFrame();
|
||||
beginFrameInit();
|
||||
}
|
||||
@@ -636,22 +649,24 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
endFrameUploadFlushNoWait();
|
||||
|
||||
final FrameState f = frameManager.frame();
|
||||
if (f.frameActive && !f.swapchainUsedThisFrame) {
|
||||
if (f.frameActive && !f.fbo0UsedThisFrame) {
|
||||
if (!emptyFrameWarned) {
|
||||
emptyFrameWarned = true;
|
||||
LOG.info("Frame {} ended without drawing to the swapchain; skipping present (further occurrences counted in sdl.emptyFrames)", f.frameNumber);
|
||||
LOG.info("Frame {} ended without drawing to FBO 0; presenting the previous contents (further occurrences counted in sdl.emptyFrames)", f.frameNumber);
|
||||
}
|
||||
frameManager.markFrameEmpty(f);
|
||||
}
|
||||
|
||||
awaitUploadFlush();
|
||||
frameManager.endFrame();
|
||||
frameManager.presentFinalTarget();
|
||||
fenceTracker.resolvePendingFences();
|
||||
resourceManager.flushDeferredReleases();
|
||||
resourceManager.recycleGpuBufferPool(frameManager.getFrameNumber());
|
||||
}
|
||||
|
||||
@Override public void onPreSwapchainInvalidatingChange(Object change) {
|
||||
if (presenter != null) presenter.drain();
|
||||
endFrameUploadFlush();
|
||||
if (frameManager.isFrameActive()) {
|
||||
frameManager.endFrame();
|
||||
@@ -700,54 +715,6 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
return transferThread != null ? transferThread.getDebugInfo() : null;
|
||||
}
|
||||
|
||||
private static final long PRESENT_STATS_WINDOW_NANOS = 1_000_000_000L;
|
||||
private static final long PRESENT_STATS_STALE_NANOS = 2_000_000_000L;
|
||||
|
||||
private long presentStatsNanos;
|
||||
private long presentStatsLoopFrames;
|
||||
private long presentStatsPresentedFrames;
|
||||
private long presentStatsSkips;
|
||||
private long presentStatsEmpty;
|
||||
private long presentStatsAcquireNanos;
|
||||
private String presentDebugLine;
|
||||
|
||||
@Override public String getPresentDebugInfo() {
|
||||
if (shutdown) return null;
|
||||
final long now = System.nanoTime();
|
||||
final long elapsed = now - presentStatsNanos;
|
||||
|
||||
if (presentStatsNanos == 0L || elapsed > PRESENT_STATS_STALE_NANOS) {
|
||||
snapshotPresentStats(now);
|
||||
presentDebugLine = null;
|
||||
return null;
|
||||
}
|
||||
if (elapsed < PRESENT_STATS_WINDOW_NANOS) return presentDebugLine;
|
||||
|
||||
final long loopDelta = frameManager.statLoopFrames() - presentStatsLoopFrames;
|
||||
final long presentedDelta = frameManager.statPresentedFrames() - presentStatsPresentedFrames;
|
||||
final long skipDelta = frameManager.statPresentSkips() - presentStatsSkips;
|
||||
final long emptyDelta = frameManager.statEmptyFrames() - presentStatsEmpty;
|
||||
final long acquireDelta = frameManager.statAcquireWaitNanos() - presentStatsAcquireNanos;
|
||||
final double seconds = elapsed / 1.0e9;
|
||||
presentDebugLine = String.format("Present: %s %dHz | loop %.0f/s shown %.0f/s | skip %.0f/s empty %.0f/s | acq %.2fms | fif %d",
|
||||
getEffectiveVSyncMode(), device.getDisplayRefreshRateHz(),
|
||||
loopDelta / seconds, presentedDelta / seconds,
|
||||
skipDelta / seconds, emptyDelta / seconds,
|
||||
loopDelta == 0 ? 0.0 : acquireDelta / (double) loopDelta / 1.0e6,
|
||||
Device.framesInFlight());
|
||||
snapshotPresentStats(now);
|
||||
return presentDebugLine;
|
||||
}
|
||||
|
||||
private void snapshotPresentStats(long now) {
|
||||
presentStatsNanos = now;
|
||||
presentStatsLoopFrames = frameManager.statLoopFrames();
|
||||
presentStatsPresentedFrames = frameManager.statPresentedFrames();
|
||||
presentStatsSkips = frameManager.statPresentSkips();
|
||||
presentStatsEmpty = frameManager.statEmptyFrames();
|
||||
presentStatsAcquireNanos = frameManager.statAcquireWaitNanos();
|
||||
}
|
||||
|
||||
@Override public void enable(int cap) { setBoolCap(cap, true); }
|
||||
@Override public void enablei(int cap, int index) { setBoolCapIndexed(cap, index, true); }
|
||||
@Override public void disable(int cap) { setBoolCap(cap, false); }
|
||||
@@ -952,7 +919,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
|
||||
final ContextState st = cs;
|
||||
|
||||
// Swapchain path: just record pending state; ensureSwapchainRenderPass consumes it lazily.
|
||||
// FBO0 path: just record pending state; ensureFbo0RenderPass consumes it lazily.
|
||||
if (st.boundFboId == 0) {
|
||||
if (wantColor) {
|
||||
st.pendingSwapchainClear = true;
|
||||
@@ -1710,7 +1677,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
final long texHandle;
|
||||
final int srcSdlFormat;
|
||||
if (cs.boundReadFboId == 0) {
|
||||
texHandle = frameManager.getSwapchainTexture();
|
||||
texHandle = frameManager.getFbo0Texture();
|
||||
srcSdlFormat = frameManager.getSwapchainFormat();
|
||||
} else {
|
||||
final FboState fbo = resourceManager.getFbo(cs.boundReadFboId);
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.lwjgl.system.Platform;
|
||||
import org.lwjglx.opengl.Display;
|
||||
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.lwjgl.sdl.SDLGPU.*;
|
||||
@@ -31,13 +32,11 @@ import static org.lwjgl.system.MemoryUtil.memUTF8;
|
||||
public final class Device {
|
||||
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
|
||||
|
||||
public static final int MIN_FRAMES_IN_FLIGHT = 1;
|
||||
public static final int DEFAULT_FRAMES_IN_FLIGHT = 2;
|
||||
public static final int MAX_FRAMES_IN_FLIGHT = 3;
|
||||
|
||||
private static volatile int framesInFlight = DEFAULT_FRAMES_IN_FLIGHT;
|
||||
private static final int FRAMES_IN_FLIGHT = Math.min(Math.max(SystemProperties.SDL_FRAMES_IN_FLIGHT, 1), MAX_FRAMES_IN_FLIGHT);
|
||||
|
||||
public static int framesInFlight() { return framesInFlight; }
|
||||
public static int framesInFlight() { return FRAMES_IN_FLIGHT; }
|
||||
|
||||
private static final int REQUESTED_SHADER_FORMATS = SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXBC | SDL_GPU_SHADERFORMAT_DXIL;
|
||||
|
||||
@@ -49,7 +48,7 @@ public final class Device {
|
||||
private String driverVersion;
|
||||
private String driverInfo;
|
||||
private VSyncMode vsyncMode = VSyncMode.ON;
|
||||
private volatile String windowThreadName;
|
||||
private volatile Thread windowThread;
|
||||
private SDL_LogOutputFunction logCallback; // prevent GC
|
||||
|
||||
private volatile boolean lost;
|
||||
@@ -155,7 +154,13 @@ public final class Device {
|
||||
throw new RuntimeException("Failed to claim window for SDL GPU device: " + SDLError.SDL_GetError());
|
||||
}
|
||||
claimed = true;
|
||||
windowThreadName = Thread.currentThread().getName();
|
||||
windowThread = Thread.currentThread();
|
||||
|
||||
if (!SDL_SetGPUAllowedFramesInFlight(device, FRAMES_IN_FLIGHT)) {
|
||||
LOG.warn("SDL_SetGPUAllowedFramesInFlight({}) failed: {}", FRAMES_IN_FLIGHT, SDLError.SDL_GetError());
|
||||
} else {
|
||||
LOG.info("SDL frames-in-flight set to {}", FRAMES_IN_FLIGHT);
|
||||
}
|
||||
|
||||
logWindowDiagnostics(window);
|
||||
LOG.info("SDL GPU device claimed window successfully");
|
||||
@@ -263,8 +268,8 @@ public final class Device {
|
||||
return driverName;
|
||||
}
|
||||
|
||||
public String getWindowThreadName() {
|
||||
return windowThreadName;
|
||||
public Thread getWindowThread() {
|
||||
return windowThread;
|
||||
}
|
||||
|
||||
public String getDeviceName() {
|
||||
@@ -295,34 +300,48 @@ public final class Device {
|
||||
return (supportedShaderFormats & SDL_GPU_SHADERFORMAT_DXBC) != 0;
|
||||
}
|
||||
|
||||
public void setFramesInFlight(int frames) {
|
||||
final int n = Math.min(Math.max(frames, MIN_FRAMES_IN_FLIGHT), MAX_FRAMES_IN_FLIGHT);
|
||||
if (n == framesInFlight || device == 0 || !claimed) return;
|
||||
if (!SDL_SetGPUAllowedFramesInFlight(device, n)) {
|
||||
LOG.warn("SDL_SetGPUAllowedFramesInFlight({}) failed: {}; keeping {}", n, SDLError.SDL_GetError(), framesInFlight);
|
||||
return;
|
||||
}
|
||||
framesInFlight = n;
|
||||
LOG.info("SDL frames-in-flight set to {}", n);
|
||||
private static final List<VSyncMode> ON_ORDER = List.of(VSyncMode.ON);
|
||||
private static final List<VSyncMode> OFF_ORDER = List.of(VSyncMode.OFF, VSyncMode.ON);
|
||||
private static final List<VSyncMode> MAILBOX_ORDER = List.of(VSyncMode.MAILBOX, VSyncMode.ON);
|
||||
|
||||
private static List<VSyncMode> presentModeOrder(VSyncMode preferred) {
|
||||
return switch (preferred) {
|
||||
case MAILBOX -> MAILBOX_ORDER;
|
||||
case OFF -> OFF_ORDER;
|
||||
default -> ON_ORDER;
|
||||
};
|
||||
}
|
||||
|
||||
public VSyncMode setVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
|
||||
public VSyncMode chooseVSyncMode(VSyncMode preferred) {
|
||||
for (VSyncMode candidate : presentModeOrder(preferred)) {
|
||||
if (supportsVSyncMode(candidate)) return candidate;
|
||||
}
|
||||
return vsyncMode;
|
||||
}
|
||||
|
||||
public VSyncMode applyVSync(VSyncMode preferred) {
|
||||
if (device == 0 || !claimed) return vsyncMode;
|
||||
final long window = Display.getWindow();
|
||||
final VSyncMode requested = preferred.resolve(vsyncEnabled);
|
||||
for (VSyncMode candidate : requested.fallbackOrder()) {
|
||||
final List<VSyncMode> order = presentModeOrder(preferred);
|
||||
for (VSyncMode candidate : order) {
|
||||
final int sdlMode = toSdl(candidate);
|
||||
if (!SDL_WindowSupportsGPUPresentMode(device, window, sdlMode)) continue;
|
||||
if (!SDL_SetGPUSwapchainParameters(device, window, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, sdlMode)) {
|
||||
LOG.warn("Failed to set present mode {}: {}", candidate, SDLError.SDL_GetError());
|
||||
continue;
|
||||
}
|
||||
if (candidate != order.get(0)) {
|
||||
LOG.warn("Present mode {} is not supported by this window; using {} instead", order.get(0), candidate);
|
||||
}
|
||||
vsyncMode = candidate;
|
||||
LOG.info("SDL present mode {} (requested {}, preference {}, vsync={}), refresh {}Hz, supported: vsync={} mailbox={} immediate={}",
|
||||
candidate, requested, preferred, vsyncEnabled, getDisplayRefreshRateHz(),
|
||||
supportsVSyncMode(VSyncMode.ON), supportsVSyncMode(VSyncMode.MAILBOX), supportsVSyncMode(VSyncMode.OFF));
|
||||
LOG.info("SDL present mode {} (preferred={}), refresh {}Hz, {}, video={}, framesInFlight={}, supported: vsync={} immediate={} mailbox={}",
|
||||
candidate, preferred, getDisplayRefreshRateHz(),
|
||||
(SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0 ? "fullscreen" : "windowed",
|
||||
SDL_GetCurrentVideoDriver(), FRAMES_IN_FLIGHT,
|
||||
supportsVSyncMode(VSyncMode.ON), supportsVSyncMode(VSyncMode.OFF), supportsVSyncMode(VSyncMode.MAILBOX));
|
||||
return candidate;
|
||||
}
|
||||
LOG.warn("No usable present mode for request {}; keeping {}", requested, vsyncMode);
|
||||
LOG.warn("No usable present mode for preference {}; keeping {}", preferred, vsyncMode);
|
||||
return vsyncMode;
|
||||
}
|
||||
|
||||
@@ -330,6 +349,25 @@ public final class Device {
|
||||
return device != 0 && claimed && SDL_WindowSupportsGPUPresentMode(device, Display.getWindow(), toSdl(mode));
|
||||
}
|
||||
|
||||
public long getClaimedWindow() {
|
||||
return device != 0 && claimed ? Display.getWindow() : 0L;
|
||||
}
|
||||
|
||||
public boolean wasWindowResized() {
|
||||
return getClaimedWindow() != 0L && Display.wasResized();
|
||||
}
|
||||
|
||||
public long getWindowSizeInPixels() {
|
||||
final long window = getClaimedWindow();
|
||||
if (window == 0) return 0L;
|
||||
try (MemoryStack stack = MemoryStack.stackPush()) {
|
||||
final IntBuffer w = stack.mallocInt(1);
|
||||
final IntBuffer h = stack.mallocInt(1);
|
||||
SDL_GetWindowSizeInPixels(window, w, h);
|
||||
return ((long) w.get(0) << 32) | (h.get(0) & 0xFFFFFFFFL);
|
||||
}
|
||||
}
|
||||
|
||||
private static int toSdl(VSyncMode mode) {
|
||||
return switch (mode) {
|
||||
case OFF -> SDL_GPU_PRESENTMODE_IMMEDIATE;
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.util.MemoryAccess;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.util.ThreadRegistry;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.resource.UploadArena;
|
||||
@@ -16,16 +17,19 @@ import org.lwjgl.PointerBuffer;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.lwjgl.sdl.SDLError;
|
||||
import org.lwjgl.sdl.SDL_FColor;
|
||||
import org.lwjgl.sdl.SDL_GPUBlitInfo;
|
||||
import org.lwjgl.sdl.SDL_GPUBlitRegion;
|
||||
import org.lwjgl.sdl.SDL_GPUColorTargetInfo;
|
||||
import org.lwjgl.sdl.SDL_GPUDepthStencilTargetInfo;
|
||||
import org.lwjglx.opengl.Display;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.lwjgl.sdl.SDLGPU.*;
|
||||
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_NONE;
|
||||
import static org.lwjgl.system.MemoryStack.*;
|
||||
|
||||
/**
|
||||
@@ -47,9 +51,6 @@ public final class FrameManager {
|
||||
public long renderPass;
|
||||
public long copyPass;
|
||||
public boolean computePassOpen;
|
||||
public long swapchainTexture;
|
||||
public int swapchainWidth;
|
||||
public int swapchainHeight;
|
||||
public boolean frameActive;
|
||||
public long currentColorTarget;
|
||||
public long currentDepthTarget;
|
||||
@@ -57,10 +58,10 @@ public final class FrameManager {
|
||||
public long frameNumber;
|
||||
public boolean clearedThisFrame;
|
||||
public boolean depthClearedThisFrame;
|
||||
public boolean swapchainUsedThisFrame;
|
||||
public boolean swapchainAcquiredThisFrame;
|
||||
public boolean fbo0UsedThisFrame;
|
||||
public boolean presentedThisFrame;
|
||||
public boolean swapchainUnavailable;
|
||||
public boolean skipPresent;
|
||||
public boolean windowThreadChecked;
|
||||
public long renderPassGeneration;
|
||||
|
||||
public boolean wantFenceOnNextSubmit;
|
||||
@@ -82,7 +83,12 @@ public final class FrameManager {
|
||||
public int batchBreakSamplerThisFrame;
|
||||
public int stateAppliesThisFrame;
|
||||
public long acquireWaitNanosThisFrame;
|
||||
public volatile long lastFrameGateNanos;
|
||||
public volatile long lastFrameGateEndNanos;
|
||||
public int midFrameSubmitsThisFrame;
|
||||
public int mipGensThisFrame;
|
||||
public int mipGenSubmitsThisFrame;
|
||||
public int uploadFlushSubmitsThisFrame;
|
||||
public int blitsThisFrame;
|
||||
public int presentSkipsThisFrame;
|
||||
public int emptyFramesThisFrame;
|
||||
@@ -143,53 +149,52 @@ public final class FrameManager {
|
||||
|
||||
private volatile boolean globalSyncAtNextBeginFrame;
|
||||
|
||||
private final CopyOnWriteArrayList<FrameState> registeredFrames = new CopyOnWriteArrayList<>();
|
||||
private final ThreadRegistry<FrameState> registeredFrames = new ThreadRegistry<>(new FrameState[0]);
|
||||
|
||||
private volatile FrameState soleFrame;
|
||||
|
||||
private final ThreadLocal<FrameState> tlFrame = ThreadLocal.withInitial(() -> {
|
||||
final FrameState f = new FrameState();
|
||||
registeredFrames.add(f);
|
||||
refreshSoleFrame();
|
||||
return f;
|
||||
});
|
||||
|
||||
private void refreshSoleFrame() {
|
||||
soleFrame = registeredFrames.size() == 1 ? registeredFrames.get(0) : null;
|
||||
}
|
||||
private final ThreadLocal<FrameState> tlFrame = new ThreadLocal<>();
|
||||
|
||||
public FrameState frame() {
|
||||
final FrameState f = soleFrame;
|
||||
if (f != null && f.owner == Thread.currentThread()) return f;
|
||||
return tlFrame.get();
|
||||
final FrameState sole = registeredFrames.sole();
|
||||
if (sole != null && sole.owner == Thread.currentThread()) return sole;
|
||||
final FrameState f = tlFrame.get();
|
||||
return f != null ? f : registerFrame();
|
||||
}
|
||||
|
||||
private FrameState registerFrame() {
|
||||
final FrameState f = new FrameState();
|
||||
tlFrame.set(f);
|
||||
registeredFrames.add(f);
|
||||
return f;
|
||||
}
|
||||
|
||||
public int registeredFrameCount() {
|
||||
return registeredFrames.size();
|
||||
}
|
||||
|
||||
public FrameState busiestFrame() {
|
||||
final FrameState[] frames = registeredFrames.snapshot();
|
||||
FrameState best = null;
|
||||
for (final FrameState f : registeredFrames) {
|
||||
for (final FrameState f : frames) {
|
||||
if (best == null || f.frameNumber > best.frameNumber) best = f;
|
||||
}
|
||||
return best != null ? best : frame();
|
||||
}
|
||||
|
||||
public static final long SWAPCHAIN_LAYOUT_HASH = 0x5CA1AB1E5CA1AB1EL;
|
||||
public static final long FBO0_LAYOUT_HASH = 0x5CA1AB1E5CA1AB1EL;
|
||||
|
||||
private final OffscreenTarget finalTarget = new OffscreenTarget();
|
||||
|
||||
public OffscreenTarget finalTarget() { return finalTarget; }
|
||||
|
||||
public void destroyFinalTarget() {
|
||||
if (resourceManager != null) finalTarget.destroy(resourceManager);
|
||||
}
|
||||
|
||||
public static final long MAX_PENDING_UPLOAD_BYTES = 16L * 1024L * 1024L;
|
||||
public static final int MAX_PENDING_UPLOAD_COMMANDS = 256;
|
||||
|
||||
private Runnable beforeEndCopyPass;
|
||||
|
||||
private long statLoopFrames;
|
||||
private long statPresentedFrames;
|
||||
private long statPresentSkips;
|
||||
private long statEmptyFrames;
|
||||
private long statAcquireWaitNanos;
|
||||
|
||||
public long statLoopFrames() { return statLoopFrames; }
|
||||
public long statPresentedFrames() { return statPresentedFrames; }
|
||||
public long statPresentSkips() { return statPresentSkips; }
|
||||
public long statEmptyFrames() { return statEmptyFrames; }
|
||||
public long statAcquireWaitNanos() { return statAcquireWaitNanos; }
|
||||
|
||||
public FrameManager(Device device) {
|
||||
this.device = device;
|
||||
@@ -199,24 +204,12 @@ public final class FrameManager {
|
||||
this.resourceManager = resourceManager;
|
||||
}
|
||||
|
||||
private static volatile boolean threadLogged;
|
||||
|
||||
public void beginFrame() {
|
||||
final FrameState f = frame();
|
||||
if (f.frameActive) {
|
||||
LOG.warn("beginFrame() called while frame already active on thread {}", Thread.currentThread().getName());
|
||||
return;
|
||||
}
|
||||
if (!threadLogged) {
|
||||
threadLogged = true;
|
||||
final String renderThread = Thread.currentThread().getName();
|
||||
final String windowThread = device.getWindowThreadName();
|
||||
if (windowThread != null && !windowThread.equals(renderThread)) {
|
||||
LOG.warn("Frames are recorded on '{}' but the SDL window was created on '{}'; SDL requires swapchain acquire/wait on the window-creating thread", renderThread, windowThread);
|
||||
} else {
|
||||
LOG.info("First beginFrame() on thread={}", renderThread);
|
||||
}
|
||||
}
|
||||
|
||||
flushPendingUploadCommandBuffer(f);
|
||||
if (globalSyncAtNextBeginFrame) {
|
||||
@@ -231,11 +224,10 @@ public final class FrameManager {
|
||||
throw new RuntimeException("Failed to acquire GPU command buffer: " + SDLError.SDL_GetError());
|
||||
}
|
||||
|
||||
f.swapchainTexture = 0;
|
||||
f.swapchainWidth = 0;
|
||||
f.swapchainHeight = 0;
|
||||
f.swapchainAcquiredThisFrame = false;
|
||||
f.swapchainUnavailable = false;
|
||||
f.presentedThisFrame = false;
|
||||
|
||||
maintainFinalTarget();
|
||||
|
||||
f.frameActive = true;
|
||||
f.renderPass = 0;
|
||||
@@ -244,25 +236,53 @@ public final class FrameManager {
|
||||
f.frameNumber++;
|
||||
f.clearedThisFrame = false;
|
||||
f.depthClearedThisFrame = false;
|
||||
f.swapchainUsedThisFrame = false;
|
||||
f.skipPresent = false;
|
||||
f.fbo0UsedThisFrame = false;
|
||||
}
|
||||
|
||||
private static final long FINAL_TARGET_SIZE_POLL_NANOS = 1_000_000_000L;
|
||||
|
||||
private long finalTargetPollNanos;
|
||||
private volatile Thread mainThread;
|
||||
|
||||
public void setMainThread(Thread thread) {
|
||||
this.mainThread = thread;
|
||||
}
|
||||
|
||||
// Resizing releases a texture an in-flight present may still reference, so drain before ensureSize.
|
||||
private void maintainFinalTarget() {
|
||||
if (resourceManager == null) return;
|
||||
final Thread owner = mainThread;
|
||||
if (owner != null && Thread.currentThread() != owner) return;
|
||||
|
||||
final long now = System.nanoTime();
|
||||
final boolean poll = finalTargetPollNanos == 0L || now - finalTargetPollNanos >= FINAL_TARGET_SIZE_POLL_NANOS;
|
||||
if (!poll && !device.wasWindowResized() && finalTarget.isReady()) return;
|
||||
if (poll) finalTargetPollNanos = now;
|
||||
|
||||
final long size = device.getWindowSizeInPixels();
|
||||
if (size == 0L) return;
|
||||
final int width = (int) (size >> 32);
|
||||
final int height = (int) size;
|
||||
if (finalTarget.isReady() && width == finalTarget.width() && height == finalTarget.height()) return;
|
||||
|
||||
if (presenter != null) presenter.drain();
|
||||
finalTarget.ensureSize(device, resourceManager, width, height);
|
||||
}
|
||||
|
||||
public void markFrameEmpty(FrameState f) {
|
||||
f.skipPresent = true;
|
||||
f.emptyFramesThisFrame++;
|
||||
}
|
||||
|
||||
static boolean swapchainClearNeedsPassBreak(ContextState st, FrameState f) {
|
||||
return (st.pendingSwapchainClear || st.pendingSwapchainDepthClear || st.pendingSwapchainStencilClear) && f.renderPass != 0 && f.currentColorTarget == f.swapchainTexture;
|
||||
return (st.pendingSwapchainClear || st.pendingSwapchainDepthClear || st.pendingSwapchainStencilClear) && f.renderPass != 0 && f.activeLayoutHash == FBO0_LAYOUT_HASH;
|
||||
}
|
||||
|
||||
public boolean ensureSwapchainRenderPass(FrameState f, ContextState st) {
|
||||
public boolean ensureFbo0RenderPass(FrameState f, ContextState st) {
|
||||
final boolean clearColor = st.pendingSwapchainClear;
|
||||
if (swapchainClearNeedsPassBreak(st, f)) {
|
||||
endRenderPassIfActive(f);
|
||||
}
|
||||
final boolean applied = ensureSwapchainRenderPass(f,
|
||||
final boolean applied = ensureFbo0RenderPass(f,
|
||||
clearColor ? st.pendingSwapchainR : st.clearR,
|
||||
clearColor ? st.pendingSwapchainG : st.clearG,
|
||||
clearColor ? st.pendingSwapchainB : st.clearB,
|
||||
@@ -277,9 +297,9 @@ public final class FrameManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean ensureSwapchainRenderPass(FrameState f, float clearR, float clearG, float clearB, float clearA, boolean clear, boolean clearDepth, float depthValue, boolean clearStencil, int stencilValue) {
|
||||
if (!ensureSwapchainAcquired(f)) return false; // present-skip; don't bind tex=0
|
||||
if (f.renderPass != 0 && f.currentColorTarget == f.swapchainTexture) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -289,7 +309,7 @@ public final class FrameManager {
|
||||
clear = true;
|
||||
}
|
||||
|
||||
final long depthTexture = resourceManager != null ? resourceManager.getOrCreateSwapchainDepthStencil(f.swapchainWidth, f.swapchainHeight) : 0L;
|
||||
final long depthTexture = resourceManager != null ? resourceManager.getOrCreateSwapchainDepthStencil(finalTarget.width(), finalTarget.height()) : 0L;
|
||||
if (depthTexture != 0 && !f.depthClearedThisFrame) {
|
||||
clearDepth = true;
|
||||
clearStencil = true;
|
||||
@@ -298,7 +318,7 @@ public final class FrameManager {
|
||||
try (var stack = stackPush()) {
|
||||
final SDL_GPUColorTargetInfo.Buffer colorTargets = SDL_GPUColorTargetInfo.calloc(1, stack);
|
||||
final long ctAddr = colorTargets.get(0).address();
|
||||
MemoryAccess.putAddress(ctAddr + SDL_GPUColorTargetInfo.TEXTURE, f.swapchainTexture);
|
||||
MemoryAccess.putAddress(ctAddr + SDL_GPUColorTargetInfo.TEXTURE, finalTarget.colorTexture());
|
||||
MemoryAccess.putInt(ctAddr + SDL_GPUColorTargetInfo.LOAD_OP, clear ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
|
||||
MemoryAccess.putInt(ctAddr + SDL_GPUColorTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
|
||||
|
||||
@@ -333,10 +353,11 @@ public final class FrameManager {
|
||||
}
|
||||
f.renderPassGeneration++;
|
||||
f.swapchainPassesThisFrame++;
|
||||
f.currentColorTarget = f.swapchainTexture;
|
||||
f.currentColorTarget = finalTarget.colorTexture();
|
||||
f.currentDepthTarget = depthTexture;
|
||||
f.activeLayoutHash = SWAPCHAIN_LAYOUT_HASH;
|
||||
f.swapchainUsedThisFrame = true;
|
||||
f.activeLayoutHash = FBO0_LAYOUT_HASH;
|
||||
f.fbo0UsedThisFrame = true;
|
||||
finalTarget.markContent();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -388,6 +409,8 @@ public final class FrameManager {
|
||||
|
||||
public void noteMipGen() { frame().mipGensThisFrame++; }
|
||||
|
||||
public void noteMipGenSubmit() { frame().mipGenSubmitsThisFrame++; }
|
||||
|
||||
public void noteBlit() { frame().blitsThisFrame++; }
|
||||
|
||||
public void noteClearPass() { frame().clearPassesThisFrame++; }
|
||||
@@ -477,17 +500,14 @@ public final class FrameManager {
|
||||
}
|
||||
|
||||
public void requestFlushOnAllRegisteredFrames() {
|
||||
final int n = registeredFrames.size();
|
||||
for (int i = 0; i < n; i++) {
|
||||
registeredFrames.get(i).flushRequested = true;
|
||||
for (final FrameState f : registeredFrames.snapshot()) {
|
||||
f.flushRequested = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void requestSyncFlushOnAllRegisteredFrames() {
|
||||
globalSyncAtNextBeginFrame = true;
|
||||
final int n = registeredFrames.size();
|
||||
for (int i = 0; i < n; i++) {
|
||||
final FrameState f = registeredFrames.get(i);
|
||||
for (final FrameState f : registeredFrames.snapshot()) {
|
||||
f.flushRequested = true;
|
||||
f.syncOnNextFlush = true;
|
||||
}
|
||||
@@ -514,10 +534,6 @@ public final class FrameManager {
|
||||
endCopyPassIfActive(f);
|
||||
endRenderPassIfActive(f);
|
||||
|
||||
if (!f.swapchainAcquiredThisFrame && !f.swapchainUnavailable && !f.skipPresent && f.commandBuffer != 0) {
|
||||
ensureSwapchainAcquired(f);
|
||||
}
|
||||
|
||||
if (f.commandBuffer != 0) {
|
||||
Tracy.beginZone(Z_SDL_SUBMIT);
|
||||
try {
|
||||
@@ -538,13 +554,13 @@ public final class FrameManager {
|
||||
f.commandBuffer = 0;
|
||||
}
|
||||
|
||||
if (Tracy.ENABLED && resourceManager != null) SdlFramePlots.emit(f, resourceManager);
|
||||
drainWindowPresentCounters(f);
|
||||
|
||||
statLoopFrames++;
|
||||
if (f.swapchainAcquiredThisFrame) statPresentedFrames++;
|
||||
statPresentSkips += f.presentSkipsThisFrame;
|
||||
statEmptyFrames += f.emptyFramesThisFrame;
|
||||
statAcquireWaitNanos += f.acquireWaitNanosThisFrame;
|
||||
if (Tracy.ENABLED && resourceManager != null) {
|
||||
SdlFramePlots.emit(f, resourceManager);
|
||||
}
|
||||
|
||||
f.midFrameSubmitsThisFrame = 0;
|
||||
|
||||
f.lastPlottedRenderPassGen = f.renderPassGeneration;
|
||||
f.computePassesThisFrame = 0;
|
||||
@@ -563,6 +579,8 @@ public final class FrameManager {
|
||||
f.stateAppliesThisFrame = 0;
|
||||
f.acquireWaitNanosThisFrame = 0;
|
||||
f.mipGensThisFrame = 0;
|
||||
f.mipGenSubmitsThisFrame = 0;
|
||||
f.uploadFlushSubmitsThisFrame = 0;
|
||||
f.blitsThisFrame = 0;
|
||||
f.clearPassesThisFrame = 0;
|
||||
f.swapchainPassesThisFrame = 0;
|
||||
@@ -577,9 +595,6 @@ public final class FrameManager {
|
||||
f.computeBatchJoinsThisFrame = 0;
|
||||
|
||||
f.frameActive = false;
|
||||
f.swapchainTexture = 0;
|
||||
|
||||
if (afterPresent != null) afterPresent.run();
|
||||
}
|
||||
|
||||
public void submitMidFrame() {
|
||||
@@ -603,6 +618,7 @@ public final class FrameManager {
|
||||
Tracy.endZone();
|
||||
}
|
||||
f.submitsThisFrame++;
|
||||
f.midFrameSubmitsThisFrame++;
|
||||
f.commandBuffer = 0;
|
||||
if (f.frameActive) {
|
||||
reacquireCommandBuffer();
|
||||
@@ -614,6 +630,7 @@ public final class FrameManager {
|
||||
if (f.frameActive) {
|
||||
endFrame();
|
||||
}
|
||||
presentFinalTarget();
|
||||
beginFrame();
|
||||
}
|
||||
|
||||
@@ -690,6 +707,7 @@ public final class FrameManager {
|
||||
device.reportGpuFailure("submit pending upload command buffer");
|
||||
}
|
||||
f.submitsThisFrame++;
|
||||
f.uploadFlushSubmitsThisFrame++;
|
||||
f.pendingUploadCommandBuffer = 0;
|
||||
f.pendingUploadBytes = 0;
|
||||
f.pendingUploadCommands = 0;
|
||||
@@ -724,22 +742,16 @@ public final class FrameManager {
|
||||
|
||||
public long getActiveLayoutHash(FrameState f) { return f.activeLayoutHash; }
|
||||
|
||||
public long getSwapchainTexture() {
|
||||
final FrameState f = frame();
|
||||
ensureSwapchainAcquired(f);
|
||||
return f.swapchainTexture;
|
||||
public long getFbo0Texture() {
|
||||
return finalTarget.colorTexture();
|
||||
}
|
||||
|
||||
public int getSwapchainWidth() {
|
||||
final FrameState f = frame();
|
||||
ensureSwapchainAcquired(f);
|
||||
return f.swapchainWidth;
|
||||
public int getFbo0Width() {
|
||||
return finalTarget.width();
|
||||
}
|
||||
|
||||
public int getSwapchainHeight() {
|
||||
final FrameState f = frame();
|
||||
ensureSwapchainAcquired(f);
|
||||
return f.swapchainHeight;
|
||||
public int getFbo0Height() {
|
||||
return finalTarget.height();
|
||||
}
|
||||
|
||||
public int getSwapchainFormat() {
|
||||
@@ -774,8 +786,8 @@ public final class FrameManager {
|
||||
return frame().frameNumber;
|
||||
}
|
||||
|
||||
public boolean isSwapchainUsedThisFrame() {
|
||||
return frame().swapchainUsedThisFrame;
|
||||
public boolean isFbo0UsedThisFrame() {
|
||||
return frame().fbo0UsedThisFrame;
|
||||
}
|
||||
|
||||
public void reacquireCommandBuffer() {
|
||||
@@ -788,24 +800,18 @@ public final class FrameManager {
|
||||
f.currentDepthTarget = 0;
|
||||
|
||||
if (f.frameActive) {
|
||||
final boolean wasAcquired = f.swapchainAcquiredThisFrame;
|
||||
f.swapchainAcquiredThisFrame = false;
|
||||
f.swapchainTexture = 0;
|
||||
if (wasAcquired && !ensureSwapchainAcquired(f)) {
|
||||
LOG.warn("Failed to re-acquire swapchain after readback: {}", SDLError.SDL_GetError());
|
||||
}
|
||||
f.clearedThisFrame = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void releaseThreadState() {
|
||||
final FrameState f = frame();
|
||||
final FrameState f = tlFrame.get();
|
||||
if (f == null) return;
|
||||
if (resourceManager != null) resourceManager.flushUploadArena(f);
|
||||
flushPendingUploadCommandBuffer(f);
|
||||
releaseFrameStateResources(f);
|
||||
registeredFrames.remove(f);
|
||||
tlFrame.remove();
|
||||
refreshSoleFrame();
|
||||
registeredFrames.remove(f);
|
||||
}
|
||||
|
||||
private void releaseFrameStateResources(FrameState f) {
|
||||
@@ -843,23 +849,98 @@ public final class FrameManager {
|
||||
}
|
||||
|
||||
public void releaseAllRegisteredFrames() {
|
||||
for (FrameState f : registeredFrames) {
|
||||
for (final FrameState f : registeredFrames.snapshot()) {
|
||||
releaseFrameStateResources(f);
|
||||
}
|
||||
registeredFrames.clear();
|
||||
soleFrame = null;
|
||||
}
|
||||
|
||||
public boolean ensureSwapchainAcquired() {
|
||||
return ensureSwapchainAcquired(frame());
|
||||
public long lastFrameGateNanos() {
|
||||
return frame().lastFrameGateNanos;
|
||||
}
|
||||
|
||||
public boolean ensureSwapchainAcquired(FrameState f) {
|
||||
if (f.swapchainAcquiredThisFrame) return true;
|
||||
if (f.swapchainUnavailable) return false;
|
||||
if (!f.frameActive || f.commandBuffer == 0) return false;
|
||||
public long lastFrameGateEndNanos() {
|
||||
return frame().lastFrameGateEndNanos;
|
||||
}
|
||||
|
||||
endActiveEncoders(f);
|
||||
private Presenter presenter;
|
||||
|
||||
public void setPresenter(Presenter presenter) {
|
||||
this.presenter = presenter;
|
||||
}
|
||||
|
||||
private final FrameState windowFrame = new FrameState();
|
||||
|
||||
public FrameState windowFrame() {
|
||||
return windowFrame;
|
||||
}
|
||||
|
||||
public void presentFinalTarget() {
|
||||
if (!finalTarget.hasContent()) return;
|
||||
final long texture = finalTarget.colorTexture();
|
||||
final int width = finalTarget.width();
|
||||
final int height = finalTarget.height();
|
||||
final Presenter p = presenter;
|
||||
if (p != null && p.isEngaged()) {
|
||||
final FrameState f = frame();
|
||||
if (f.presentedThisFrame) return;
|
||||
f.presentedThisFrame = true;
|
||||
p.requestPresent(texture, width, height);
|
||||
} else {
|
||||
presentBlit(frame(), texture, width, height, SDL_FLIP_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
private final AtomicLong windowAcquireWaitNanos = new AtomicLong();
|
||||
private final AtomicInteger windowPresentSkips = new AtomicInteger();
|
||||
|
||||
private void drainWindowPresentCounters(FrameState f) {
|
||||
if (f == windowFrame) return;
|
||||
final long acquire = windowAcquireWaitNanos.getAndSet(0L);
|
||||
if (acquire != 0L) {
|
||||
f.acquireWaitNanosThisFrame += acquire;
|
||||
f.lastFrameGateNanos = windowFrame.lastFrameGateNanos;
|
||||
f.lastFrameGateEndNanos = windowFrame.lastFrameGateEndNanos;
|
||||
}
|
||||
f.presentSkipsThisFrame += windowPresentSkips.getAndSet(0);
|
||||
}
|
||||
|
||||
private void noteAcquireWait(FrameState f, long waitNanos) {
|
||||
if (f == windowFrame) windowAcquireWaitNanos.addAndGet(waitNanos);
|
||||
else f.acquireWaitNanosThisFrame += waitNanos;
|
||||
}
|
||||
|
||||
private void notePresentSkip(FrameState f) {
|
||||
if (f == windowFrame) windowPresentSkips.incrementAndGet();
|
||||
else f.presentSkipsThisFrame++;
|
||||
}
|
||||
|
||||
public void presentOnWindowThread(long srcTexture, int srcW, int srcH) {
|
||||
presentBlit(windowFrame, srcTexture, srcW, srcH, SDL_FLIP_NONE);
|
||||
windowFrame.presentedThisFrame = false;
|
||||
windowFrame.swapchainUnavailable = false;
|
||||
}
|
||||
|
||||
public boolean presentBlit(FrameState f, long srcTexture, int srcW, int srcH, int flipMode) {
|
||||
if (srcTexture == 0 || srcW <= 0 || srcH <= 0) return false;
|
||||
if (f.presentedThisFrame || f.swapchainUnavailable) return false;
|
||||
|
||||
if (!f.windowThreadChecked) {
|
||||
f.windowThreadChecked = true;
|
||||
final Thread self = Thread.currentThread();
|
||||
final Thread windowThread = device.getWindowThread();
|
||||
if (windowThread != null && windowThread != self) {
|
||||
LOG.warn("Swapchain acquired on '{}' but the SDL window was created on '{}'; SDL requires acquire on the window-creating thread", self.getName(), windowThread.getName());
|
||||
} else {
|
||||
LOG.info("First swapchain acquire on thread={}", self.getName());
|
||||
}
|
||||
}
|
||||
|
||||
final long window = device.getClaimedWindow();
|
||||
if (window == 0) return false;
|
||||
|
||||
final long cb = SDL_AcquireGPUCommandBuffer(device.getDevice());
|
||||
if (cb == 0) return false;
|
||||
|
||||
final long t0 = System.nanoTime();
|
||||
final long tex;
|
||||
@@ -872,7 +953,7 @@ public final class FrameManager {
|
||||
final IntBuffer pHeight = stack.ints(0);
|
||||
Tracy.beginZone(Z_SDL_ACQUIRE_WAIT);
|
||||
try {
|
||||
callOk = SDL_WaitAndAcquireGPUSwapchainTexture(f.commandBuffer, Display.getWindow(), pTexture, pWidth, pHeight);
|
||||
callOk = SDL_WaitAndAcquireGPUSwapchainTexture(cb, window, pTexture, pWidth, pHeight);
|
||||
} finally {
|
||||
Tracy.endZone();
|
||||
}
|
||||
@@ -880,25 +961,43 @@ public final class FrameManager {
|
||||
w = pWidth.get(0);
|
||||
h = pHeight.get(0);
|
||||
} catch (RuntimeException re) {
|
||||
f.acquireWaitNanosThisFrame += System.nanoTime() - t0;
|
||||
SDL_CancelGPUCommandBuffer(cb);
|
||||
f.swapchainUnavailable = true;
|
||||
throw re;
|
||||
}
|
||||
f.acquireWaitNanosThisFrame += System.nanoTime() - t0;
|
||||
final long tEnd = System.nanoTime();
|
||||
f.lastFrameGateNanos = tEnd - t0;
|
||||
f.lastFrameGateEndNanos = tEnd;
|
||||
noteAcquireWait(f, tEnd - t0);
|
||||
|
||||
if (!callOk) {
|
||||
if (!callOk || tex == 0) {
|
||||
SDL_CancelGPUCommandBuffer(cb);
|
||||
f.swapchainUnavailable = true;
|
||||
notePresentSkip(f);
|
||||
return false;
|
||||
}
|
||||
if (tex == 0) {
|
||||
f.swapchainUnavailable = true;
|
||||
f.presentSkipsThisFrame++;
|
||||
|
||||
try (MemoryStack stack = stackPush()) {
|
||||
final long info = stack.ncalloc(SDL_GPUBlitInfo.ALIGNOF, 1, SDL_GPUBlitInfo.SIZEOF);
|
||||
writeBlitRegion(info + SDL_GPUBlitInfo.SOURCE, srcTexture, srcW, srcH);
|
||||
writeBlitRegion(info + SDL_GPUBlitInfo.DESTINATION, tex, w, h);
|
||||
MemoryAccess.putInt(info + SDL_GPUBlitInfo.FILTER, SDL_GPU_FILTER_LINEAR);
|
||||
MemoryAccess.putInt(info + SDL_GPUBlitInfo.FLIP_MODE, flipMode);
|
||||
nSDL_BlitGPUTexture(cb, info);
|
||||
}
|
||||
|
||||
if (!SDL_SubmitGPUCommandBuffer(cb)) {
|
||||
device.reportGpuFailure("submit present blit");
|
||||
return false;
|
||||
}
|
||||
f.swapchainTexture = tex;
|
||||
f.swapchainWidth = w;
|
||||
f.swapchainHeight = h;
|
||||
f.swapchainAcquiredThisFrame = true;
|
||||
f.presentedThisFrame = true;
|
||||
if (afterPresent != null) afterPresent.run();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void writeBlitRegion(long region, long texture, int w, int h) {
|
||||
MemoryAccess.putAddress(region + SDL_GPUBlitRegion.TEXTURE, texture);
|
||||
MemoryAccess.putInt(region + SDL_GPUBlitRegion.W, w);
|
||||
MemoryAccess.putInt(region + SDL_GPUBlitRegion.H, h);
|
||||
}
|
||||
}
|
||||
+14
-3
@@ -1,7 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.splash;
|
||||
package com.gtnewhorizons.angelica.sdlgpu.frame;
|
||||
|
||||
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
@@ -9,13 +8,14 @@ import org.lwjgl.opengl.GL11;
|
||||
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
|
||||
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||
|
||||
public final class SplashOffscreenTarget {
|
||||
public final class OffscreenTarget {
|
||||
|
||||
private int fboGlId;
|
||||
private int colorTexGlId;
|
||||
private long colorTexHandle;
|
||||
private int width;
|
||||
private int height;
|
||||
private boolean hasContent;
|
||||
|
||||
public void create(Device device, ResourceManager rm, int width, int height, int swapchainSdlFormat) {
|
||||
this.width = width;
|
||||
@@ -37,10 +37,20 @@ public final class SplashOffscreenTarget {
|
||||
fbo.cachedFormatsDirty = true;
|
||||
}
|
||||
|
||||
public void ensureSize(Device device, ResourceManager rm, int width, int height) {
|
||||
if (width <= 0 || height <= 0) return;
|
||||
if (colorTexHandle != 0 && width == this.width && height == this.height) return;
|
||||
destroy(rm);
|
||||
create(device, rm, width, height, device.getSwapchainTextureFormat());
|
||||
}
|
||||
|
||||
public int fboId() { return fboGlId; }
|
||||
public long colorTexture() { return colorTexHandle; }
|
||||
public int width() { return width; }
|
||||
public int height() { return height; }
|
||||
public boolean isReady() { return colorTexHandle != 0; }
|
||||
public boolean hasContent() { return hasContent; }
|
||||
public void markContent() { hasContent = true; }
|
||||
|
||||
public boolean isFor(ContextState st) {
|
||||
return fboGlId != 0 && st.boundFboId == fboGlId;
|
||||
@@ -56,5 +66,6 @@ public final class SplashOffscreenTarget {
|
||||
colorTexGlId = 0;
|
||||
colorTexHandle = 0;
|
||||
}
|
||||
hasContent = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.frame;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
public final class Presenter {
|
||||
|
||||
private final FrameManager frameManager;
|
||||
private final Executor windowThreadExecutor;
|
||||
private final Semaphore pending = new Semaphore(1);
|
||||
|
||||
public Presenter(FrameManager frameManager, Executor windowThreadExecutor) {
|
||||
this.frameManager = frameManager;
|
||||
this.windowThreadExecutor = windowThreadExecutor;
|
||||
}
|
||||
|
||||
public boolean isEngaged() {
|
||||
return windowThreadExecutor != null && GLStateManager.isSplashComplete();
|
||||
}
|
||||
|
||||
public void requestPresent(long srcTexture, int srcW, int srcH) {
|
||||
pending.acquireUninterruptibly();
|
||||
boolean submitted = false;
|
||||
try {
|
||||
windowThreadExecutor.execute(() -> {
|
||||
try {
|
||||
frameManager.presentOnWindowThread(srcTexture, srcW, srcH);
|
||||
} finally {
|
||||
pending.release();
|
||||
}
|
||||
});
|
||||
submitted = true;
|
||||
} finally {
|
||||
if (!submitted) pending.release();
|
||||
}
|
||||
}
|
||||
|
||||
public void drain() {
|
||||
pending.acquireUninterruptibly();
|
||||
pending.release();
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,8 @@ final class SdlFramePlots {
|
||||
private static final long PLOT_PRESENT_TO_PRESENT_US = Tracy.plotHandle("sdl.presentToPresentUs");
|
||||
private static final long PLOT_FRAMES_IN_FLIGHT = Tracy.plotHandle("sdl.framesInFlight");
|
||||
private static final long PLOT_MIP_GENS = Tracy.plotHandle("sdl.mipGens");
|
||||
private static final long PLOT_MIP_GEN_SUBMITS = Tracy.plotHandle("sdl.mipGenSubmits");
|
||||
private static final long PLOT_UPLOAD_FLUSH_SUBMITS = Tracy.plotHandle("sdl.uploadFlushSubmits");
|
||||
private static final long PLOT_BLIT_FALLBACKS = Tracy.plotHandle("sdl.blitFallbacks");
|
||||
private static final long PLOT_PRESENT_SKIPS = Tracy.plotHandle("sdl.presentSkips");
|
||||
private static final long PLOT_EMPTY_FRAMES = Tracy.plotHandle("sdl.emptyFrames");
|
||||
@@ -54,6 +56,8 @@ final class SdlFramePlots {
|
||||
private static final long PLOT_BUFFER_POOL_HITS = Tracy.plotHandle("sdl.bufferPoolHits");
|
||||
private static final long PLOT_PIPELINE_KEY_RECOMPUTES = Tracy.plotHandle("sdl.pipelineKeyRecomputes");
|
||||
private static final long PLOT_PIPELINE_FAST_PATH_HITS = Tracy.plotHandle("sdl.pipelineFastPathHits");
|
||||
private static final long PLOT_MID_FRAME_SUBMITS = Tracy.plotHandle("sdl.midFrameSubmits");
|
||||
private static final long PLOT_GATE_US = Tracy.plotHandle("sdl.gateUs");
|
||||
|
||||
private static final int[] contextCounters = new int[SDLGPURenderBackend.CTR_COUNT];
|
||||
|
||||
@@ -99,9 +103,13 @@ final class SdlFramePlots {
|
||||
f.lastEndFrameNanos = nowNanos;
|
||||
Tracy.plotInt(PLOT_FRAMES_IN_FLIGHT, Device.framesInFlight());
|
||||
Tracy.plotInt(PLOT_MIP_GENS, f.mipGensThisFrame);
|
||||
Tracy.plotInt(PLOT_MIP_GEN_SUBMITS, f.mipGenSubmitsThisFrame);
|
||||
Tracy.plotInt(PLOT_UPLOAD_FLUSH_SUBMITS, f.uploadFlushSubmitsThisFrame);
|
||||
Tracy.plotInt(PLOT_BLIT_FALLBACKS, f.blitsThisFrame);
|
||||
Tracy.plotInt(PLOT_PRESENT_SKIPS, f.presentSkipsThisFrame);
|
||||
Tracy.plotInt(PLOT_EMPTY_FRAMES, f.emptyFramesThisFrame);
|
||||
Tracy.plotInt(PLOT_DROPPED_DRAWS, f.droppedDrawsThisFrame);
|
||||
Tracy.plotInt(PLOT_MID_FRAME_SUBMITS, f.midFrameSubmitsThisFrame);
|
||||
Tracy.plotInt(PLOT_GATE_US, f.lastFrameGateNanos / 1000);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -109,7 +109,7 @@ public final class PipelineApplier {
|
||||
}
|
||||
|
||||
if (st.boundFboId == 0) {
|
||||
if (frameManager.ensureSwapchainRenderPass(f, st)) {
|
||||
if (frameManager.ensureFbo0RenderPass(f, st)) {
|
||||
st.pipeline.setDepthTargetFormat(resourceManager.getSwapchainDepthStencilFormat());
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -64,7 +64,7 @@ public final class FboState {
|
||||
h = Hashing.fmix64(h, db);
|
||||
}
|
||||
h = Hashing.fmix64(h, depthTexture);
|
||||
if (h == 0L || h == FrameManager.SWAPCHAIN_LAYOUT_HASH) h = 0xD1CE5EEDL;
|
||||
if (h == 0L || h == FrameManager.FBO0_LAYOUT_HASH) h = 0xD1CE5EEDL;
|
||||
structuralLayoutHash = h;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -587,7 +587,7 @@ public final class ResourceManager {
|
||||
final GpuBucket b = gpuBufferPool.get(Hashing.packHiLo(usage, bucket));
|
||||
if (b == null || b.handles.isEmpty()) return 0;
|
||||
for (int i = 0, n = b.handles.size(); i < n; i++) {
|
||||
if (gpuPoolCurrentFrame - b.frames.getLong(i) >= Device.framesInFlight()) {
|
||||
if (gpuPoolCurrentFrame - b.frames.getLong(i) >= Device.MAX_FRAMES_IN_FLIGHT) {
|
||||
final long handle = b.handles.getLong(i);
|
||||
final int last = n - 1;
|
||||
if (i != last) {
|
||||
@@ -1376,7 +1376,7 @@ public final class ResourceManager {
|
||||
for (int i = 0; i < n; i += 2) {
|
||||
final long handle = batchSegmentPending.getLong(i);
|
||||
final long stamp = batchSegmentPending.getLong(i + 1);
|
||||
if (currentFrame - stamp >= Device.framesInFlight()) {
|
||||
if (currentFrame - stamp >= Device.MAX_FRAMES_IN_FLIGHT) {
|
||||
batchSegmentFree.enqueue(handle);
|
||||
} else {
|
||||
batchSegmentPending.set(w, handle);
|
||||
|
||||
@@ -101,19 +101,11 @@ public final class TextureOps {
|
||||
|
||||
public void readbackTexture(long texHandle, int x, int y, int w, int h, int level, ByteBuffer output) {
|
||||
if (texHandle == 0 || output == null) return;
|
||||
frameManager.endCopyPassIfActive();
|
||||
frameManager.endRenderPassIfActive();
|
||||
frameManager.submitMidFrame();
|
||||
|
||||
if (texHandle == frameManager.getSwapchainTexture()) {
|
||||
final long cb = frameManager.getCommandBuffer();
|
||||
if (cb == 0) return;
|
||||
resourceManager.downloadFromTexture(cb, texHandle, x, y, w, h, level, output);
|
||||
frameManager.reacquireCommandBuffer();
|
||||
} else {
|
||||
final long cb = SDL_AcquireGPUCommandBuffer(device.getDevice());
|
||||
if (cb == 0) return;
|
||||
resourceManager.downloadFromTexture(cb, texHandle, x, y, w, h, level, output);
|
||||
}
|
||||
final long cb = SDL_AcquireGPUCommandBuffer(device.getDevice());
|
||||
if (cb == 0) return;
|
||||
resourceManager.downloadFromTexture(cb, texHandle, x, y, w, h, level, output);
|
||||
}
|
||||
|
||||
public void copyTexSubImageImpl(ContextState st, int destGlId, int level, int xoffset, int yoffset, int x, int y, int width, int height) {
|
||||
@@ -210,6 +202,7 @@ public final class TextureOps {
|
||||
if (!SDL_SubmitGPUCommandBuffer(uploadCb)) {
|
||||
device.reportGpuFailure("submit mipmap generation CB");
|
||||
}
|
||||
frameManager.noteMipGenSubmit();
|
||||
|
||||
if (f.commandBuffer == uploadCb) {
|
||||
f.commandBuffer = 0;
|
||||
|
||||
+15
-11
@@ -31,11 +31,6 @@ public final class PerFrameBlockInjector {
|
||||
private PerFrameBlockInjector() {}
|
||||
|
||||
public static String inject(String source, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
|
||||
final PerFrameUniformBlock[] blocks = new PerFrameUniformBlock[ShaderManager.BLOCK_COUNT];
|
||||
blocks[ShaderManager.BLOCK_PER_FRAME] = normalize(perFrame);
|
||||
blocks[ShaderManager.BLOCK_PER_PASS] = normalize(perPass);
|
||||
if (blocks[0] == null && blocks[1] == null) return source;
|
||||
|
||||
final GLSLParser.Translation_unitContext root;
|
||||
try {
|
||||
root = GlslTransformUtils.parseFullQuiet(source);
|
||||
@@ -43,8 +38,19 @@ public final class PerFrameBlockInjector {
|
||||
return source;
|
||||
}
|
||||
|
||||
final List<Edit> edits = new ArrayList<>();
|
||||
collectEdits(root, perFrame, perPass, edits);
|
||||
return edits.isEmpty() ? source : applyEdits(source, edits);
|
||||
}
|
||||
|
||||
public static void collectEdits(GLSLParser.Translation_unitContext root, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass, List<Edit> out) {
|
||||
final PerFrameUniformBlock[] blocks = new PerFrameUniformBlock[ShaderManager.BLOCK_COUNT];
|
||||
blocks[ShaderManager.BLOCK_PER_FRAME] = normalize(perFrame);
|
||||
blocks[ShaderManager.BLOCK_PER_PASS] = normalize(perPass);
|
||||
if (blocks[0] == null && blocks[1] == null) return;
|
||||
|
||||
final int insertAt = firstDeclarationStart(root);
|
||||
if (insertAt < 0) return source;
|
||||
if (insertAt < 0) return;
|
||||
|
||||
final Map<String, String> memberTypes = new LinkedHashMap<>();
|
||||
final Object2IntOpenHashMap<String> memberBlock = new Object2IntOpenHashMap<>();
|
||||
@@ -71,18 +77,16 @@ public final class PerFrameBlockInjector {
|
||||
if (referenced.contains(m.name())) { injected[b] = true; anyInjected = true; break; }
|
||||
}
|
||||
}
|
||||
if (!anyInjected) return source;
|
||||
if (!anyInjected) return;
|
||||
|
||||
final List<Edit> edits = new ArrayList<>(stripEdits.size() + 2);
|
||||
final StringBuilder blockDecls = new StringBuilder(128);
|
||||
for (int b = 0; b < blocks.length; b++) {
|
||||
if (injected[b]) appendBlockText(blockDecls, b, blocks[b].members(), shadowed);
|
||||
}
|
||||
edits.add(new Edit(insertAt, insertAt - 1, blockDecls.toString()));
|
||||
out.add(new Edit(insertAt, insertAt - 1, blockDecls.toString()));
|
||||
for (int i = 0; i < stripEdits.size(); i++) {
|
||||
if (injected[stripBlocks.getInt(i)]) edits.add(stripEdits.get(i));
|
||||
if (injected[stripBlocks.getInt(i)]) out.add(stripEdits.get(i));
|
||||
}
|
||||
return applyEdits(source, edits);
|
||||
}
|
||||
|
||||
private static PerFrameUniformBlock normalize(PerFrameUniformBlock block) {
|
||||
|
||||
@@ -188,19 +188,46 @@ public final class ShaderManager {
|
||||
return new PrewarmTransformResult(transformedSource, Set.of());
|
||||
}
|
||||
final List<Edit> edits = new ArrayList<>();
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
PerFrameBlockInjector.collectEdits(root, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock, edits);
|
||||
}
|
||||
final GlslVulkanPreprocess.Metadata meta = GlslVulkanPreprocess.collectEdits(transformedSource, root, glShaderType, "prewarm", true, edits);
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) {
|
||||
ClipZRemap.collectEdits(root, edits);
|
||||
}
|
||||
SamplerStripper.collectEdits(root, transformedSource, edits);
|
||||
String s = edits.isEmpty() ? transformedSource : GlslVulkanPreprocess.applyEdits(transformedSource, edits);
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
s = PerFrameBlockInjector.inject(s, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
}
|
||||
final String s = edits.isEmpty() ? transformedSource : GlslVulkanPreprocess.applyEdits(transformedSource, edits);
|
||||
return new PrewarmTransformResult(s, meta.boolUniforms());
|
||||
}
|
||||
|
||||
public static PrewarmTransformResult applyPrewarmTransformsFull(String finalSource, GLSLParser.Translation_unitContext bodyTree, int headerLen, int glShaderType) {
|
||||
final String header = finalSource.substring(0, headerLen);
|
||||
final String body = finalSource.substring(headerLen);
|
||||
final List<Edit> edits = new ArrayList<>();
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
PerFrameBlockInjector.collectEdits(bodyTree, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock, edits);
|
||||
}
|
||||
final GlslVulkanPreprocess.Metadata meta = GlslVulkanPreprocess.collectEdits(null, bodyTree, glShaderType, "prewarm", true, edits);
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) {
|
||||
ClipZRemap.collectEdits(bodyTree, edits);
|
||||
}
|
||||
SamplerStripper.collectEdits(bodyTree, body, edits);
|
||||
String outHeader = header;
|
||||
if (meta.needsSamplerless()) {
|
||||
final int nl = header.indexOf('\n');
|
||||
outHeader = header.substring(0, nl) + "\n" + GlslVulkanPreprocess.SAMPLERLESS_EXTENSION + header.substring(nl);
|
||||
}
|
||||
if (edits.isEmpty() && outHeader == header) {
|
||||
return new PrewarmTransformResult(finalSource, meta.boolUniforms());
|
||||
}
|
||||
return new PrewarmTransformResult(outHeader + GlslVulkanPreprocess.applyEdits(body, edits), meta.boolUniforms());
|
||||
}
|
||||
|
||||
public static void prewarmSpirv(String transformedSource, int glShaderType) {
|
||||
prewarmSpirv(transformedSource, null, 0, glShaderType);
|
||||
}
|
||||
|
||||
public static void prewarmSpirv(String transformedSource, GLSLParser.Translation_unitContext bodyTree, int headerLen, int glShaderType) {
|
||||
if (glShaderType != GL20.GL_VERTEX_SHADER && glShaderType != GL20.GL_FRAGMENT_SHADER
|
||||
&& glShaderType != GL43.GL_COMPUTE_SHADER && glShaderType != GL32.GL_GEOMETRY_SHADER) {
|
||||
return;
|
||||
@@ -211,7 +238,9 @@ public final class ShaderManager {
|
||||
if (PREWARM_CACHE.containsKey(key)) return;
|
||||
}
|
||||
|
||||
final PrewarmTransformResult pre = applyPrewarmTransformsFull(transformedSource, glShaderType);
|
||||
final PrewarmTransformResult pre = bodyTree != null
|
||||
? applyPrewarmTransformsFull(transformedSource, bodyTree, headerLen, glShaderType)
|
||||
: applyPrewarmTransformsFull(transformedSource, glShaderType);
|
||||
|
||||
final SpirvCompiler.Result r = SpirvCompiler.compile(pre.source(), shaderKindFor(glShaderType), "prewarm", SpirvCompiler.Options.vulkanForced460Core());
|
||||
if (r.spirv() == null) return;
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
import org.antlr.v4.runtime.tree.ParseTreeWalker;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
@@ -18,11 +19,29 @@ public final class UscaledRetype {
|
||||
|
||||
public record Attrib(String name, int location, int declVecSize, int boundVecSize, boolean signed) {}
|
||||
|
||||
private record RetypeKey(String source, List<Attrib> attribs) {}
|
||||
|
||||
private static final int CACHE_MAX = 64;
|
||||
private static final Object2ObjectLinkedOpenHashMap<RetypeKey, String> CACHE = new Object2ObjectLinkedOpenHashMap<>();
|
||||
|
||||
private UscaledRetype() {}
|
||||
|
||||
public static String retype(String source, List<Attrib> attribs) {
|
||||
if (attribs.isEmpty()) return source;
|
||||
|
||||
final RetypeKey key = new RetypeKey(source, List.copyOf(attribs));
|
||||
synchronized (CACHE) {
|
||||
if (CACHE.containsKey(key)) return CACHE.getAndMoveToFirst(key);
|
||||
}
|
||||
final String result = retypeUncached(source, attribs);
|
||||
synchronized (CACHE) {
|
||||
CACHE.putAndMoveToFirst(key, result);
|
||||
while (CACHE.size() > CACHE_MAX) CACHE.removeLast();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String retypeUncached(String source, List<Attrib> attribs) {
|
||||
final GLSLParser.Translation_unitContext root;
|
||||
try {
|
||||
root = GlslTransformUtils.parseFullQuiet(source);
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.splash;
|
||||
|
||||
import com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.OffscreenTarget;
|
||||
|
||||
public final class SplashDispatcher {
|
||||
|
||||
@@ -32,7 +33,7 @@ public final class SplashDispatcher {
|
||||
return (long) w << 32 | (h & 0xffffffffL);
|
||||
}
|
||||
|
||||
public static void tryDispatch(SDLGPURenderBackend backend, SplashOffscreenTarget target) {
|
||||
public static void tryDispatch(SDLGPURenderBackend backend, OffscreenTarget target) {
|
||||
if (!frameReady || target == null) return;
|
||||
final long now = System.nanoTime();
|
||||
if (now - lastBlitNanos < MIN_INTERVAL_NS) return;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class ThreadRegistry<T> {
|
||||
|
||||
private final T[] empty;
|
||||
private final Object lock = new Object();
|
||||
|
||||
private volatile T[] entries;
|
||||
private volatile T sole;
|
||||
|
||||
public ThreadRegistry(T[] empty) {
|
||||
this.empty = empty;
|
||||
this.entries = empty;
|
||||
}
|
||||
|
||||
public T sole() {
|
||||
return sole;
|
||||
}
|
||||
|
||||
public T[] snapshot() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return entries.length;
|
||||
}
|
||||
|
||||
public void add(T entry) {
|
||||
synchronized (lock) {
|
||||
final T[] prev = entries;
|
||||
final T[] next = Arrays.copyOf(prev, prev.length + 1);
|
||||
next[prev.length] = entry;
|
||||
publish(next);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean remove(T entry) {
|
||||
synchronized (lock) {
|
||||
final T[] prev = entries;
|
||||
int idx = -1;
|
||||
for (int i = 0; i < prev.length; i++) {
|
||||
if (prev[i] == entry) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx < 0) return false;
|
||||
final T[] next = Arrays.copyOf(prev, prev.length - 1);
|
||||
System.arraycopy(prev, idx + 1, next, idx, prev.length - idx - 1);
|
||||
publish(next);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
synchronized (lock) {
|
||||
publish(empty);
|
||||
}
|
||||
}
|
||||
|
||||
private void publish(T[] next) {
|
||||
entries = next;
|
||||
sole = next.length == 1 ? next[0] : null;
|
||||
}
|
||||
}
|
||||
+5
-12
@@ -7,7 +7,6 @@ import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
import org.lwjgl.opengl.GL42;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@@ -15,17 +14,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ImageUnitBindingStateTest {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static ContextState state() throws Exception {
|
||||
final Field f = SDLGPURenderBackend.class.getDeclaredField("tlState");
|
||||
f.setAccessible(true);
|
||||
return ((ThreadLocal<ContextState>) f.get(null)).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindImageTextureRecordsTheFullTuple() throws Exception {
|
||||
void bindImageTextureRecordsTheFullTuple() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
final ContextState st = state();
|
||||
final ContextState st = SdlTestRig.contextState();
|
||||
|
||||
backend.bindImageTexture(2, 77, 3, true, 5, GL15.GL_READ_WRITE, GL30.GL_R32UI);
|
||||
|
||||
@@ -38,7 +31,7 @@ class ImageUnitBindingStateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void getIntegerIndexedAnswersImageBindingsFromState() throws Exception {
|
||||
void getIntegerIndexedAnswersImageBindingsFromState() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
|
||||
backend.bindImageTexture(1, 42, 2, false, 4, GL15.GL_READ_ONLY, GL30.GL_RGBA32UI);
|
||||
@@ -52,9 +45,9 @@ class ImageUnitBindingStateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void outOfRangeUnitIsIgnored() throws Exception {
|
||||
void outOfRangeUnitIsIgnored() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
final ContextState st = state();
|
||||
final ContextState st = SdlTestRig.contextState();
|
||||
|
||||
backend.bindImageTexture(0, 11, 0, true, 0, GL15.GL_READ_ONLY, GL30.GL_R32UI);
|
||||
backend.bindImageTexture(ContextState.MAX_IMAGE_UNITS, 99, 0, true, 0, GL15.GL_READ_ONLY, GL30.GL_R32UI);
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu;
|
||||
|
||||
import com.mitchej123.lwjgl.LWJGLService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
class SDLGPULWJGLServiceGatingTest {
|
||||
|
||||
@Test
|
||||
void anUnengagedSdlDeclinesAndNeverArmsTheFallbackTripwire() {
|
||||
assertFalse(SDLGPUGate.isEngaged(), "test JVM must not have an engaged SDL GPU device");
|
||||
assertEquals(LWJGLService.PRIORITY_UNAVAILABLE, new SDLGPULWJGLService().getPriority());
|
||||
assertFalse(SDLGPULWJGLService.isOfferedAsAvailable(), "declining must leave fallBackToGL() usable");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
|
||||
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.resource.ResourceManager;
|
||||
import org.lwjgl.sdl.SDLError;
|
||||
@@ -57,6 +59,10 @@ public final class SdlTestRig {
|
||||
return create().frameManager;
|
||||
}
|
||||
|
||||
public static ContextState contextState() {
|
||||
return Reflect.invokeStatic(SDLGPURenderBackend.class, "s", new Class<?>[0]);
|
||||
}
|
||||
|
||||
private static long realSdlDevice;
|
||||
|
||||
private static final int SHADER_FORMATS = SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXBC | SDL_GPU_SHADERFORMAT_DXIL;
|
||||
|
||||
+1
-8
@@ -48,13 +48,6 @@ class UniformNegativeLocationTest {
|
||||
st.releaseUniformStaging(prog);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static ContextState threadState() throws Exception {
|
||||
final Field f = SDLGPURenderBackend.class.getDeclaredField("tlState");
|
||||
f.setAccessible(true);
|
||||
return ((ThreadLocal<ContextState>) f.get(null)).get();
|
||||
}
|
||||
|
||||
private static void clearApplier(SDLGPURenderBackend backend) throws Exception {
|
||||
final Field f = SDLGPURenderBackend.class.getDeclaredField("pipelineApplier");
|
||||
f.setAccessible(true);
|
||||
@@ -66,7 +59,7 @@ class UniformNegativeLocationTest {
|
||||
void scalarAndVectorEntryPointsNeverReachTheApplier() throws Exception {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
clearApplier(backend);
|
||||
threadState().boundProgramObj = null;
|
||||
SdlTestRig.contextState().boundProgramObj = null;
|
||||
|
||||
final IntBuffer iv = MemoryUtil.memAllocInt(4);
|
||||
try {
|
||||
|
||||
+2
-2
@@ -6,13 +6,13 @@ import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.util.ThreadRegistry;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class ContextCounterAggregationTest {
|
||||
|
||||
private List<ContextState> registered;
|
||||
private ThreadRegistry<ContextState> registered;
|
||||
private ContextState a;
|
||||
private ContextState b;
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.frame;
|
||||
|
||||
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@Timeout(10)
|
||||
class PresenterTest {
|
||||
|
||||
private static final class DeferredExecutor implements Executor {
|
||||
final Deque<Runnable> tasks = new ArrayDeque<>();
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
tasks.add(command);
|
||||
}
|
||||
|
||||
void runNext() {
|
||||
tasks.removeFirst().run();
|
||||
}
|
||||
}
|
||||
|
||||
private static Presenter presenter(DeferredExecutor executor) {
|
||||
return new Presenter(new FrameManager(new Device()), executor);
|
||||
}
|
||||
|
||||
private static void awaitWaiting(Thread t) throws InterruptedException {
|
||||
while (t.getState() != Thread.State.WAITING && t.getState() != Thread.State.TIMED_WAITING) {
|
||||
assertTrue(t.isAlive(), "thread died instead of blocking");
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void aSecondRequestBlocksUntilTheFirstPresentRuns() throws Exception {
|
||||
final DeferredExecutor executor = new DeferredExecutor();
|
||||
final Presenter p = presenter(executor);
|
||||
|
||||
p.requestPresent(1L, 10, 10);
|
||||
assertEquals(1, executor.tasks.size());
|
||||
|
||||
final Thread second = new Thread(() -> p.requestPresent(2L, 10, 10));
|
||||
second.start();
|
||||
awaitWaiting(second);
|
||||
assertEquals(1, executor.tasks.size(), "second present must not enqueue while the first is in flight");
|
||||
|
||||
executor.runNext();
|
||||
second.join();
|
||||
assertEquals(1, executor.tasks.size(), "second present enqueues only after the first released the slot");
|
||||
}
|
||||
|
||||
@Test
|
||||
void drainBlocksUntilThePendingPresentCompletes() throws Exception {
|
||||
final DeferredExecutor executor = new DeferredExecutor();
|
||||
final Presenter p = presenter(executor);
|
||||
|
||||
p.requestPresent(1L, 10, 10);
|
||||
|
||||
final Thread drainer = new Thread(p::drain);
|
||||
drainer.start();
|
||||
awaitWaiting(drainer);
|
||||
|
||||
executor.runNext();
|
||||
drainer.join();
|
||||
}
|
||||
|
||||
@Test
|
||||
void anInlineExecutorReleasesTheSlotImmediately() {
|
||||
final Presenter p = new Presenter(new FrameManager(new Device()), Runnable::run);
|
||||
p.requestPresent(1L, 10, 10);
|
||||
p.requestPresent(2L, 10, 10);
|
||||
p.drain();
|
||||
}
|
||||
|
||||
@Test
|
||||
void presentingDoesNotEnterTheFrameRegistry() throws Exception {
|
||||
final DeferredExecutor executor = new DeferredExecutor();
|
||||
final FrameManager fm = new FrameManager(new Device());
|
||||
final Presenter p = new Presenter(fm, executor);
|
||||
fm.frame();
|
||||
final int before = fm.registeredFrameCount();
|
||||
|
||||
p.requestPresent(1L, 10, 10);
|
||||
|
||||
final Thread windowThread = new Thread(executor::runNext, "window-thread");
|
||||
windowThread.start();
|
||||
windowThread.join();
|
||||
|
||||
assertEquals(before, fm.registeredFrameCount(), "the window thread must stay out of the frame registry");
|
||||
}
|
||||
}
|
||||
+10
-13
@@ -8,13 +8,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class SwapchainClearPassBreakTest {
|
||||
|
||||
private static final long SWAPCHAIN = 0x5A1CL;
|
||||
|
||||
private static FrameManager.FrameState activeSwapchainPass() {
|
||||
private static FrameManager.FrameState activeFbo0Pass() {
|
||||
final FrameManager.FrameState f = new FrameManager.FrameState();
|
||||
f.swapchainTexture = SWAPCHAIN;
|
||||
f.renderPass = 0xBEEFL;
|
||||
f.currentColorTarget = SWAPCHAIN;
|
||||
f.activeLayoutHash = FrameManager.FBO0_LAYOUT_HASH;
|
||||
return f;
|
||||
}
|
||||
|
||||
@@ -22,23 +19,23 @@ class SwapchainClearPassBreakTest {
|
||||
void aPendingColorClearBreaksTheActivePass() {
|
||||
final ContextState st = new ContextState();
|
||||
st.pendingSwapchainClear = true;
|
||||
assertTrue(swapchainClearNeedsPassBreak(st, activeSwapchainPass()), "otherwise the clear is deferred to the next pass and wipes what this one drew");
|
||||
assertTrue(swapchainClearNeedsPassBreak(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, activeSwapchainPass()));
|
||||
assertTrue(swapchainClearNeedsPassBreak(depth, activeFbo0Pass()));
|
||||
|
||||
final ContextState stencil = new ContextState();
|
||||
stencil.pendingSwapchainStencilClear = true;
|
||||
assertTrue(swapchainClearNeedsPassBreak(stencil, activeSwapchainPass()));
|
||||
assertTrue(swapchainClearNeedsPassBreak(stencil, activeFbo0Pass()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nothingPendingLeavesThePassAlone() {
|
||||
assertFalse(swapchainClearNeedsPassBreak(new ContextState(), activeSwapchainPass()));
|
||||
assertFalse(swapchainClearNeedsPassBreak(new ContextState(), activeFbo0Pass()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -46,18 +43,18 @@ class SwapchainClearPassBreakTest {
|
||||
final ContextState st = new ContextState();
|
||||
st.pendingSwapchainClear = true;
|
||||
|
||||
final FrameManager.FrameState f = activeSwapchainPass();
|
||||
final FrameManager.FrameState f = activeFbo0Pass();
|
||||
f.renderPass = 0;
|
||||
assertFalse(swapchainClearNeedsPassBreak(st, f));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aPassBoundToAnFboIsNotTheSwapchainPass() {
|
||||
void aPassBoundToAnFboIsNotTheFbo0Pass() {
|
||||
final ContextState st = new ContextState();
|
||||
st.pendingSwapchainClear = true;
|
||||
|
||||
final FrameManager.FrameState f = activeSwapchainPass();
|
||||
f.currentColorTarget = 0xF80L;
|
||||
final FrameManager.FrameState f = activeFbo0Pass();
|
||||
f.activeLayoutHash = 0xF80L;
|
||||
assertFalse(swapchainClearNeedsPassBreak(st, f));
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.resource;
|
||||
|
||||
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
|
||||
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||
import static org.lwjgl.system.MemoryUtil.memAlloc;
|
||||
import static org.lwjgl.system.MemoryUtil.memFree;
|
||||
|
||||
@Timeout(60)
|
||||
class ReadbackSubmitOrderGpuTest {
|
||||
|
||||
private static final int SIZE = 4;
|
||||
|
||||
@AfterAll
|
||||
static void releaseDevice() {
|
||||
SdlTestRig.releaseRealDevice();
|
||||
}
|
||||
|
||||
@Test
|
||||
void aReadbackSubmitsTheFramesWritesFirst() throws Exception {
|
||||
final SdlTestRig rig = SdlTestRig.acquireRealDevice();
|
||||
final ResourceManager rm = rig.resourceManager;
|
||||
final FrameManager fm = rig.frameManager;
|
||||
final TextureOps textureOps = new TextureOps(rig.device, fm, rm, null);
|
||||
|
||||
final int glId = rm.genTexture();
|
||||
final long texture = rm.createTexture(glId, GL11.GL_TEXTURE_2D, GL11.GL_RGBA8, SIZE, SIZE, 1, 1, SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COLOR_TARGET);
|
||||
assertNotEquals(0L, texture, "texture creation failed");
|
||||
|
||||
fm.beginFrame();
|
||||
final FrameManager.FrameState f = fm.frame();
|
||||
final long frameCb = f.commandBuffer;
|
||||
assertNotEquals(0L, frameCb, "no frame command buffer to order against");
|
||||
assertEquals(0, f.midFrameSubmitsThisFrame);
|
||||
|
||||
final ByteBuffer out = memAlloc(SIZE * SIZE * 4);
|
||||
try {
|
||||
textureOps.readbackTexture(texture, 0, 0, SIZE, SIZE, 0, out);
|
||||
} finally {
|
||||
memFree(out);
|
||||
}
|
||||
|
||||
assertEquals(1, f.midFrameSubmitsThisFrame, "the frame command buffer must be submitted before the download buffer, or the readback sees stale contents");
|
||||
assertNotEquals(frameCb, f.commandBuffer, "the frame must continue on a freshly acquired command buffer");
|
||||
|
||||
fm.endFrame();
|
||||
}
|
||||
}
|
||||
+9
-15
@@ -1,29 +1,23 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.sampler;
|
||||
|
||||
import com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
class SamplerBindGenerationTest {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static ContextState state() throws Exception {
|
||||
final Field f = SDLGPURenderBackend.class.getDeclaredField("tlState");
|
||||
f.setAccessible(true);
|
||||
return ((ThreadLocal<ContextState>) f.get(null)).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void switchingActiveUnitDoesNotBumpTheGeneration() throws Exception {
|
||||
void switchingActiveUnitDoesNotBumpTheGeneration() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
final ContextState st = state();
|
||||
final ContextState st = SdlTestRig.contextState();
|
||||
|
||||
backend.activeTexture(GL13.GL_TEXTURE0);
|
||||
final int before = st.samplerBindGen;
|
||||
@@ -37,9 +31,9 @@ class SamplerBindGenerationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void aRealBindChangeBumpsTheGeneration() throws Exception {
|
||||
void aRealBindChangeBumpsTheGeneration() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
final ContextState st = state();
|
||||
final ContextState st = SdlTestRig.contextState();
|
||||
|
||||
backend.activeTexture(GL13.GL_TEXTURE0);
|
||||
backend.bindTexture(GL11.GL_TEXTURE_2D, 0);
|
||||
@@ -50,9 +44,9 @@ class SamplerBindGenerationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void rebindingTheSameTextureDoesNotBumpTheGeneration() throws Exception {
|
||||
void rebindingTheSameTextureDoesNotBumpTheGeneration() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
final ContextState st = state();
|
||||
final ContextState st = SdlTestRig.contextState();
|
||||
|
||||
backend.activeTexture(GL13.GL_TEXTURE0);
|
||||
backend.bindTexture(GL11.GL_TEXTURE_2D, 99);
|
||||
@@ -63,9 +57,9 @@ class SamplerBindGenerationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindingsAreHeldPerUnitSoTheActiveUnitIsNotSamplerState() throws Exception {
|
||||
void bindingsAreHeldPerUnitSoTheActiveUnitIsNotSamplerState() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
final ContextState st = state();
|
||||
final ContextState st = SdlTestRig.contextState();
|
||||
|
||||
backend.activeTexture(GL13.GL_TEXTURE2);
|
||||
backend.bindTexture(GL11.GL_TEXTURE_2D, 55);
|
||||
|
||||
+107
@@ -1,15 +1,28 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock.Member;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.UniformType;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL32;
|
||||
import org.lwjgl.opengl.GL43;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class PrewarmSingleParseEquivalenceTest {
|
||||
|
||||
@AfterEach
|
||||
void clearBlocks() {
|
||||
GLSMHooks.perFrameUniformBlock = null;
|
||||
GLSMHooks.perPassUniformBlock = null;
|
||||
}
|
||||
|
||||
private static String legacy(String source, int glShaderType) {
|
||||
final GlslVulkanPreprocess.Result pre = GlslVulkanPreprocess.run(source, glShaderType, "test", true);
|
||||
String src = pre != null ? pre.rewrittenSource() : source;
|
||||
@@ -17,6 +30,9 @@ class PrewarmSingleParseEquivalenceTest {
|
||||
src = ClipZRemap.injectGLToVulkanClipZ(src);
|
||||
}
|
||||
src = SamplerStripper.stripUnused(src);
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
src = PerFrameBlockInjector.inject(src, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
@@ -28,6 +44,14 @@ class PrewarmSingleParseEquivalenceTest {
|
||||
assertEquals(legacyOut, parseOnceOut, "single-parse vs chained pipeline diverged for " + label);
|
||||
}
|
||||
|
||||
private static void assertEquivalentIgnoringWhitespace(String label, String source, int glShaderType) {
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String legacyOut = legacy(source, glShaderType);
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String parseOnceOut = ShaderManager.applyPrewarmTransforms(source, glShaderType);
|
||||
assertEquals(legacyOut.replaceAll("\\s+", " "), parseOnceOut.replaceAll("\\s+", " "), "single-parse vs chained pipeline diverged beyond whitespace for " + label);
|
||||
}
|
||||
|
||||
@Test
|
||||
void minimalFragment_noTransformsNeeded() {
|
||||
final String src = "#version 460 core\n"
|
||||
@@ -135,4 +159,87 @@ class PrewarmSingleParseEquivalenceTest {
|
||||
final String src = "this is not valid GLSL @@@@\n";
|
||||
assertEquivalent("unparseable", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
private static final PerFrameUniformBlock PER_FRAME = new PerFrameUniformBlock(List.of(
|
||||
new Member("cameraPosition", UniformType.VEC3),
|
||||
new Member("frameTimeCounter", UniformType.FLOAT)));
|
||||
|
||||
private static final PerFrameUniformBlock PER_PASS = new PerFrameUniformBlock(List.of(
|
||||
new Member("alphaTestRef", UniformType.FLOAT)));
|
||||
|
||||
@Test
|
||||
void perFrameBlock_replaceableUniformStrippedAndInjected() {
|
||||
GLSMHooks.perFrameUniformBlock = PER_FRAME;
|
||||
GLSMHooks.perPassUniformBlock = PER_PASS;
|
||||
final String src = "#version 460 core\n"
|
||||
+ "uniform float frameTimeCounter;\n"
|
||||
+ "uniform float alphaTestRef;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() {\n"
|
||||
+ " if (fragColor.a < alphaTestRef) discard;\n"
|
||||
+ " fragColor = vec4(frameTimeCounter);\n"
|
||||
+ "}\n";
|
||||
assertEquivalent("pfb-replaceable", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void perFrameBlock_firstDeclIsUnlocatedVsInput() {
|
||||
GLSMHooks.perFrameUniformBlock = PER_FRAME;
|
||||
final String src = "#version 460 core\n"
|
||||
+ "in vec3 a_Position;\n"
|
||||
+ "layout(location = 5) in vec2 a_TexCoord;\n"
|
||||
+ "uniform vec3 cameraPosition;\n"
|
||||
+ "void main() {\n"
|
||||
+ " gl_Position = vec4(a_Position - cameraPosition, 1.0);\n"
|
||||
+ "}\n";
|
||||
assertEquivalent("pfb-unlocated-first-in", src, GL20.GL_VERTEX_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void perFrameBlock_memberUniformWithInitializer() {
|
||||
GLSMHooks.perPassUniformBlock = PER_PASS;
|
||||
final String src = "#version 460 core\n"
|
||||
+ "uniform float alphaTestRef = 0.1;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() {\n"
|
||||
+ " if (fragColor.a < alphaTestRef) discard;\n"
|
||||
+ "}\n";
|
||||
assertEquivalent("pfb-initializer", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void perFrameBlock_firstDeclIsStrippedSampler() {
|
||||
GLSMHooks.perFrameUniformBlock = PER_FRAME;
|
||||
final String src = "#version 460 core\n"
|
||||
+ "uniform sampler2D u_Unused;\n"
|
||||
+ "uniform float frameTimeCounter;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() {\n"
|
||||
+ " fragColor = vec4(frameTimeCounter);\n"
|
||||
+ "}\n";
|
||||
assertEquivalentIgnoringWhitespace("pfb-stripped-first-sampler", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void perFrameBlock_shadowedMemberKeepsLocalDeclaration() {
|
||||
GLSMHooks.perFrameUniformBlock = PER_FRAME;
|
||||
final String src = "#version 460 core\n"
|
||||
+ "uniform vec4 frameTimeCounter;\n"
|
||||
+ "uniform vec3 cameraPosition;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() {\n"
|
||||
+ " fragColor = frameTimeCounter + vec4(cameraPosition, 0.0);\n"
|
||||
+ "}\n";
|
||||
assertEquivalent("pfb-shadowed", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void perFrameBlock_noReferencesLeavesSourceUntouched() {
|
||||
GLSMHooks.perFrameUniformBlock = PER_FRAME;
|
||||
GLSMHooks.perPassUniformBlock = PER_PASS;
|
||||
final String src = "#version 460 core\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = vec4(1.0); }\n";
|
||||
assertEquivalent("pfb-unreferenced", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock.Member;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.UniformType;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL32;
|
||||
import org.taumc.glsl.ShaderParser;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class PrewarmTreeHandoffEquivalenceTest {
|
||||
|
||||
private static final String HEADER = "#version 460 core\n";
|
||||
|
||||
@AfterEach
|
||||
void clearBlocks() {
|
||||
GLSMHooks.perFrameUniformBlock = null;
|
||||
GLSMHooks.perPassUniformBlock = null;
|
||||
}
|
||||
|
||||
private static void assertHandoffEquivalent(String label, String body, int glShaderType) {
|
||||
final GLSLParser.Translation_unitContext tree = ShaderParser.parseShader(body).full();
|
||||
GlslTransformUtils.restoreReservedWordsInTree(tree);
|
||||
final String printedBody = GlslTransformUtils.getFormattedShaderRebased(tree, "");
|
||||
final String finalSource = HEADER + printedBody;
|
||||
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final ShaderManager.PrewarmTransformResult reparse = ShaderManager.applyPrewarmTransformsFull(finalSource, glShaderType);
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final ShaderManager.PrewarmTransformResult treeFed = ShaderManager.applyPrewarmTransformsFull(finalSource, tree, HEADER.length(), glShaderType);
|
||||
|
||||
assertEquals(reparse.source(), treeFed.source(), "tree handoff diverged from re-parse for " + label);
|
||||
assertEquals(reparse.boolUniforms(), treeFed.boolUniforms(), "bool uniforms diverged for " + label);
|
||||
}
|
||||
|
||||
@Test
|
||||
void minimalFragment() {
|
||||
assertHandoffEquivalent("minimal-fragment",
|
||||
"layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = vec4(1.0); }\n",
|
||||
GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unusedSamplerStripped() {
|
||||
assertHandoffEquivalent("unused-sampler",
|
||||
"uniform sampler2D u_Unused;\n"
|
||||
+ "uniform sampler2D u_Used;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = texture(u_Used, vec2(0.5)); }\n",
|
||||
GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void perFrameBlockInjection() {
|
||||
GLSMHooks.perFrameUniformBlock = new PerFrameUniformBlock(List.of(
|
||||
new Member("cameraPosition", UniformType.VEC3),
|
||||
new Member("frameTimeCounter", UniformType.FLOAT)));
|
||||
assertHandoffEquivalent("per-frame-block",
|
||||
"uniform float frameTimeCounter;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = vec4(frameTimeCounter); }\n",
|
||||
GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void vertexClipZAndUnlocatedInputs() {
|
||||
GLSMHooks.perFrameUniformBlock = new PerFrameUniformBlock(List.of(
|
||||
new Member("cameraPosition", UniformType.VEC3)));
|
||||
assertHandoffEquivalent("vertex-clipz-unlocated",
|
||||
"layout(location = 0) in vec3 a_Position;\n"
|
||||
+ "in vec2 a_TexCoord;\n"
|
||||
+ "uniform vec3 cameraPosition;\n"
|
||||
+ "out vec2 v_TexCoord;\n"
|
||||
+ "void main() {\n"
|
||||
+ " v_TexCoord = a_TexCoord;\n"
|
||||
+ " gl_Position = vec4(a_Position - cameraPosition, 1.0);\n"
|
||||
+ "}\n",
|
||||
GL20.GL_VERTEX_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void boolUniformMetadata() {
|
||||
assertHandoffEquivalent("bool-uniform",
|
||||
"uniform bool u_Enabled;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = vec4(u_Enabled ? 1.0 : 0.0); }\n",
|
||||
GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void texelFetchPath() {
|
||||
assertHandoffEquivalent("texelfetch",
|
||||
"uniform sampler2D u_Data;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = texelFetch(u_Data, ivec2(0), 0); }\n",
|
||||
GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void geometryStage() {
|
||||
assertHandoffEquivalent("geometry",
|
||||
"layout(triangles) in;\n"
|
||||
+ "layout(triangle_strip, max_vertices = 3) out;\n"
|
||||
+ "void main() {\n"
|
||||
+ " for (int i = 0; i < 3; i++) {\n"
|
||||
+ " gl_Position = gl_in[i].gl_Position;\n"
|
||||
+ " EmitVertex();\n"
|
||||
+ " }\n"
|
||||
+ " EndPrimitive();\n"
|
||||
+ "}\n",
|
||||
GL32.GL_GEOMETRY_SHADER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
class ThreadRegistryTest {
|
||||
|
||||
private static ThreadRegistry<Object> registry() {
|
||||
return new ThreadRegistry<>(new Object[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void soleTracksTheSingleEntry() {
|
||||
final ThreadRegistry<Object> r = registry();
|
||||
final Object a = new Object();
|
||||
final Object b = new Object();
|
||||
|
||||
assertNull(r.sole(), "empty registry has no sole entry");
|
||||
r.add(a);
|
||||
assertSame(a, r.sole());
|
||||
r.add(b);
|
||||
assertNull(r.sole(), "two entries means no sole entry");
|
||||
r.remove(b);
|
||||
assertSame(a, r.sole());
|
||||
r.remove(a);
|
||||
assertNull(r.sole());
|
||||
assertEquals(0, r.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeOfAnAbsentEntryLeavesTheRegistryAlone() {
|
||||
final ThreadRegistry<Object> r = registry();
|
||||
final Object a = new Object();
|
||||
r.add(a);
|
||||
final Object[] before = r.snapshot();
|
||||
|
||||
assertFalse(r.remove(new Object()));
|
||||
assertSame(before, r.snapshot(), "a no-op remove must not republish");
|
||||
assertSame(a, r.sole());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import com.gtnewhorizons.angelica.glsm.hooks.DeferredBlendHandler;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.DeferredDepthColorHandler;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.ShaderTransformPostProcessor;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderType;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.ShaderWorkSubmitter;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
|
||||
import net.coderbot.iris.Iris;
|
||||
@@ -48,6 +51,27 @@ public class IrisGLSMBridge {
|
||||
StateUpdateNotifiers.colorModulatorNotifier = listener -> colorModulatorListener = listener;
|
||||
}
|
||||
|
||||
public static void installPostTransformHook() {
|
||||
if (GLSMHooks.postTransformProcessor != null) return;
|
||||
GLSMHooks.postTransformProcessor = new ShaderTransformPostProcessor() {
|
||||
@Override
|
||||
public void onTransformed(String src, ShaderType shaderType) {
|
||||
onTransformed(src, null, 0, shaderType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransformed(String src, GLSLParser.Translation_unitContext bodyTree, int headerLen, ShaderType shaderType) {
|
||||
if (Iris.ShaderTransformExecutor.isOnWorker()) {
|
||||
SDLGPUGate.prewarmSpirv(src, bodyTree, headerLen, shaderType.id);
|
||||
} else {
|
||||
Iris.ShaderTransformExecutor.submitTracked(() -> {
|
||||
SDLGPUGate.prewarmSpirv(src, bodyTree, headerLen, shaderType.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
GLSMConfig.expandVertexFormats = Iris.enabled;
|
||||
IrisSamplers.initRenderer();
|
||||
@@ -57,15 +81,7 @@ public class IrisGLSMBridge {
|
||||
return Iris.ShaderTransformExecutor.submitTracked(work);
|
||||
}
|
||||
};
|
||||
GLSMHooks.postTransformProcessor = (src, shaderType) -> {
|
||||
if (Iris.ShaderTransformExecutor.isOnWorker()) {
|
||||
SDLGPUGate.prewarmSpirv(src, shaderType.id);
|
||||
} else {
|
||||
Iris.ShaderTransformExecutor.submitTracked(() -> {
|
||||
SDLGPUGate.prewarmSpirv(src, shaderType.id);
|
||||
});
|
||||
}
|
||||
};
|
||||
installPostTransformHook();
|
||||
GLSMHooks.blendHandler = new DeferredBlendHandler() {
|
||||
@Override
|
||||
public boolean isBlendLocked() {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.proxy;
|
||||
import com.gtnewhorizons.angelica.rendering.culling.GpuCulling;
|
||||
import static com.gtnewhorizons.angelica.AngelicaMod.MOD_ID;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -36,6 +37,7 @@ import com.google.common.base.Objects;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager;
|
||||
import com.gtnewhorizons.angelica.commands.AngelicaCommand;
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
import com.gtnewhorizons.angelica.common.BlockError;
|
||||
import com.gtnewhorizons.angelica.compat.ModStatus;
|
||||
import com.gtnewhorizons.angelica.compat.bettercrashes.BetterCrashesCompat;
|
||||
@@ -52,6 +54,7 @@ import com.gtnewhorizons.angelica.debug.TPSGraph;
|
||||
import com.gtnewhorizons.angelica.dynamiclights.DynamicLights;
|
||||
import com.gtnewhorizons.angelica.dynamiclights.config.EntityLightConfig;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
|
||||
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
|
||||
import com.gtnewhorizons.angelica.iris.IrisGLSMBridge;
|
||||
import com.gtnewhorizons.angelica.loading.AngelicaClientTweaker;
|
||||
@@ -59,8 +62,10 @@ import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
|
||||
import com.gtnewhorizons.angelica.render.CloudRenderer;
|
||||
import com.gtnewhorizons.angelica.render.EmissiveTextureAutoloader;
|
||||
import com.gtnewhorizons.angelica.rendering.AngelicaBlockSafetyRegistry;
|
||||
import com.gtnewhorizons.angelica.rendering.FramePacer;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasDebugScreenHandler;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasSetup;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasWorldRenderer;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.threading.ChunkTaskRegistry;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.threading.DefaultChunkTaskProvider;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.threading.ThreadedChunkTaskProvider;
|
||||
@@ -83,6 +88,7 @@ import jss.notfine.gui.GuiCustomMenu;
|
||||
import jss.notfine.gui.NotFineGameOptionPages;
|
||||
import jss.notfine.gui.options.named.FOVMode;
|
||||
import me.flashyreese.mods.reeses_sodium_options.client.gui.ReeseSodiumVideoOptionsScreen;
|
||||
import me.jellysquid.mods.sodium.client.gui.FrameRateOptions;
|
||||
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
|
||||
import me.jellysquid.mods.sodium.client.gui.SodiumOptionsGUI;
|
||||
import net.coderbot.iris.Iris;
|
||||
@@ -106,8 +112,15 @@ public final class ClientProxy extends CommonProxy {
|
||||
return CONFIG;
|
||||
}
|
||||
|
||||
public static void applyVSyncMode(boolean vsyncEnabled) {
|
||||
GLStateManager.setVSyncMode(options().advanced.vsyncMode, vsyncEnabled);
|
||||
public static void toggleVSync(boolean enabled) {
|
||||
final SodiumGameOptions opts = options();
|
||||
opts.advanced.vsyncMode = enabled ? GLStateManager.preferredTearFreeMode() : VSyncMode.OFF;
|
||||
try {
|
||||
opts.writeChanges();
|
||||
} catch (IOException e) {
|
||||
AngelicaMod.LOGGER.warn("Could not persist the vsync mode", e);
|
||||
}
|
||||
GLStateManager.setVSyncMode(opts.advanced.vsyncMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,6 +134,9 @@ public final class ClientProxy extends CommonProxy {
|
||||
MinecraftForge.EVENT_BUS.register(new AngelicaTesrMeshCache.ReloadListener());
|
||||
ModelRegistry.registerModid(MOD_ID);
|
||||
blockError = new BlockError();
|
||||
if (AngelicaConfig.enableIris) {
|
||||
Iris.warmupShaderTransforms();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -131,6 +147,16 @@ public final class ClientProxy extends CommonProxy {
|
||||
HUDCaching.init();
|
||||
}
|
||||
CeleritasSetup.ensureInitialized();
|
||||
final SodiumGameOptions opts = options();
|
||||
if (opts.advanced.vsyncMode == null) {
|
||||
opts.advanced.vsyncMode = FrameRateOptions.defaultMode();
|
||||
}
|
||||
Minecraft.getMinecraft().gameSettings.enableVsync = opts.advanced.vsyncMode.tearFree();
|
||||
GLStateManager.setVSyncPreference(opts.advanced.vsyncMode);
|
||||
FramePacer.setIdleWork(deadlineNanos -> {
|
||||
final CeleritasWorldRenderer renderer = CeleritasWorldRenderer.getInstanceOrNull();
|
||||
if (renderer != null) renderer.runFrameIdleWork(deadlineNanos);
|
||||
});
|
||||
MinecraftForge.EVENT_BUS.register(CeleritasDebugScreenHandler.INSTANCE);
|
||||
if (AngelicaConfig.enableIris) {
|
||||
IrisGLSMBridge.register();
|
||||
|
||||
@@ -3,114 +3,262 @@ package com.gtnewhorizons.angelica.rendering;
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
public final class FramePacer {
|
||||
|
||||
public interface IdleWork {
|
||||
void run(long deadlineNanos);
|
||||
}
|
||||
|
||||
private static final int MIN_PLAUSIBLE_REFRESH_HZ = 20;
|
||||
private static final int MAX_PLAUSIBLE_REFRESH_HZ = 1000;
|
||||
private static final long REFRESH_POLL_NANOS = 1_000_000_000L;
|
||||
private static final long IDLE_WORK_MIN_BUDGET_NANOS = 500_000L;
|
||||
private static final long GATE_BLOCK_MIN_NANOS = 100_000L;
|
||||
private static final int GATE_STREAK_FRAMES = 60;
|
||||
static final int GATE_SETTLE_FRAMES = 120;
|
||||
static final int PACING_HEADROOM_PERCENT = 5;
|
||||
|
||||
private static final Tracy.ZoneId Z_PACER_WAIT = Tracy.zoneId("pacerBackpressure", Tracy.COLOR_SWAP);
|
||||
private static final Tracy.ZoneId Z_PACER_IDLE = Tracy.zoneId("pacerIdleWork", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_PACER_SLEEP = Tracy.zoneId("pacerSleep", Tracy.COLOR_SWAP);
|
||||
private static final long P_FRAME_GATE_US = Tracy.plotHandle("frameGateUs");
|
||||
|
||||
private static long deadline;
|
||||
private static long lastFrameTime;
|
||||
private static long fpsLimitOverhead;
|
||||
|
||||
private static long lastRefreshPollNanos;
|
||||
private static int cachedRefreshHz;
|
||||
private static boolean warnedUnknownRefresh;
|
||||
private static boolean warnedUncapped;
|
||||
|
||||
private static VSyncMode lastVSyncMode = VSyncMode.ON;
|
||||
private static int lastCapHz;
|
||||
private static int lastRefreshHz;
|
||||
private static int lastCeilingHz;
|
||||
|
||||
private static IdleWork idleWork;
|
||||
private static PacerSleeper sleeper = newSleeper();
|
||||
private static int gateStreak;
|
||||
private static Boolean settledBlocking;
|
||||
private static boolean candidateBlocking;
|
||||
private static int candidateFrames;
|
||||
private static Boolean loggedBlocking;
|
||||
private static VSyncMode loggedMode;
|
||||
private static int loggedRefreshHz;
|
||||
private static int loggedCapHz;
|
||||
|
||||
private static boolean gateBlocking;
|
||||
private static long lastSeenGateEnd;
|
||||
|
||||
private FramePacer() {}
|
||||
|
||||
private static PacerSleeper newSleeper() {
|
||||
return new PacerSleeper(System::nanoTime, LockSupport::parkNanos, true);
|
||||
}
|
||||
|
||||
public static void setIdleWork(IdleWork work) {
|
||||
idleWork = work;
|
||||
}
|
||||
|
||||
public static int plausibleRefreshHz(int hz) {
|
||||
return hz >= MIN_PLAUSIBLE_REFRESH_HZ && hz <= MAX_PLAUSIBLE_REFRESH_HZ ? hz : 0;
|
||||
}
|
||||
|
||||
public static int pacingCeilingHz(VSyncMode mode, int capHz, int refreshHz) {
|
||||
if (mode.blocksOnVBlank()) return 0;
|
||||
public static int withHeadroom(int hz, int percent) {
|
||||
if (hz <= 0) return 0;
|
||||
return (int) Math.max(hz + 1L, Math.round(hz * (100 + percent) / 100.0));
|
||||
}
|
||||
|
||||
public static int pacingCeilingHz(VSyncMode mode, int capHz, int refreshHz, boolean gateActive) {
|
||||
final int cap = Math.max(capHz, 0);
|
||||
if (mode != VSyncMode.MAILBOX) return cap;
|
||||
final int backstop = backstopHz(mode, refreshHz, gateActive);
|
||||
if (cap == 0) return backstop;
|
||||
if (backstop == 0) return cap;
|
||||
return Math.min(cap, backstop);
|
||||
}
|
||||
|
||||
static int backstopHz(VSyncMode mode, int refreshHz, boolean gateActive) {
|
||||
if (!mode.tearFree()) return 0;
|
||||
final int refresh = plausibleRefreshHz(refreshHz);
|
||||
if (cap == 0) return refresh;
|
||||
return refresh == 0 ? cap : Math.min(cap, refresh);
|
||||
if (mode == VSyncMode.ON && gateActive) return withHeadroom(refresh, PACING_HEADROOM_PERCENT);
|
||||
return refresh;
|
||||
}
|
||||
|
||||
public static long sleepNanosFor(long targetNanos, long lastWorkNanos, long overheadNanos) {
|
||||
return targetNanos - lastWorkNanos - overheadNanos;
|
||||
static boolean observeGate(long gateNanos, long thresholdNanos) {
|
||||
if (gateNanos > thresholdNanos) gateStreak = GATE_STREAK_FRAMES;
|
||||
else if (gateStreak > 0) gateStreak--;
|
||||
return gateStreak > 0;
|
||||
}
|
||||
|
||||
public static int updateCeiling(int capHz) {
|
||||
static boolean gateActive() {
|
||||
return gateStreak > 0;
|
||||
}
|
||||
|
||||
static Boolean settleGateBlocking(boolean observed) {
|
||||
if (settledBlocking != null && observed == settledBlocking) {
|
||||
candidateFrames = 0;
|
||||
} else if (candidateFrames == 0 || observed != candidateBlocking) {
|
||||
candidateBlocking = observed;
|
||||
candidateFrames = 1;
|
||||
} else if (++candidateFrames >= GATE_SETTLE_FRAMES) {
|
||||
settledBlocking = observed;
|
||||
candidateFrames = 0;
|
||||
}
|
||||
return settledBlocking;
|
||||
}
|
||||
|
||||
static boolean vsyncNotHonoured(VSyncMode mode, boolean gateBlocking) {
|
||||
return mode == VSyncMode.ON && !gateBlocking;
|
||||
}
|
||||
|
||||
static boolean pacingStatusChanged(boolean blocking, VSyncMode mode, int refreshHz, int capHz) {
|
||||
final Boolean settled = settleGateBlocking(blocking);
|
||||
if (settled == null) return false;
|
||||
if (settled.equals(loggedBlocking) && mode == loggedMode && refreshHz == loggedRefreshHz && capHz == loggedCapHz) {
|
||||
return false;
|
||||
}
|
||||
loggedBlocking = settled;
|
||||
loggedMode = mode;
|
||||
loggedRefreshHz = refreshHz;
|
||||
loggedCapHz = capHz;
|
||||
return true;
|
||||
}
|
||||
|
||||
static String pacingLine(VSyncMode mode, int refreshHz, int capHz, boolean gateBlocking) {
|
||||
final StringBuilder sb = new StringBuilder("Frame pacing: ");
|
||||
sb.append(switch (mode) {
|
||||
case OFF -> "vsync off";
|
||||
case MAILBOX -> "mailbox";
|
||||
case ON -> "vsync";
|
||||
});
|
||||
if (mode.tearFree() && refreshHz > 0) sb.append(' ').append(refreshHz).append("Hz");
|
||||
if (capHz > 0) sb.append(", cap ").append(capHz);
|
||||
if (vsyncNotHonoured(mode, gateBlocking)) sb.append(", driver not blocking, paced by wall clock");
|
||||
else if (gateBlocking) sb.append(", hardware paced");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static long gateBlockThresholdNanos(long targetNanos) {
|
||||
return Math.max(GATE_BLOCK_MIN_NANOS, targetNanos / 32);
|
||||
}
|
||||
|
||||
public static long nextDeadline(long previousDeadline, long now, long targetNanos) {
|
||||
if (previousDeadline == 0L || now - previousDeadline > 2 * targetNanos) return now + targetNanos;
|
||||
return previousDeadline + targetNanos;
|
||||
}
|
||||
|
||||
public static long endFrame(int capHz, Runnable renderAheadWait) {
|
||||
lastVSyncMode = GLStateManager.getEffectiveVSyncMode();
|
||||
lastCapHz = Math.max(capHz, 0);
|
||||
lastRefreshHz = refreshHz();
|
||||
return pacingCeilingHz(lastVSyncMode, lastCapHz, lastRefreshHz);
|
||||
return pace(pacingCeilingHz(lastVSyncMode, lastCapHz, lastRefreshHz, gateActive()), renderAheadWait);
|
||||
}
|
||||
|
||||
public static long pace(int ceilingHz) {
|
||||
if (ceilingHz > 0) {
|
||||
final long time = System.nanoTime();
|
||||
final long targetNanos = 1_000_000_000L / ceilingHz;
|
||||
|
||||
final long sleepNanos = sleepNanosFor(targetNanos, time - lastFrameTime, fpsLimitOverhead);
|
||||
if (sleepNanos > 0) {
|
||||
try {
|
||||
Thread.sleep(sleepNanos / 1_000_000, (int) sleepNanos % 1_000_000);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
long overhead = System.nanoTime() - time - sleepNanos;
|
||||
if (overhead < 0 || overhead > targetNanos / 2) overhead = 0;
|
||||
fpsLimitOverhead = overhead;
|
||||
}
|
||||
public static long pace(int ceilingHz, Runnable renderAheadWait) {
|
||||
if (ceilingHz != lastCeilingHz) {
|
||||
lastCeilingHz = ceilingHz;
|
||||
deadline = 0L;
|
||||
}
|
||||
|
||||
final long time = System.nanoTime();
|
||||
final long period = time - lastFrameTime;
|
||||
lastFrameTime = time;
|
||||
long now = System.nanoTime();
|
||||
long phaseEnd;
|
||||
|
||||
final long targetNanos = ceilingHz > 0 ? 1_000_000_000L / ceilingHz : 0L;
|
||||
final long thresholdNanos = gateBlockThresholdNanos(targetNanos);
|
||||
long frameGateNanos = 0L;
|
||||
|
||||
if (ceilingHz > 0) {
|
||||
frameGateNanos = GLStateManager.lastFrameGateNanos();
|
||||
final long gateEnd = GLStateManager.lastFrameGateEndNanos();
|
||||
Tracy.plotInt(P_FRAME_GATE_US, frameGateNanos / 1000);
|
||||
|
||||
if (gateEnd != lastSeenGateEnd) {
|
||||
lastSeenGateEnd = gateEnd;
|
||||
if (frameGateNanos > thresholdNanos && GLStateManager.gateAnchorsNextFrameStart()) {
|
||||
deadline = gateEnd;
|
||||
}
|
||||
}
|
||||
deadline = nextDeadline(deadline, now, targetNanos);
|
||||
|
||||
final IdleWork work = idleWork;
|
||||
if (work != null && deadline - now > IDLE_WORK_MIN_BUDGET_NANOS) {
|
||||
Tracy.beginZone(Z_PACER_IDLE);
|
||||
try {
|
||||
work.run(deadline);
|
||||
} finally {
|
||||
Tracy.endZone();
|
||||
}
|
||||
phaseEnd = System.nanoTime();
|
||||
now = phaseEnd;
|
||||
}
|
||||
|
||||
if (now < deadline) {
|
||||
final long preSleep = now;
|
||||
Tracy.beginZone(Z_PACER_SLEEP);
|
||||
try {
|
||||
now = sleeper.sleepUntil(deadline, now);
|
||||
} finally {
|
||||
Tracy.endZone();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deadline = 0L;
|
||||
}
|
||||
|
||||
Tracy.beginZone(Z_PACER_WAIT);
|
||||
try {
|
||||
if (renderAheadWait != null) renderAheadWait.run();
|
||||
} finally {
|
||||
Tracy.endZone();
|
||||
}
|
||||
phaseEnd = System.nanoTime();
|
||||
final long waitNanos = phaseEnd - now;
|
||||
now = phaseEnd;
|
||||
if (ceilingHz > 0 && waitNanos > thresholdNanos) deadline = now;
|
||||
gateBlocking = observeGate(ceilingHz > 0 ? Math.max(frameGateNanos, waitNanos) : 0L, thresholdNanos);
|
||||
|
||||
GLStateManager.pumpDisplayMessages();
|
||||
|
||||
if (pacingStatusChanged(gateBlocking, lastVSyncMode, lastRefreshHz, lastCapHz)) {
|
||||
final boolean blocking = settledBlocking;
|
||||
final String line = pacingLine(lastVSyncMode, lastRefreshHz, lastCapHz, blocking);
|
||||
if (vsyncNotHonoured(lastVSyncMode, blocking)) AngelicaMod.LOGGER.warn(line);
|
||||
else AngelicaMod.LOGGER.info(line);
|
||||
}
|
||||
|
||||
final long period = lastFrameTime == 0L ? 0L : now - lastFrameTime;
|
||||
lastFrameTime = now;
|
||||
return period;
|
||||
}
|
||||
|
||||
private static int refreshHz() {
|
||||
final long now = System.nanoTime();
|
||||
if (lastRefreshPollNanos == 0L || now - lastRefreshPollNanos >= REFRESH_POLL_NANOS) {
|
||||
lastRefreshPollNanos = now;
|
||||
cachedRefreshHz = GLStateManager.getDisplayRefreshRateHz();
|
||||
}
|
||||
final int hz = plausibleRefreshHz(cachedRefreshHz);
|
||||
final int reported = GLStateManager.getDisplayRefreshRateHz();
|
||||
final int hz = plausibleRefreshHz(reported);
|
||||
|
||||
if (hz == 0 && lastVSyncMode == VSyncMode.MAILBOX && !warnedUnknownRefresh) {
|
||||
warnedUnknownRefresh = true;
|
||||
AngelicaMod.LOGGER.warn("Display refresh rate reported as {}Hz; Mailbox will not be paced to the refresh rate. The Max Framerate setting still applies.", cachedRefreshHz);
|
||||
if (hz == 0 && lastCapHz == 0 && lastVSyncMode.tearFree() && !warnedUncapped) {
|
||||
warnedUncapped = true;
|
||||
AngelicaMod.LOGGER.warn("Display refresh rate reported as {}Hz, so the frame rate cannot be bounded if the driver ignores vsync. Set Max Framerate to bound it.", reported);
|
||||
}
|
||||
return hz;
|
||||
}
|
||||
|
||||
public static String debugIndicator(VSyncMode mode, int refreshHz, int capHz) {
|
||||
final boolean vsync = mode.blocksOnVBlank();
|
||||
final boolean mailbox = mode == VSyncMode.MAILBOX;
|
||||
|
||||
final boolean showCap = capHz > 0 && !vsync;
|
||||
if (!vsync && !mailbox && !showCap) return null;
|
||||
public static String debugIndicator(VSyncMode mode, int refreshHz, int capHz, boolean gateActive) {
|
||||
final int cap = Math.max(capHz, 0);
|
||||
final boolean showCap = cap > 0 && cap == pacingCeilingHz(mode, cap, refreshHz, gateActive);
|
||||
if (!mode.tearFree() && !showCap) return null;
|
||||
|
||||
final StringBuilder sb = new StringBuilder(" [");
|
||||
if (vsync || mailbox) {
|
||||
sb.append(vsync ? "vsync" : "mailbox");
|
||||
if (refreshHz > 0) sb.append(' ').append(refreshHz);
|
||||
if (mode.tearFree()) {
|
||||
sb.append(mode == VSyncMode.MAILBOX ? "mailbox" : "vsync");
|
||||
final int refresh = plausibleRefreshHz(refreshHz);
|
||||
if (refresh > 0) sb.append(' ').append(refresh);
|
||||
if (showCap) sb.append(", ");
|
||||
}
|
||||
if (showCap) sb.append("cap ").append(capHz);
|
||||
if (showCap) sb.append("cap ").append(cap);
|
||||
return sb.append(']').toString();
|
||||
}
|
||||
|
||||
public static String debugIndicator() {
|
||||
return debugIndicator(lastVSyncMode, lastRefreshHz, lastCapHz);
|
||||
return debugIndicator(lastVSyncMode, lastRefreshHz, lastCapHz, gateActive());
|
||||
}
|
||||
|
||||
public static VSyncMode lastVSyncMode() { return lastVSyncMode; }
|
||||
|
||||
public static int lastCapHz() { return lastCapHz; }
|
||||
|
||||
public static int lastRefreshHz() { return lastRefreshHz; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
import java.util.function.LongConsumer;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
final class PacerSleeper {
|
||||
|
||||
private static final long SPIN_FLOOR_NANOS = 100_000L;
|
||||
private static final long MIN_PARK_NANOS = 1_000_000L;
|
||||
private static final long MAX_OVERSHOOT_NANOS = 4_000_000L;
|
||||
private static final int SLOTS = 10;
|
||||
|
||||
private final LongSupplier clock;
|
||||
private final LongConsumer parker;
|
||||
private final boolean spinWait;
|
||||
private final long[] overshoots = new long[SLOTS];
|
||||
private long overshootSum;
|
||||
private int offset;
|
||||
|
||||
PacerSleeper(LongSupplier clock, LongConsumer parker, boolean spinWait) {
|
||||
this.clock = clock;
|
||||
this.parker = parker;
|
||||
this.spinWait = spinWait;
|
||||
}
|
||||
|
||||
long sleepUntil(long deadlineNanos, long now) {
|
||||
while (true) {
|
||||
final long remaining = deadlineNanos - now;
|
||||
if (remaining <= SPIN_FLOOR_NANOS) break;
|
||||
if (Thread.currentThread().isInterrupted()) return clock.getAsLong();
|
||||
|
||||
long request = remaining - avgOvershootNanos() - SPIN_FLOOR_NANOS;
|
||||
if (request <= 0L) {
|
||||
if (remaining <= MIN_PARK_NANOS) break;
|
||||
request = MIN_PARK_NANOS;
|
||||
}
|
||||
|
||||
final long before = now;
|
||||
parker.accept(request);
|
||||
now = clock.getAsLong();
|
||||
record(Math.min(Math.max(0L, now - before - request), MAX_OVERSHOOT_NANOS));
|
||||
}
|
||||
|
||||
if (spinWait) {
|
||||
while (now < deadlineNanos) {
|
||||
Thread.yield();
|
||||
now = clock.getAsLong();
|
||||
}
|
||||
}
|
||||
return now;
|
||||
}
|
||||
|
||||
long avgOvershootNanos() {
|
||||
return overshootSum / SLOTS;
|
||||
}
|
||||
|
||||
private void record(long nanos) {
|
||||
overshootSum += nanos - overshoots[offset];
|
||||
overshoots[offset] = nanos;
|
||||
offset = (offset + 1) % SLOTS;
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -12,6 +12,7 @@ import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACK
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
|
||||
import com.gtnewhorizons.angelica.glsm.states.FogState;
|
||||
import com.gtnewhorizons.angelica.rendering.FramePacer;
|
||||
import com.gtnewhorizons.angelica.rendering.tesr.ModelPartBatcher;
|
||||
import com.gtnewhorizons.angelica.rendering.tesr.TesrBatchRenderer;
|
||||
import cpw.mods.fml.common.eventhandler.EventPriority;
|
||||
@@ -78,10 +79,6 @@ public class CeleritasDebugScreenHandler {
|
||||
if (transferInfo != null) {
|
||||
lines.add(transferInfo);
|
||||
}
|
||||
final String presentInfo = RENDER_BACKEND.getPresentDebugInfo();
|
||||
if (presentInfo != null) {
|
||||
lines.add(presentInfo);
|
||||
}
|
||||
if (SystemProperties.DEBUG_F3_DETAIL) {
|
||||
lines.addAll(TesrBatchRenderer.INSTANCE.getDebugDetailStrings());
|
||||
lines.add(ModelPartBatcher.INSTANCE.getDebugString());
|
||||
|
||||
+29
@@ -21,6 +21,7 @@ import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.embeddedt.embeddium.impl.gl.device.CommandList;
|
||||
import org.embeddedt.embeddium.impl.gl.device.RenderDevice;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.ChunkRenderMatrices;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.RenderSection;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.data.MinecraftBuiltRenderSectionData;
|
||||
@@ -114,6 +115,34 @@ public class CeleritasWorldRenderer extends SimpleWorldRenderer<WorldClient, Ang
|
||||
return this.renderSectionManager;
|
||||
}
|
||||
|
||||
private long asyncEwmaNanos;
|
||||
private long uploadEwmaNanos;
|
||||
|
||||
private static long blendEwma(long ewmaNanos, long tookNanos) {
|
||||
return ewmaNanos == 0L ? tookNanos : (ewmaNanos * 3 + tookNanos) / 4;
|
||||
}
|
||||
|
||||
public void runFrameIdleWork(long deadlineNanos) {
|
||||
final AngelicaRenderSectionManager rsm = this.renderSectionManager;
|
||||
if (rsm == null || this.world == null) return;
|
||||
RenderDevice.enterManagedCode();
|
||||
try {
|
||||
long start = System.nanoTime();
|
||||
if (start + asyncEwmaNanos > deadlineNanos) return;
|
||||
rsm.runAsyncTasks();
|
||||
long now = System.nanoTime();
|
||||
asyncEwmaNanos = blendEwma(asyncEwmaNanos, now - start);
|
||||
|
||||
start = now;
|
||||
if (start + uploadEwmaNanos > deadlineNanos) return;
|
||||
rsm.uploadChunks();
|
||||
now = System.nanoTime();
|
||||
uploadEwmaNanos = blendEwma(uploadEwmaNanos, now - start);
|
||||
} finally {
|
||||
RenderDevice.exitManagedCode();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return this.world != null;
|
||||
|
||||
@@ -14,39 +14,63 @@ import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
import net.minecraft.client.settings.GameSettings;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
public record FrameRateOptions(Option<VSyncMode> vsync, Option<Integer> maxFramerate) {
|
||||
|
||||
public static FrameRateOptions create(MinecraftOptionsStorage vanillaOpts, SodiumOptionsStorage sodiumOpts) {
|
||||
final VSyncMode[] allowed = selectableModes();
|
||||
final String[] names = modeNames(VSyncMode.values());
|
||||
|
||||
final OptionImpl<SodiumGameOptions, VSyncMode> vsync = OptionImpl.createBuilder(VSyncMode.class, sodiumOpts)
|
||||
.setName(I18n.format("options.vsync"))
|
||||
.setTooltip(I18n.format("sodium.options.v_sync.tooltip"))
|
||||
.setControl(option -> new CyclingControl<>(option, VSyncMode.class,
|
||||
GLStateManager.getSelectableVSyncModes().toArray(new VSyncMode[0]),
|
||||
new String[]{
|
||||
I18n.format("sodium.options.vsync_mode.auto"),
|
||||
I18n.format("sodium.options.vsync_mode.on"),
|
||||
I18n.format("sodium.options.vsync_mode.mailbox"),
|
||||
I18n.format("sodium.options.vsync_mode.off")}))
|
||||
.setControl(option -> new CyclingControl<>(option, VSyncMode.class, allowed, names))
|
||||
.setBinding((opts, value) -> {
|
||||
opts.advanced.vsyncMode = value;
|
||||
|
||||
final GameSettings settings = Minecraft.getMinecraft().gameSettings;
|
||||
settings.enableVsync = value.tearFree();
|
||||
settings.saveOptions();
|
||||
GLStateManager.setVSyncMode(value, settings.enableVsync);
|
||||
}, opts -> opts.advanced.vsyncMode.resolve(Minecraft.getMinecraft().gameSettings.enableVsync))
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
mc.gameSettings.enableVsync = value.tearFree();
|
||||
mc.gameSettings.saveOptions();
|
||||
GLStateManager.setVSyncMode(value);
|
||||
}, FrameRateOptions::storedMode)
|
||||
.setImpact(OptionImpact.VARIES)
|
||||
.build();
|
||||
|
||||
final OptionImpl<GameSettings, Integer> maxFramerate = OptionImpl.createBuilder(int.class, vanillaOpts)
|
||||
.setName(I18n.format("options.framerateLimit"))
|
||||
.setTooltip(I18n.format("sodium.options.fps_limit.tooltip"))
|
||||
.setControl(option -> new SliderControl(option, 5, 260, 5, ControlValueFormatter.fpsLimit()))
|
||||
.setControl(option -> new SliderControl(option, 5, 260, 1, ControlValueFormatter.fpsLimit()))
|
||||
.setBinding((opts, value) -> opts.limitFramerate = value, opts -> opts.limitFramerate)
|
||||
.build();
|
||||
|
||||
maxFramerate.iris$dynamicallyEnable(() -> !vsync.getValue().blocksOnVBlank());
|
||||
|
||||
return new FrameRateOptions(vsync, maxFramerate);
|
||||
}
|
||||
|
||||
static VSyncMode[] selectableModes() {
|
||||
final VSyncMode[] supported = Arrays.stream(VSyncMode.values())
|
||||
.filter(GLStateManager::supportsVSyncMode)
|
||||
.toArray(VSyncMode[]::new);
|
||||
return supported.length == 0 ? new VSyncMode[]{ VSyncMode.ON } : supported;
|
||||
}
|
||||
|
||||
static String[] modeKeys(VSyncMode[] modes) {
|
||||
return Arrays.stream(modes)
|
||||
.map(mode -> "sodium.options.vsync_mode." + mode.name().toLowerCase(Locale.ROOT))
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
private static String[] modeNames(VSyncMode[] modes) {
|
||||
return Arrays.stream(modeKeys(modes)).map(I18n::format).toArray(String[]::new);
|
||||
}
|
||||
|
||||
public static VSyncMode defaultMode() {
|
||||
return Minecraft.getMinecraft().gameSettings.enableVsync ? GLStateManager.preferredTearFreeMode() : VSyncMode.OFF;
|
||||
}
|
||||
|
||||
public static VSyncMode storedMode(SodiumGameOptions opts) {
|
||||
final VSyncMode stored = opts.advanced.vsyncMode;
|
||||
if (stored == null) return defaultMode();
|
||||
return GLStateManager.supportsVSyncMode(stored) ? stored : VSyncMode.ON;
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,9 @@ public class SodiumGameOptionPages {
|
||||
private static final MinecraftOptionsStorage vanillaOpts = new MinecraftOptionsStorage();
|
||||
private static final AngelicaOptionsStorage angelicaOpts = new AngelicaOptionsStorage();
|
||||
|
||||
private static final int MIN_RENDER_AHEAD = 0;
|
||||
private static final int MAX_RENDER_AHEAD = 9;
|
||||
|
||||
public static OptionPage general() {
|
||||
final List<OptionGroup> groups = new ArrayList<>();
|
||||
final OptionGroup.Builder firstGroupBuilder = OptionGroup.createBuilder();
|
||||
@@ -508,12 +511,9 @@ public class SodiumGameOptionPages {
|
||||
.add(OptionImpl.createBuilder(int.class, sodiumOpts)
|
||||
.setName(I18n.format("sodium.options.cpu_render_ahead_limit.name"))
|
||||
.setTooltip(I18n.format("sodium.options.cpu_render_ahead_limit.tooltip"))
|
||||
.setControl(o -> new SliderControl(o, GLStateManager.getMinRenderAhead(), GLStateManager.getMaxRenderAhead(), 1, ControlValueFormatter.quantity("sodium.options.cpu_render_ahead_limit.value")))
|
||||
.setControl(o -> new SliderControl(o, MIN_RENDER_AHEAD, MAX_RENDER_AHEAD, 1, ControlValueFormatter.quantity("sodium.options.cpu_render_ahead_limit.value")))
|
||||
.setImpact(OptionImpact.MEDIUM)
|
||||
.setBinding((opts, value) -> {
|
||||
opts.performance.cpuRenderAheadLimit = value;
|
||||
GLStateManager.applyRenderAheadLimit(value);
|
||||
}, opts -> GLStateManager.renderAheadLimit(opts.performance.cpuRenderAheadLimit))
|
||||
.setBinding((opts, value) -> opts.performance.cpuRenderAheadLimit = value, opts -> opts.performance.cpuRenderAheadLimit)
|
||||
.setEnabled(GLStateManager.capabilities != null && GLStateManager.capabilities.OpenGL32)
|
||||
.build())
|
||||
|
||||
|
||||
@@ -195,6 +195,7 @@ public class SodiumGameOptions {
|
||||
}
|
||||
|
||||
public static class AdvancedSettings {
|
||||
public VSyncMode vsyncMode;
|
||||
public boolean useVertexArrayObjects = true;
|
||||
public boolean useChunkMultidraw = true;
|
||||
public MultiDrawMode multiDrawMode = MultiDrawMode.DIRECT;
|
||||
@@ -204,7 +205,6 @@ public class SodiumGameOptions {
|
||||
public boolean ignoreDriverBlacklist = false;
|
||||
public StreamingUploader.UploadStrategy streamingUploadStrategy = StreamingUploader.UploadStrategy.MAP_BUFFER_RANGE;
|
||||
public boolean enableDeferredBatching = true;
|
||||
public VSyncMode vsyncMode = VSyncMode.AUTO;
|
||||
}
|
||||
|
||||
public static class PerformanceSettings {
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.gtnewhorizons.angelica.glsm.RenderSystem;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
|
||||
import com.gtnewhorizons.angelica.iris.IrisGLSMBridge;
|
||||
import com.gtnewhorizons.angelica.proxy.ClientProxy;
|
||||
import com.gtnewhorizons.angelica.rendering.StateAwareTessellator;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder;
|
||||
@@ -533,6 +534,20 @@ public class Iris {
|
||||
}
|
||||
}
|
||||
|
||||
public static void warmupShaderTransforms() {
|
||||
if (currentPack == null) return;
|
||||
try {
|
||||
if (BackendManager.RENDER_BACKEND.isSDLGPU()) {
|
||||
IrisGLSMBridge.installPostTransformHook();
|
||||
}
|
||||
final ProgramSet programs = currentPack.getProgramSet(lastDimensionName != null ? lastDimensionName : "Overworld");
|
||||
PerFrameUniformBlockHarvester.harvest(programs);
|
||||
DeferredWorldRenderingPipeline.warmupTransforms(programs);
|
||||
} catch (Throwable t) {
|
||||
logger.warn("Early shader transform warmup failed; transforms will run at pipeline creation", t);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean loadExternalShaderpack(String name) {
|
||||
final Path shaderPackRoot;
|
||||
final Path shaderPackConfigTxt;
|
||||
|
||||
@@ -119,6 +119,7 @@ import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.IntFunction;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -236,6 +237,11 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
|
||||
final long _t0 = System.nanoTime();
|
||||
long _tLast = _t0;
|
||||
|
||||
final int entryTextureUnit = GLStateManager.getActiveTextureUnit();
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE2);
|
||||
customTextureManager = new CustomTextureManager(programs.getPackDirectives(), programs.getPack().getCustomTextureDataMap(), programs.getPack().getIrisCustomTextureDataMap(), programs.getPack().getCustomNoiseTexture());
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + entryTextureUnit);
|
||||
|
||||
final Map<Integer, CompletableFuture<Map<PatchShaderType, String>>> prepareTransformFutures =
|
||||
submitCompositeTransforms(programs.getPrepare(), TextureStage.PREPARE, programs.getPackDirectives().getTextureMap());
|
||||
final Map<Integer, CompletableFuture<Map<PatchShaderType, String>>> deferredTransformFutures =
|
||||
@@ -340,8 +346,6 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
|
||||
// Don't clobber anything in texture unit 0. It probably won't cause issues, but we're just being cautious here.
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE2);
|
||||
|
||||
customTextureManager = new CustomTextureManager(programs.getPackDirectives(), programs.getPack().getCustomTextureDataMap(), programs.getPack().getIrisCustomTextureDataMap(), programs.getPack().getCustomNoiseTexture());
|
||||
|
||||
whitePixel = new NativeImageBackedSingleColorTexture(255, 255, 255, 255);
|
||||
|
||||
// Initialize custom images
|
||||
@@ -488,7 +492,9 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
|
||||
this.instancedAttributeTransforms = new HashMap<>();
|
||||
for (Map.Entry<Pair<String, InputAvailability>, CompletableFuture<Map<PatchShaderType, String>>> entry : instancedTransformFutures.entrySet()) {
|
||||
try {
|
||||
this.instancedAttributeTransforms.put(entry.getKey(), entry.getValue().join());
|
||||
final Map<PatchShaderType, String> transformed = entry.getValue().join();
|
||||
if (transformed == null) continue;
|
||||
this.instancedAttributeTransforms.put(entry.getKey(), transformed);
|
||||
} catch (Exception e) {
|
||||
Iris.logger.warn("Instanced transform failed for {}; disabling TESR instancing", entry.getKey().getLeft(), e);
|
||||
supportsTesrInstancing = false;
|
||||
@@ -2174,20 +2180,26 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
|
||||
submitAttributeTransforms(ProgramFallbackResolver resolver, ProgramId[] ids, boolean instanced) {
|
||||
final Map<Pair<String, InputAvailability>, CompletableFuture<Map<PatchShaderType, String>>> futures = new HashMap<>();
|
||||
final Set<String> processedSourceNames = new HashSet<>();
|
||||
final Map<String, Boolean> mvBuiltinsMemo = new ConcurrentHashMap<>();
|
||||
for (ProgramId id : ids) {
|
||||
if (id.getGroup() == ProgramGroup.Dh) continue;
|
||||
final ProgramSource source = resolver.resolveNullable(id);
|
||||
if (source == null || !processedSourceNames.add(source.getName())) {
|
||||
continue;
|
||||
}
|
||||
if (instanced && referencesMvBuiltinsOutsideVertex(source)) {
|
||||
Iris.logger.info("TESR instancing: {} references MV builtins outside the vertex stage, keeping CPU path", source.getName());
|
||||
continue;
|
||||
}
|
||||
final String vertexSource = source.getVertexSource().orElse(null);
|
||||
final boolean scrollGlint = GlintScrollInjector.shouldInject(id, source);
|
||||
for (InputAvailability avail : INPUT_AVAILABILITIES) {
|
||||
futures.put(Pair.of(source.getName(), avail), Iris.ShaderTransformExecutor.submitTracked(() -> {
|
||||
if (instanced && mvBuiltinsMemo.computeIfAbsent(source.getName(), k -> {
|
||||
final boolean references = referencesMvBuiltinsOutsideVertex(source);
|
||||
if (references) {
|
||||
Iris.logger.info("TESR instancing: {} references MV builtins outside the vertex stage, keeping CPU path", k);
|
||||
}
|
||||
return references;
|
||||
})) {
|
||||
return null;
|
||||
}
|
||||
final String geometry = source.getGeometrySource().orElse(null);
|
||||
final String tessControl = source.getTessControlSource().orElse(null);
|
||||
final String tessEval = source.getTessEvalSource().orElse(null);
|
||||
@@ -2263,4 +2275,38 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static void warmupTransforms(ProgramSet programs) {
|
||||
final Object2ObjectMap<Tri<String, TextureType, TextureStage>, String> tm = programs.getPackDirectives().getTextureMap();
|
||||
submitCompositeTransforms(programs.getPrepare(), TextureStage.PREPARE, tm);
|
||||
submitCompositeTransforms(programs.getDeferred(), TextureStage.DEFERRED, tm);
|
||||
submitCompositeTransforms(programs.getComposite(), TextureStage.COMPOSITE_AND_FINAL, tm);
|
||||
programs.getCompositeFinal().filter(ProgramSource::isValid)
|
||||
.ifPresent(source -> submitCompositeTransform(source, TextureStage.COMPOSITE_AND_FINAL, tm));
|
||||
|
||||
final ProgramFallbackResolver resolver = new ProgramFallbackResolver(programs);
|
||||
submitAttributeTransforms(resolver, ProgramId.values(), false);
|
||||
submitAttributeTransforms(resolver, INSTANCED_PROGRAM_IDS, true);
|
||||
|
||||
final Optional<ProgramSource> terrainSource = first(programs.getGbuffersTerrain(), programs.getGbuffersTexturedLit(), programs.getGbuffersTextured(), programs.getGbuffersBasic());
|
||||
final Optional<ProgramSource> translucentSource = first(programs.getGbuffersWater(), terrainSource);
|
||||
final Optional<ProgramSource> shadowSource = programs.getShadow();
|
||||
final Optional<ProgramSource> shadowTranslucentSource = first(programs.getShadowWater(), shadowSource);
|
||||
terrainSource.ifPresent(DeferredWorldRenderingPipeline::submitCeleritasTerrainTransform);
|
||||
translucentSource.ifPresent(DeferredWorldRenderingPipeline::submitCeleritasTerrainTransform);
|
||||
shadowSource.ifPresent(DeferredWorldRenderingPipeline::submitCeleritasTerrainTransform);
|
||||
shadowTranslucentSource.ifPresent(DeferredWorldRenderingPipeline::submitCeleritasTerrainTransform);
|
||||
|
||||
submitSetupComputeTransforms(programs.getSetup(), tm);
|
||||
|
||||
final ComputeSource[] shadowCompute = programs.getShadowCompute();
|
||||
if (shadowCompute != null) {
|
||||
for (final ComputeSource source : shadowCompute) {
|
||||
if (source == null || !source.getSource().isPresent()) continue;
|
||||
final String name = source.getName();
|
||||
final String src = source.getSource().get();
|
||||
Iris.ShaderTransformExecutor.submitTracked(() -> TransformPatcher.patchCompute(name, src, TextureStage.GBUFFERS_AND_SHADOW, tm));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,9 +13,14 @@ import net.coderbot.iris.pipeline.transform.parameter.AttributeParameters;
|
||||
import net.coderbot.iris.pipeline.transform.parameter.Parameters;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.ShaderConstants;
|
||||
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
import org.antlr.v4.runtime.tree.ParseTree;
|
||||
import org.antlr.v4.runtime.tree.TerminalNode;
|
||||
import org.taumc.glsl.ShaderParser;
|
||||
import org.taumc.glsl.Transformer;
|
||||
import org.taumc.glsl.grammar.GLSLLexer;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumMap;
|
||||
@@ -108,58 +113,71 @@ public class ShaderTransformer {
|
||||
return NegotiationResult.error("Shader requires GLSL " + effectiveVersion + " but hardware max is " + maxGlsl);
|
||||
}
|
||||
|
||||
private static Pattern hoistPattern;
|
||||
/** By capturing group: a prefix match's text is not the keyword, so it cannot be looked up by name. */
|
||||
private static int[] hoistGroupVersions;
|
||||
private static VersionRequirement[] enabledRequirements;
|
||||
private static int maxSupportedHoistVersion;
|
||||
|
||||
public static void init() {
|
||||
final StringBuilder patternBuilder = new StringBuilder();
|
||||
final int[] groupVersions = new int[VERSION_REQUIREMENTS.length + 1];
|
||||
int groupCount = 0;
|
||||
enabledRequirements = Arrays.stream(VERSION_REQUIREMENTS)
|
||||
.filter(req -> req.supported.getAsBoolean())
|
||||
.toArray(VersionRequirement[]::new);
|
||||
int maxVersion = 0;
|
||||
|
||||
for (VersionRequirement req : VERSION_REQUIREMENTS) {
|
||||
if (req.supported.getAsBoolean()) {
|
||||
if (!patternBuilder.isEmpty()) patternBuilder.append('|');
|
||||
|
||||
patternBuilder.append("(\\b").append(Pattern.quote(req.keyword)) .append(req.prefix ? "\\w*" : "\\b").append(')');
|
||||
groupVersions[++groupCount] = req.minVersion;
|
||||
maxVersion = Math.max(maxVersion, req.minVersion);
|
||||
}
|
||||
}
|
||||
|
||||
if (!patternBuilder.isEmpty()) {
|
||||
hoistPattern = Pattern.compile(patternBuilder.toString());
|
||||
hoistGroupVersions = Arrays.copyOf(groupVersions, groupCount + 1);
|
||||
for (VersionRequirement req : enabledRequirements) {
|
||||
maxVersion = Math.max(maxVersion, req.minVersion);
|
||||
}
|
||||
maxSupportedHoistVersion = maxVersion;
|
||||
|
||||
Iris.logger.info("Shader version hoisting: {} feature(s) GLSL {}", groupCount, maxVersion > 0 ? maxVersion : "N/A");
|
||||
Iris.logger.info("Shader version hoisting: {} feature(s) GLSL {}", enabledRequirements.length, maxVersion > 0 ? maxVersion : "N/A");
|
||||
}
|
||||
|
||||
private static int getRequiredVersion(String shaderSource, int declaredVersion) {
|
||||
if (hoistPattern == null || declaredVersion >= maxSupportedHoistVersion) {
|
||||
if (enabledRequirements == null || enabledRequirements.length == 0 || declaredVersion >= maxSupportedHoistVersion) {
|
||||
return declaredVersion;
|
||||
}
|
||||
|
||||
final Matcher m = hoistPattern.matcher(shaderSource);
|
||||
final GLSLLexer lexer = new GLSLLexer(CharStreams.fromString(shaderSource));
|
||||
int required = declaredVersion;
|
||||
while (m.find()) {
|
||||
for (int group = 1; group < hoistGroupVersions.length; group++) {
|
||||
if (m.start(group) < 0) continue;
|
||||
if (hoistGroupVersions[group] > required) required = hoistGroupVersions[group];
|
||||
break;
|
||||
}
|
||||
for (Token t = lexer.nextToken(); t.getType() != Token.EOF; t = lexer.nextToken()) {
|
||||
final int channel = t.getChannel();
|
||||
if (channel == GLSLLexer.COMMENTS || channel == Token.HIDDEN_CHANNEL) continue;
|
||||
if (t.getStopIndex() - t.getStartIndex() < 3) continue;
|
||||
required = matchRequirement(t.getText(), required);
|
||||
if (required >= maxSupportedHoistVersion) break;
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
private static int getRequiredVersion(GLSLParser.Translation_unitContext tree, String headerTail, int declaredVersion) {
|
||||
if (enabledRequirements == null || enabledRequirements.length == 0 || declaredVersion >= maxSupportedHoistVersion) {
|
||||
return declaredVersion;
|
||||
}
|
||||
int required = headerTail.isEmpty() ? declaredVersion : getRequiredVersion(headerTail, declaredVersion);
|
||||
if (required >= maxSupportedHoistVersion) return required;
|
||||
return scanTree(tree, required);
|
||||
}
|
||||
|
||||
private static int scanTree(ParseTree tree, int required) {
|
||||
if (tree instanceof TerminalNode terminal) {
|
||||
final String text = terminal.getSymbol().getText();
|
||||
return (text == null || text.length() < 4) ? required : matchRequirement(text, required);
|
||||
}
|
||||
for (int i = 0; i < tree.getChildCount() && required < maxSupportedHoistVersion; i++) {
|
||||
required = scanTree(tree.getChild(i), required);
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
private static int matchRequirement(String text, int required) {
|
||||
for (VersionRequirement req : enabledRequirements) {
|
||||
if (req.minVersion <= required) break;
|
||||
if (req.prefix ? text.startsWith(req.keyword) : text.equals(req.keyword)) return req.minVersion;
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
private record StageHeader(int version, String extensions) {}
|
||||
|
||||
private static String finalizeStage(int version, String headerTail, String body, PatchShaderType stage) {
|
||||
final int required = getRequiredVersion(headerTail + body, version);
|
||||
private static String finalizeStage(int version, String headerTail, String body, GLSLParser.Translation_unitContext tree, PatchShaderType stage) {
|
||||
final int required = getRequiredVersion(tree, headerTail, version);
|
||||
if (required > version) {
|
||||
final NegotiationResult negotiation = negotiateVersion(required, stage);
|
||||
if (negotiation.isError()) {
|
||||
@@ -206,7 +224,13 @@ public class ShaderTransformer {
|
||||
}
|
||||
}
|
||||
|
||||
record StageArtifact(GLSLParser.Translation_unitContext tree, int headerLen) {}
|
||||
|
||||
public static <P extends Parameters> Map<PatchShaderType, String> transform(String vertex, String geometry, String tessControl, String tessEval, String fragment, P parameters) {
|
||||
return transform(vertex, geometry, tessControl, tessEval, fragment, parameters, null);
|
||||
}
|
||||
|
||||
public static <P extends Parameters> Map<PatchShaderType, String> transform(String vertex, String geometry, String tessControl, String tessEval, String fragment, P parameters, EnumMap<PatchShaderType, StageArtifact> artifactsOut) {
|
||||
if (vertex == null && geometry == null && tessControl == null && tessEval == null && fragment == null) {
|
||||
return null;
|
||||
} else {
|
||||
@@ -227,13 +251,14 @@ public class ShaderTransformer {
|
||||
result = shaderTransformationCache.getAndMoveToLast(key);
|
||||
}
|
||||
if(result == null || !useCache) {
|
||||
result = transformInternal(inputs, patchType, parameters);
|
||||
result = transformInternal(inputs, patchType, parameters, artifactsOut);
|
||||
// Clear this, we don't want whatever random type was last transformed being considered for the key
|
||||
parameters.type = null;
|
||||
synchronized (shaderTransformationCache) {
|
||||
// Double-check in case another thread added it while we were transforming
|
||||
Map<PatchShaderType, String> existing = shaderTransformationCache.getAndMoveToLast(key);
|
||||
if (existing != null) {
|
||||
if (artifactsOut != null) artifactsOut.clear();
|
||||
return existing;
|
||||
}
|
||||
if(shaderTransformationCache.size() >= CACHE_SIZE) {
|
||||
@@ -248,6 +273,10 @@ public class ShaderTransformer {
|
||||
}
|
||||
|
||||
public static <P extends Parameters> Map<PatchShaderType, String> transformCompute(String compute, P parameters) {
|
||||
return transformCompute(compute, parameters, null);
|
||||
}
|
||||
|
||||
public static <P extends Parameters> Map<PatchShaderType, String> transformCompute(String compute, P parameters, EnumMap<PatchShaderType, StageArtifact> artifactsOut) {
|
||||
if (compute == null) {
|
||||
return null;
|
||||
} else {
|
||||
@@ -264,13 +293,14 @@ public class ShaderTransformer {
|
||||
result = shaderTransformationCache.getAndMoveToLast(key);
|
||||
}
|
||||
if (result == null || !useCache) {
|
||||
result = transformComputeInternal(compute, patchType, parameters);
|
||||
result = transformComputeInternal(compute, patchType, parameters, artifactsOut);
|
||||
// Clear this, we don't want whatever random type was last transformed being considered for the key
|
||||
parameters.type = null;
|
||||
synchronized (shaderTransformationCache) {
|
||||
// Double-check in case another thread added it while we were transforming
|
||||
final Map<PatchShaderType, String> existing = shaderTransformationCache.getAndMoveToLast(key);
|
||||
if (existing != null) {
|
||||
if (artifactsOut != null) artifactsOut.clear();
|
||||
return existing;
|
||||
}
|
||||
if (shaderTransformationCache.size() >= CACHE_SIZE) {
|
||||
@@ -284,7 +314,7 @@ public class ShaderTransformer {
|
||||
}
|
||||
}
|
||||
|
||||
private static <P extends Parameters> Map<PatchShaderType, String> transformComputeInternal(String compute, Patch patchType, P parameters) {
|
||||
private static <P extends Parameters> Map<PatchShaderType, String> transformComputeInternal(String compute, Patch patchType, P parameters, EnumMap<PatchShaderType, StageArtifact> artifactsOut) {
|
||||
final EnumMap<PatchShaderType, String> result = new EnumMap<>(PatchShaderType.class);
|
||||
|
||||
final Stopwatch watch = Stopwatch.createStarted();
|
||||
@@ -348,19 +378,28 @@ public class ShaderTransformer {
|
||||
|
||||
final String headerTail = extensions.isEmpty() ? "" : "\n" + extensions;
|
||||
final StringBuilder formattedShaderBuilder = new StringBuilder();
|
||||
final GLSLParser.Translation_unitContext[] treeHolder = new GLSLParser.Translation_unitContext[1];
|
||||
|
||||
transformer.mutateTree(tree -> formattedShaderBuilder.append(GlslTransformUtils.getFormattedShader(tree, "")));
|
||||
transformer.mutateTree(tree -> {
|
||||
GlslTransformUtils.restoreReservedWordsInTree(tree);
|
||||
treeHolder[0] = tree;
|
||||
formattedShaderBuilder.append(GlslTransformUtils.getFormattedShaderRebased(tree, ""));
|
||||
});
|
||||
|
||||
final String formattedShader = GlslTransformUtils.restoreReservedWords(finalizeStage(versionInt, headerTail, formattedShaderBuilder.toString(), PatchShaderType.COMPUTE));
|
||||
final String printedBody = formattedShaderBuilder.toString();
|
||||
final String formattedShader = finalizeStage(versionInt, headerTail, printedBody, treeHolder[0], PatchShaderType.COMPUTE);
|
||||
|
||||
result.put(PatchShaderType.COMPUTE, formattedShader);
|
||||
if (artifactsOut != null) {
|
||||
artifactsOut.put(PatchShaderType.COMPUTE, new StageArtifact(treeHolder[0], formattedShader.length() - printedBody.length()));
|
||||
}
|
||||
|
||||
watch.stop();
|
||||
Iris.logger.info("[Load #{}] Transformed compute shader for {} in {}", Iris.getShaderPackLoadId(), patchType.name(), watch);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static <P extends Parameters> Map<PatchShaderType, String> transformInternal(EnumMap<PatchShaderType, String> inputs, Patch patchType, P parameters) {
|
||||
private static <P extends Parameters> Map<PatchShaderType, String> transformInternal(EnumMap<PatchShaderType, String> inputs, Patch patchType, P parameters, EnumMap<PatchShaderType, StageArtifact> artifactsOut) {
|
||||
final EnumMap<PatchShaderType, String> result = new EnumMap<>(PatchShaderType.class);
|
||||
final EnumMap<PatchShaderType, Transformer> types = new EnumMap<>(PatchShaderType.class);
|
||||
final EnumMap<PatchShaderType, StageHeader> prepatched = new EnumMap<>(PatchShaderType.class);
|
||||
@@ -446,12 +485,21 @@ public class ShaderTransformer {
|
||||
}
|
||||
|
||||
final StringBuilder formattedShaderBuilder = new StringBuilder();
|
||||
final GLSLParser.Translation_unitContext[] treeHolder = new GLSLParser.Translation_unitContext[1];
|
||||
|
||||
transformer.mutateTree(tree -> formattedShaderBuilder.append(GlslTransformUtils.getFormattedShader(tree, "")));
|
||||
transformer.mutateTree(tree -> {
|
||||
GlslTransformUtils.restoreReservedWordsInTree(tree);
|
||||
treeHolder[0] = tree;
|
||||
formattedShaderBuilder.append(GlslTransformUtils.getFormattedShaderRebased(tree, ""));
|
||||
});
|
||||
|
||||
final String formattedShader = GlslTransformUtils.restoreReservedWords( finalizeStage(stageHeader.version(), headerTail, formattedShaderBuilder.toString(), shaderType));
|
||||
final String printedBody = formattedShaderBuilder.toString();
|
||||
final String formattedShader = finalizeStage(stageHeader.version(), headerTail, printedBody, treeHolder[0], shaderType);
|
||||
|
||||
result.put(shaderType, formattedShader);
|
||||
if (artifactsOut != null && !(patchType == Patch.CELERITAS_TERRAIN && shaderType == PatchShaderType.VERTEX)) {
|
||||
artifactsOut.put(shaderType, new StageArtifact(treeHolder[0], formattedShader.length() - printedBody.length()));
|
||||
}
|
||||
}
|
||||
maybeExtractRwImageStores(result, patchType);
|
||||
watch.stop();
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.coderbot.iris.pipeline.transform;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.CompatShaderTransformer;
|
||||
import com.gtnewhorizons.angelica.glsm.RenderSystem;
|
||||
import java.util.EnumMap;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.ShaderTransformPostProcessor;
|
||||
@@ -92,8 +93,9 @@ public class TransformPatcher {
|
||||
}
|
||||
|
||||
// if there is no cache result, transform the shaders
|
||||
final EnumMap<PatchShaderType, ShaderTransformer.StageArtifact> artifacts = new EnumMap<>(PatchShaderType.class);
|
||||
if (result == null) {
|
||||
result = ShaderTransformer.transform(vertex, geometry, tessControl, tessEval, fragment, parameters);
|
||||
result = ShaderTransformer.transform(vertex, geometry, tessControl, tessEval, fragment, parameters, artifacts);
|
||||
if (useCache) {
|
||||
synchronized (cache) {
|
||||
// Double-check in case another thread added it while we were transforming
|
||||
@@ -122,7 +124,12 @@ public class TransformPatcher {
|
||||
for (Map.Entry<PatchShaderType, String> entry : result.entrySet()) {
|
||||
final String src = entry.getValue();
|
||||
if (src == null) continue;
|
||||
processor.onTransformed(src, entry.getKey().glShaderType);
|
||||
final ShaderTransformer.StageArtifact art = artifacts.get(entry.getKey());
|
||||
if (art != null) {
|
||||
processor.onTransformed(src, art.tree(), art.headerLen(), entry.getKey().glShaderType);
|
||||
} else {
|
||||
processor.onTransformed(src, entry.getKey().glShaderType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,12 +190,18 @@ public class TransformPatcher {
|
||||
if (compute == null) {
|
||||
return null;
|
||||
}
|
||||
Map<PatchShaderType, String> result = ShaderTransformer.transformCompute(compute, new ComputeParameters(Patch.COMPUTE, stage, textureMap));
|
||||
final EnumMap<PatchShaderType, ShaderTransformer.StageArtifact> artifacts = new EnumMap<>(PatchShaderType.class);
|
||||
Map<PatchShaderType, String> result = ShaderTransformer.transformCompute(compute, new ComputeParameters(Patch.COMPUTE, stage, textureMap), artifacts);
|
||||
final String transformed = result != null ? result.get(PatchShaderType.COMPUTE) : null;
|
||||
if (transformed != null && BackendManager.RENDER_BACKEND.isSDLGPU()) {
|
||||
final ShaderTransformPostProcessor processor = GLSMHooks.postTransformProcessor;
|
||||
if (processor != null) {
|
||||
processor.onTransformed(transformed, PatchShaderType.COMPUTE.glShaderType);
|
||||
final ShaderTransformer.StageArtifact art = artifacts.get(PatchShaderType.COMPUTE);
|
||||
if (art != null) {
|
||||
processor.onTransformed(transformed, art.tree(), art.headerLen(), PatchShaderType.COMPUTE.glShaderType);
|
||||
} else {
|
||||
processor.onTransformed(transformed, PatchShaderType.COMPUTE.glShaderType);
|
||||
}
|
||||
}
|
||||
}
|
||||
return transformed;
|
||||
|
||||
@@ -24,8 +24,11 @@ sodium.options.clouds.name=Clouds
|
||||
sodium.options.clouds.tooltip=Controls whether or not clouds will be visible.
|
||||
sodium.options.gui_scale.tooltip=Sets the maximum scale factor to be used for the user interface. If 'auto' is used, then the largest scale factor will always be used.
|
||||
sodium.options.fullscreen.tooltip=If enabled, the game will display in full-screen (if supported).
|
||||
sodium.options.v_sync.tooltip=Synchronizes frames to your monitor to stop tearing. §lOff§r shows each frame the moment it is ready: lowest latency, but the image can tear, and Max Framerate applies. §lOn§r waits for the monitor, which removes tearing and hands it the frame rate, so §lMax Framerate has no effect§r. §lMailbox§r removes tearing without waiting, keeping most of the latency of Off, and Max Framerate still applies. Mailbox needs the SDL-GPU backend on Vulkan or D3D12.
|
||||
sodium.options.fps_limit.tooltip=Limits the maximum FPS (frames per second). This can help reduce battery usage and general system load when multi-tasking. §lHas no effect while VSync is On§r, because the monitor sets the frame rate instead; use Mailbox to combine a limit with tear-free output.
|
||||
sodium.options.v_sync.tooltip=Off presents each frame immediately: lowest latency, but tearing. On waits for vblank and queues the new frame: no tearing, more latency. Mailbox waits for vblank but replaces the queued frame: no tearing, latency close to Off.
|
||||
sodium.options.vsync_mode.off=Off
|
||||
sodium.options.vsync_mode.on=On
|
||||
sodium.options.vsync_mode.mailbox=Mailbox
|
||||
sodium.options.fps_limit.tooltip=Limits the maximum FPS (frames per second). This can help reduce battery usage and general system load when multi-tasking. Applies in every VSync mode.
|
||||
sodium.options.view_bobbing.tooltip=If enabled, the player's view will sway and bob when moving around. Players who experience motion sickness while playing can benefit from disabling this.
|
||||
sodium.options.attack_indicator.tooltip=Controls where the Attack Indicator is displayed on screen.
|
||||
sodium.options.autosave_indicator.tooltip=If enabled, an indicator will be shown when the game is saving the world to disk.
|
||||
@@ -75,7 +78,7 @@ sodium.options.animate_only_visible_textures.tooltip=If enabled, only animated t
|
||||
sodium.options.translucency_sorting.name=Translucency Sorting
|
||||
sodium.options.translucency_sorting.tooltip=If enabled, translucent blocks will be sorted correctly from back to front to provide better rendering, especially when multiple translucent blocks are in view and layered. This is a bit of an expensive operation.
|
||||
sodium.options.cpu_render_ahead_limit.name=CPU Render-Ahead Limit
|
||||
sodium.options.cpu_render_ahead_limit.tooltip=How many frames the CPU may run ahead of the GPU. Lower values cut input latency, higher values smooth out uneven frame times. Very low or high values may create frame rate instability. The SDL-GPU backend limits this to 3.
|
||||
sodium.options.cpu_render_ahead_limit.tooltip=How many frames the CPU may run ahead of the GPU. Lower values cut input latency, higher values smooth out uneven frame times. Very low or high values may create frame rate instability. 0 disables it; the SDL-GPU backend ignores it and applies its own backpressure.
|
||||
sodium.options.cpu_render_ahead_limit.value=%s frame(s)
|
||||
sodium.options.allow_direct_memory_access.name=Allow Direct Memory Access
|
||||
sodium.options.allow_direct_memory_access.tooltip=If enabled, some critical code paths will be allowed to use direct memory access for performance. This often greatly reduces CPU overhead for chunk and entity rendering, but can make it harder to diagnose some bugs and crashes. You should only disable this if you've been asked to or otherwise know what you're doing.
|
||||
@@ -261,10 +264,6 @@ options.angelica.texteffects.drop_shadow.tooltip=Per-segment drop shadow via %s,
|
||||
options.angelica.texteffects.ampersand_conversion=Ampersand Conversion
|
||||
options.angelica.texteffects.ampersand_conversion.tooltip=Convert %s prefix codes to formatting codes at render time
|
||||
|
||||
sodium.options.vsync_mode.auto=Auto
|
||||
sodium.options.vsync_mode.on=On
|
||||
sodium.options.vsync_mode.mailbox=Mailbox
|
||||
sodium.options.vsync_mode.off=Off
|
||||
sodium.options.multidraw_mode.name=Multi-Draw Mode
|
||||
sodium.options.multidraw_mode.tooltip=Controls how draw commands are dispatched for terrain rendering. §lDirect§r uses glMultiDrawElementsBaseVertex (default, GL 3.2+). §lIndirect§r uses GPU indirect rendering (GL 4.3+), may reduce CPU overhead on some hardware, and is required for GPU / Hi-Z culling. §lIndividual§r issues individual draw calls.
|
||||
sodium.options.multidraw_mode.direct=Direct
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
package com.gtnewhorizons.angelica.mixins.early.angelica;
|
||||
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
|
||||
import com.gtnewhorizons.angelica.proxy.ClientProxy;
|
||||
import net.minecraft.client.settings.GameSettings;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
@Mixin(GameSettings.class)
|
||||
public class MixinGameSettings implements IGameSettingsExt {
|
||||
@Unique
|
||||
private boolean angelica$showFpsGraph = false;
|
||||
|
||||
@Redirect(method = "setOptionValue", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;setVSyncEnabled(Z)V", remap = false))
|
||||
private void angelica$redirectVSync(boolean sync) {
|
||||
ClientProxy.toggleVSync(sync);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean angelica$showFpsGraph() {
|
||||
return this.angelica$showFpsGraph;
|
||||
|
||||
+23
-19
@@ -61,23 +61,42 @@ public abstract class MixinMinecraft {
|
||||
GLStateManager.glEnable(GL11.GL_LIGHTING);
|
||||
}
|
||||
|
||||
@Inject(method = "func_147120_f", at = @At("RETURN"))
|
||||
@Unique
|
||||
private final Runnable angelica$renderAheadWait = () -> {
|
||||
final int limit = ClientProxy.options().performance.cpuRenderAheadLimit;
|
||||
if (limit > 0 && !GLStateManager.hasSwapchainBackpressure()) {
|
||||
celeritas$renderAheadManager.startFrame(limit);
|
||||
}
|
||||
};
|
||||
|
||||
@Inject(method = "runGameLoop", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/EntityRenderer;updateCameraAndRender(F)V"))
|
||||
private void angelica$freshenMouseInput(CallbackInfo ci) {
|
||||
GLStateManager.pumpDisplayMessages();
|
||||
}
|
||||
|
||||
@Inject(method = "runGameLoop", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;func_147120_f()V"))
|
||||
private void celeritas$renderAheadFence(CallbackInfo ci) {
|
||||
if (ClientProxy.options().performance.cpuRenderAheadLimit > 0 && !GLStateManager.hasSwapchainBackpressure()) {
|
||||
celeritas$renderAheadManager.endFrame();
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "runGameLoop", at = @At(value = "INVOKE", target = "Ljava/lang/Thread;yield()V", remap = false))
|
||||
private void angelica$limitFPS(CallbackInfo ci) {
|
||||
if (AngelicaMod.proxy == null) return;
|
||||
|
||||
final int capHz = isFramerateLimitBelowMax() ? getLimitFramerate() : 0;
|
||||
AngelicaMod.proxy.putFrametime(FramePacer.pace(FramePacer.updateCeiling(capHz)));
|
||||
AngelicaMod.proxy.putFrametime(FramePacer.endFrame(capHz, angelica$renderAheadWait));
|
||||
}
|
||||
|
||||
@Redirect(method = {"startGame", "toggleFullscreen"}, at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;setVSyncEnabled(Z)V", remap = false))
|
||||
private void angelica$redirectVSync(boolean sync) {
|
||||
ClientProxy.applyVSyncMode(sync);
|
||||
GLStateManager.setVSyncEnabled(sync);
|
||||
}
|
||||
|
||||
@Inject(method = "startGame", at = @At("RETURN"))
|
||||
private void angelica$markSplashCompleteOnStartGame(CallbackInfo ci) {
|
||||
GLStateManager.markSplashComplete("startGame");
|
||||
GLStateManager.applyRenderAheadLimit(ClientProxy.options().performance.cpuRenderAheadLimit);
|
||||
}
|
||||
|
||||
@WrapWithCondition(method = "runGameLoop", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;sync(I)V", remap = false))
|
||||
@@ -90,21 +109,6 @@ public abstract class MixinMinecraft {
|
||||
((IGameSettingsExt) gameSettings).angelica$setShowFpsGraph(Keyboard.isKeyDown(Keyboard.KEY_LMENU) || Keyboard.isKeyDown(Keyboard.KEY_RMENU));
|
||||
}
|
||||
|
||||
@Inject(method = "runTick", at = @At("HEAD"))
|
||||
private void celeritas$renderAheadStartFrame(CallbackInfo ci) {
|
||||
final int limit = GLStateManager.renderAheadLimit(ClientProxy.options().performance.cpuRenderAheadLimit);
|
||||
if (limit > 0) {
|
||||
celeritas$renderAheadManager.startFrame(limit);
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "runTick", at = @At("RETURN"))
|
||||
private void celeritas$renderAheadEndFrame(CallbackInfo ci) {
|
||||
if (GLStateManager.renderAheadLimit(ClientProxy.options().performance.cpuRenderAheadLimit) > 0) {
|
||||
celeritas$renderAheadManager.endFrame();
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "runGameLoop", at = @At("RETURN"))
|
||||
private void angelica$streamingBufferEndFrame(CallbackInfo ci) {
|
||||
TessellatorStreamingDrawer.endFrame();
|
||||
|
||||
+4
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.mixins.early.angelica.debug;
|
||||
|
||||
import com.gtnewhorizons.angelica.rendering.FramePacer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.settings.GameSettings;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
@@ -16,8 +17,11 @@ public class MixinMinecraft_FPSCap {
|
||||
|
||||
@Shadow public String debug;
|
||||
|
||||
@Shadow public GameSettings gameSettings;
|
||||
|
||||
@Inject(method = "runGameLoop", at = @At(value = "FIELD", target = "Lnet/minecraft/client/Minecraft;debug:Ljava/lang/String;", shift = At.Shift.AFTER, ordinal = 0))
|
||||
private void angelica$appendFPSCapInfo(CallbackInfo ci) {
|
||||
if (!this.gameSettings.showDebugInfo) return;
|
||||
final String indicator = FramePacer.debugIndicator();
|
||||
if (indicator == null) return;
|
||||
this.debug = this.debug.replace(" fps,", " fps" + indicator + ",");
|
||||
|
||||
@@ -5,41 +5,55 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class FramePacerTest {
|
||||
|
||||
@Test
|
||||
void vsyncNeverPaces() {
|
||||
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.ON, 0, 0));
|
||||
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.ON, 60, 144));
|
||||
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.ON, 260, 60));
|
||||
void vsyncStillHonoursTheCap() {
|
||||
assertEquals(60, FramePacer.pacingCeilingHz(VSyncMode.ON, 60, 144, true));
|
||||
assertEquals(30, FramePacer.pacingCeilingHz(VSyncMode.ON, 30, 144, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void vsyncOffHonorsOnlyTheCap() {
|
||||
assertEquals(215, FramePacer.pacingCeilingHz(VSyncMode.OFF, 215, 0));
|
||||
assertEquals(215, FramePacer.pacingCeilingHz(VSyncMode.OFF, 215, 144));
|
||||
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.OFF, 0, 144));
|
||||
void anActiveGateGetsTheHeadroomBackstop() {
|
||||
assertEquals(151, FramePacer.pacingCeilingHz(VSyncMode.ON, 0, 144, true));
|
||||
assertEquals(151, FramePacer.pacingCeilingHz(VSyncMode.ON, 260, 144, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mailboxTakesTheLowerOfCapAndRefresh() {
|
||||
assertEquals(60, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 60, 143));
|
||||
assertEquals(143, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 215, 143));
|
||||
assertEquals(143, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 143, 143));
|
||||
void aQuietGateGetsThePlainRefreshBackstopToAvoidQueueSaturation() {
|
||||
assertEquals(144, FramePacer.pacingCeilingHz(VSyncMode.ON, 0, 144, false));
|
||||
assertEquals(144, FramePacer.pacingCeilingHz(VSyncMode.ON, 260, 144, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mailboxTreatsZeroAsNoLimitFromThatSource() {
|
||||
assertEquals(143, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 0, 143));
|
||||
assertEquals(215, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 215, 0));
|
||||
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 0, 0));
|
||||
void mailboxGetsThePlainRefreshBackstopRegardlessOfTheGate() {
|
||||
assertEquals(60, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 0, 60, false));
|
||||
assertEquals(60, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 0, 60, true));
|
||||
assertEquals(45, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 45, 60, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonPositiveCapIsUncapped() {
|
||||
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.OFF, -1, 0));
|
||||
assertEquals(143, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, -1, 143));
|
||||
void vsyncOffIsBoundedOnlyByTheCap() {
|
||||
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.OFF, 0, 144, false));
|
||||
assertEquals(215, FramePacer.pacingCeilingHz(VSyncMode.OFF, 215, 144, false));
|
||||
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.OFF, -1, 144, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void anUnknownRefreshRateLeavesTearFreeModesUnbounded() {
|
||||
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.ON, 0, 0, true));
|
||||
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.ON, 0, 19, false));
|
||||
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.ON, 0, 1001, true));
|
||||
assertEquals(215, FramePacer.pacingCeilingHz(VSyncMode.ON, 215, 1001, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gateThresholdScalesWithThePeriodAboveAFloor() {
|
||||
assertEquals(100_000L, FramePacer.gateBlockThresholdNanos(1_000_000L));
|
||||
assertEquals(173_611L, FramePacer.gateBlockThresholdNanos(5_555_555L));
|
||||
assertEquals(520_833L, FramePacer.gateBlockThresholdNanos(16_666_666L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -53,45 +67,68 @@ class FramePacerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void implausibleRefreshLeavesMailboxOnTheCapAlone() {
|
||||
assertEquals(215, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 215, 19));
|
||||
assertEquals(215, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 215, 1001));
|
||||
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 0, 1001));
|
||||
void headroomRoundsToTheNearestHz() {
|
||||
assertEquals(21, FramePacer.withHeadroom(20, FramePacer.PACING_HEADROOM_PERCENT));
|
||||
assertEquals(62, FramePacer.withHeadroom(59, FramePacer.PACING_HEADROOM_PERCENT));
|
||||
assertEquals(63, FramePacer.withHeadroom(60, FramePacer.PACING_HEADROOM_PERCENT));
|
||||
assertEquals(151, FramePacer.withHeadroom(144, FramePacer.PACING_HEADROOM_PERCENT));
|
||||
assertEquals(252, FramePacer.withHeadroom(240, FramePacer.PACING_HEADROOM_PERCENT));
|
||||
assertEquals(1050, FramePacer.withHeadroom(1000, FramePacer.PACING_HEADROOM_PERCENT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sleepDeductsWorkAndOverhead() {
|
||||
assertEquals(10_000L, FramePacer.sleepNanosFor(16_000L, 5_000L, 1_000L));
|
||||
void headroomAlwaysClearsTheRefreshRate() {
|
||||
for (int hz = 20; hz <= 1000; hz++) {
|
||||
assertTrue(FramePacer.withHeadroom(hz, FramePacer.PACING_HEADROOM_PERCENT) > hz, "headroom must exceed " + hz + "Hz");
|
||||
assertTrue(FramePacer.withHeadroom(hz, 0) > hz, "a zero-percent headroom must still exceed " + hz + "Hz");
|
||||
}
|
||||
assertEquals(0, FramePacer.withHeadroom(0, FramePacer.PACING_HEADROOM_PERCENT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void anOverrunAsksForNoSleep() {
|
||||
assertEquals(-5_000L, FramePacer.sleepNanosFor(16_000L, 21_000L, 0L));
|
||||
void theDeadlineAdvancesByExactlyOnePeriodDespiteJitter() {
|
||||
final long target = 16_666_666L;
|
||||
long deadline = FramePacer.nextDeadline(0L, 1_000L, target);
|
||||
assertEquals(1_000L + target, deadline);
|
||||
|
||||
final long first = deadline;
|
||||
final long[] wakeJitter = { -400_000L, 900_000L, -120_000L, 250_000L };
|
||||
for (int i = 0; i < wakeJitter.length; i++) {
|
||||
deadline = FramePacer.nextDeadline(deadline, deadline + wakeJitter[i], target);
|
||||
assertEquals(first + target * (i + 1), deadline, "jitter must not accumulate into the deadline");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallingMoreThanTwoPeriodsBehindResynchronises() {
|
||||
final long target = 16_666_666L;
|
||||
final long deadline = 1_000_000_000L;
|
||||
final long lateNow = deadline + target * 3;
|
||||
assertEquals(lateNow + target, FramePacer.nextDeadline(deadline, lateNow, target));
|
||||
}
|
||||
|
||||
@Test
|
||||
void indicatorNamesWhatIsHoldingTheRateDown() {
|
||||
assertEquals(" [vsync 144]", FramePacer.debugIndicator(VSyncMode.ON, 144, 0));
|
||||
assertEquals(" [mailbox 144]", FramePacer.debugIndicator(VSyncMode.MAILBOX, 144, 0));
|
||||
assertEquals(" [mailbox 144, cap 60]", FramePacer.debugIndicator(VSyncMode.MAILBOX, 144, 60));
|
||||
assertEquals(" [cap 60]", FramePacer.debugIndicator(VSyncMode.OFF, 144, 60));
|
||||
assertEquals(" [vsync 144]", FramePacer.debugIndicator(VSyncMode.ON, 144, 0, false));
|
||||
assertEquals(" [vsync 144, cap 60]", FramePacer.debugIndicator(VSyncMode.ON, 144, 60, false));
|
||||
assertEquals(" [cap 215]", FramePacer.debugIndicator(VSyncMode.OFF, 144, 215, false));
|
||||
assertEquals(" [mailbox 144]", FramePacer.debugIndicator(VSyncMode.MAILBOX, 144, 0, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void indicatorOmitsACapThatDoesNothing() {
|
||||
assertEquals(" [vsync 144]", FramePacer.debugIndicator(VSyncMode.ON, 144, 60));
|
||||
assertEquals(" [vsync]", FramePacer.debugIndicator(VSyncMode.ON, 0, 60));
|
||||
void indicatorOmitsACapTheBackstopAlreadyBeats() {
|
||||
assertEquals(" [vsync 144]", FramePacer.debugIndicator(VSyncMode.ON, 144, 260, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void indicatorStillReportsVsyncWithoutARefreshRate() {
|
||||
assertEquals(" [vsync]", FramePacer.debugIndicator(VSyncMode.ON, 0, 0));
|
||||
assertEquals(" [mailbox, cap 60]", FramePacer.debugIndicator(VSyncMode.MAILBOX, 0, 60));
|
||||
assertEquals(" [vsync]", FramePacer.debugIndicator(VSyncMode.ON, 0, 0, false));
|
||||
assertEquals(" [vsync, cap 60]", FramePacer.debugIndicator(VSyncMode.ON, 0, 60, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void indicatorIsAbsentWhenNothingLimits() {
|
||||
assertNull(FramePacer.debugIndicator(VSyncMode.OFF, 144, 0));
|
||||
assertNull(FramePacer.debugIndicator(VSyncMode.OFF, 0, 0));
|
||||
assertNull(FramePacer.debugIndicator(VSyncMode.OFF, 144, 0, false));
|
||||
assertNull(FramePacer.debugIndicator(VSyncMode.OFF, 0, 0, false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.function.LongConsumer;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PacerSleeperTest {
|
||||
|
||||
private static final long MS = 1_000_000L;
|
||||
private static final long SPIN_FLOOR = 100_000L;
|
||||
|
||||
private static final class FakeTime {
|
||||
long now;
|
||||
long lag;
|
||||
long granularity;
|
||||
long nextParkActual = -1;
|
||||
int parks;
|
||||
long lastRequest;
|
||||
|
||||
LongSupplier clock() {
|
||||
return () -> now;
|
||||
}
|
||||
|
||||
LongConsumer parker() {
|
||||
return nanos -> {
|
||||
parks++;
|
||||
lastRequest = nanos;
|
||||
final long actual;
|
||||
if (nextParkActual >= 0) {
|
||||
actual = nextParkActual;
|
||||
nextParkActual = -1;
|
||||
} else {
|
||||
actual = Math.max(nanos + lag, granularity);
|
||||
}
|
||||
now += actual;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static PacerSleeper sleeper(FakeTime time) {
|
||||
return new PacerSleeper(time.clock(), time.parker(), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aLongWaitCostsOneParkNotAChunkPerMillisecond() {
|
||||
final FakeTime time = new FakeTime();
|
||||
final PacerSleeper s = sleeper(time);
|
||||
|
||||
s.sleepUntil(10 * MS, 0L);
|
||||
|
||||
assertEquals(1, time.parks, "the request must be sized to the deadline, not to a fixed chunk");
|
||||
assertEquals(10 * MS - SPIN_FLOOR, time.lastRequest, "the park should land one spin floor short of the deadline");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whatIsLeftForTheSpinNeverExceedsTheFloor() {
|
||||
final FakeTime time = new FakeTime();
|
||||
time.lag = 20_000L;
|
||||
final PacerSleeper s = sleeper(time);
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
final long deadline = time.now + 13 * MS;
|
||||
final long woke = s.sleepUntil(deadline, time.now);
|
||||
assertTrue(deadline - woke <= SPIN_FLOOR,
|
||||
"left " + (deadline - woke) + "ns to spin, floor is " + SPIN_FLOOR);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void theLagEstimateConvergesSoTheDeadlineIsNotOvershot() {
|
||||
final FakeTime time = new FakeTime();
|
||||
time.lag = 300_000L;
|
||||
final PacerSleeper s = sleeper(time);
|
||||
|
||||
for (int i = 0; i < 15; i++) {
|
||||
s.sleepUntil(time.now + 13 * MS, time.now);
|
||||
}
|
||||
assertEquals(300_000L, s.avgOvershootNanos(), "the measured lag should settle on the platform's actual lag");
|
||||
|
||||
final long deadline = time.now + 13 * MS;
|
||||
final long woke = s.sleepUntil(deadline, time.now);
|
||||
assertTrue(woke <= deadline, "a converged estimate must not overshoot: overshot by " + (woke - deadline));
|
||||
}
|
||||
|
||||
@Test
|
||||
void anEarlyWakeReParksInsteadOfSpinningTheRemainder() {
|
||||
final FakeTime time = new FakeTime();
|
||||
final PacerSleeper s = sleeper(time);
|
||||
time.nextParkActual = 2 * MS;
|
||||
|
||||
final long woke = s.sleepUntil(10 * MS, 0L);
|
||||
|
||||
assertEquals(2, time.parks, "a short park must be followed by another, not by a spin");
|
||||
assertTrue(10 * MS - woke <= SPIN_FLOOR, "the second park should still land inside the floor");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aCoarseTimerParksLateRatherThanSpinningOutTheFrame() {
|
||||
final FakeTime time = new FakeTime();
|
||||
time.granularity = 15_600_000L;
|
||||
final PacerSleeper s = sleeper(time);
|
||||
|
||||
for (int i = 0; i < 30; i++) {
|
||||
final long deadline = time.now + 13_889_000L;
|
||||
final long woke = s.sleepUntil(deadline, time.now);
|
||||
|
||||
assertEquals(i + 1, time.parks, "every frame must still park exactly once");
|
||||
assertTrue(woke >= deadline, "frame " + i + " left " + (deadline - woke) + "ns to busy-wait; a coarse timer must never hand the frame to the spin loop");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void aPeriodShorterThanTheWakeUpLagStillParksRatherThanSpinning() {
|
||||
final FakeTime time = new FakeTime();
|
||||
time.granularity = 15_600_000L;
|
||||
final PacerSleeper s = sleeper(time);
|
||||
|
||||
for (int i = 0; i < 30; i++) {
|
||||
final int before = time.parks;
|
||||
final long deadline = time.now + 3 * MS;
|
||||
final long woke = s.sleepUntil(deadline, time.now);
|
||||
|
||||
assertEquals(before + 1, time.parks, "frame " + i + " must park, not fall through to the spin loop");
|
||||
assertTrue(woke >= deadline, "frame " + i + " left " + (deadline - woke) + "ns to busy-wait");
|
||||
}
|
||||
assertTrue(s.avgOvershootNanos() >= 3 * MS, "the estimate should have grown past the period by now");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aOneOffStallDoesNotPoisonTheEstimate() {
|
||||
final FakeTime time = new FakeTime();
|
||||
final PacerSleeper s = sleeper(time);
|
||||
|
||||
time.nextParkActual = 500 * MS;
|
||||
s.sleepUntil(13 * MS, 0L);
|
||||
|
||||
assertEquals(4 * MS / 10, s.avgOvershootNanos(), "a half-second scheduler stall must be clamped, not averaged in whole");
|
||||
}
|
||||
|
||||
@Test
|
||||
void eachParkEvictsExactlyOneSlotFromTheEstimate() {
|
||||
final FakeTime time = new FakeTime();
|
||||
time.lag = MS;
|
||||
final PacerSleeper s = sleeper(time);
|
||||
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
s.sleepUntil(time.now + 13 * MS, time.now);
|
||||
assertEquals(i * MS / 10, s.avgOvershootNanos(), "the running sum must replace the evicted slot, not accumulate");
|
||||
}
|
||||
for (int i = 0; i < 10; i++) {
|
||||
s.sleepUntil(time.now + 13 * MS, time.now);
|
||||
}
|
||||
assertEquals(MS, s.avgOvershootNanos(), "a full window of identical lags averages to that lag");
|
||||
}
|
||||
|
||||
@Test
|
||||
void interruptionAbandonsTheWait() {
|
||||
final FakeTime time = new FakeTime();
|
||||
final PacerSleeper s = sleeper(time);
|
||||
|
||||
Thread.currentThread().interrupt();
|
||||
try {
|
||||
s.sleepUntil(100 * MS, 0L);
|
||||
} finally {
|
||||
assertTrue(Thread.interrupted(), "interrupt flag must survive the bail-out");
|
||||
}
|
||||
assertEquals(0, time.parks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aDeadlineInsideTheFloorParksNotAtAll() {
|
||||
final FakeTime time = new FakeTime();
|
||||
final PacerSleeper s = sleeper(time);
|
||||
|
||||
final long woke = s.sleepUntil(SPIN_FLOOR, 0L);
|
||||
|
||||
assertEquals(0, time.parks);
|
||||
assertEquals(0L, woke);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,695 @@
|
||||
package com.mitchej123.lwjgl;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.nio.Buffer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
|
||||
/**
|
||||
* Headless {@link LWJGLService} for unit tests.
|
||||
*/
|
||||
public final class HeadlessTestLWJGLService extends LWJGLService {
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return -1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpenGLVersionSupported(int major, int minor) {
|
||||
throw new UnsupportedOperationException("isOpenGLVersionSupported requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExtensionSupported(GLExtension extension) {
|
||||
throw new UnsupportedOperationException("isExtensionSupported requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPointerSize() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGenBuffers() {
|
||||
throw new UnsupportedOperationException("glGenBuffers requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDeleteBuffers(int buffer) {
|
||||
throw new UnsupportedOperationException("glDeleteBuffers requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBindBuffer(int target, int buffer) {
|
||||
throw new UnsupportedOperationException("glBindBuffer requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBufferData(int target, long size, int usage) {
|
||||
throw new UnsupportedOperationException("glBufferData requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBufferData(int target, ByteBuffer data, int usage) {
|
||||
throw new UnsupportedOperationException("glBufferData requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBufferData(int target, long size, long data, int usage) {
|
||||
throw new UnsupportedOperationException("glBufferData requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBufferSubData(int target, long offset, ByteBuffer data) {
|
||||
throw new UnsupportedOperationException("glBufferSubData requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBufferSubData(int target, long offset, long size, long data) {
|
||||
throw new UnsupportedOperationException("glBufferSubData requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBufferStorage(int target, long size, int flags) {
|
||||
throw new UnsupportedOperationException("glBufferStorage requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer glMapBufferRange(int target, long offset, long length, int flags) {
|
||||
throw new UnsupportedOperationException("glMapBufferRange requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nglMapBuffer(int target, int access) {
|
||||
throw new UnsupportedOperationException("nglMapBuffer requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer glMapBuffer(int target, int access) {
|
||||
throw new UnsupportedOperationException("glMapBuffer requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUnmapBuffer(int target) {
|
||||
throw new UnsupportedOperationException("glUnmapBuffer requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glFlushMappedBufferRange(int target, long offset, long length) {
|
||||
throw new UnsupportedOperationException("glFlushMappedBufferRange requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glCopyBufferSubData(int readTarget, int writeTarget, long readOffset, long writeOffset, long size) {
|
||||
throw new UnsupportedOperationException("glCopyBufferSubData requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBindBufferBase(int target, int index, int buffer) {
|
||||
throw new UnsupportedOperationException("glBindBufferBase requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGenVertexArrays() {
|
||||
throw new UnsupportedOperationException("glGenVertexArrays requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDeleteVertexArrays(int array) {
|
||||
throw new UnsupportedOperationException("glDeleteVertexArrays requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBindVertexArray(int array) {
|
||||
throw new UnsupportedOperationException("glBindVertexArray requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, long pointer) {
|
||||
throw new UnsupportedOperationException("glVertexAttribPointer requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glVertexAttribIPointer(int index, int size, int type, int stride, long pointer) {
|
||||
throw new UnsupportedOperationException("glVertexAttribIPointer requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glEnableVertexAttribArray(int index) {
|
||||
throw new UnsupportedOperationException("glEnableVertexAttribArray requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glVertexAttribDivisor(int index, int divisor) {
|
||||
throw new UnsupportedOperationException("glVertexAttribDivisor requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glCreateShader(int type) {
|
||||
throw new UnsupportedOperationException("glCreateShader requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glShaderSource(int shader, CharSequence source) {
|
||||
throw new UnsupportedOperationException("glShaderSource requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glShaderSourceSafe(int shader, CharSequence source) {
|
||||
throw new UnsupportedOperationException("glShaderSourceSafe requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glCompileShader(int shader) {
|
||||
throw new UnsupportedOperationException("glCompileShader requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String glGetShaderInfoLog(int shader, int maxLength) {
|
||||
throw new UnsupportedOperationException("glGetShaderInfoLog requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGetShaderi(int shader, int pname) {
|
||||
throw new UnsupportedOperationException("glGetShaderi requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDeleteShader(int shader) {
|
||||
throw new UnsupportedOperationException("glDeleteShader requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glCreateProgram() {
|
||||
throw new UnsupportedOperationException("glCreateProgram requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glAttachShader(int program, int shader) {
|
||||
throw new UnsupportedOperationException("glAttachShader requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glLinkProgram(int program) {
|
||||
throw new UnsupportedOperationException("glLinkProgram requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String glGetProgramInfoLog(int program, int maxLength) {
|
||||
throw new UnsupportedOperationException("glGetProgramInfoLog requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGetProgrami(int program, int pname) {
|
||||
throw new UnsupportedOperationException("glGetProgrami requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUseProgram(int program) {
|
||||
throw new UnsupportedOperationException("glUseProgram requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDeleteProgram(int program) {
|
||||
throw new UnsupportedOperationException("glDeleteProgram requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBindAttribLocation(int program, int index, CharSequence name) {
|
||||
throw new UnsupportedOperationException("glBindAttribLocation requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBindFragDataLocation(int program, int colorNumber, CharSequence name) {
|
||||
throw new UnsupportedOperationException("glBindFragDataLocation requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGetUniformLocation(int program, CharSequence name) {
|
||||
throw new UnsupportedOperationException("glGetUniformLocation requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGetUniformBlockIndex(int program, CharSequence name) {
|
||||
throw new UnsupportedOperationException("glGetUniformBlockIndex requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUniformBlockBinding(int program, int blockIndex, int blockBinding) {
|
||||
throw new UnsupportedOperationException("glUniformBlockBinding requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUniform1f(int location, float v0) {
|
||||
throw new UnsupportedOperationException("glUniform1f requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUniform1i(int location, int v0) {
|
||||
throw new UnsupportedOperationException("glUniform1i requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUniform1fv(int location, FloatBuffer value) {
|
||||
throw new UnsupportedOperationException("glUniform1fv requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUniform2i(int location, int v0, int v1) {
|
||||
throw new UnsupportedOperationException("glUniform2i requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUniform3f(int location, float v0, float v1, float v2) {
|
||||
throw new UnsupportedOperationException("glUniform3f requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUniform3fv(int location, FloatBuffer value) {
|
||||
throw new UnsupportedOperationException("glUniform3fv requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUniform3fv(int location, float[] value) {
|
||||
throw new UnsupportedOperationException("glUniform3fv requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUniform4fv(int location, FloatBuffer value) {
|
||||
throw new UnsupportedOperationException("glUniform4fv requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUniform4fv(int location, float[] value) {
|
||||
throw new UnsupportedOperationException("glUniform4fv requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUniformMatrix3fv(int location, boolean transpose, FloatBuffer value) {
|
||||
throw new UnsupportedOperationException("glUniformMatrix3fv requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glUniformMatrix4fv(int location, boolean transpose, FloatBuffer value) {
|
||||
throw new UnsupportedOperationException("glUniformMatrix4fv requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDrawElementsBaseVertex(int mode, int count, int type, long indices, int basevertex) {
|
||||
throw new UnsupportedOperationException("glDrawElementsBaseVertex requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glMultiDrawElementsBaseVertex(int mode, long pCount, int type, long pIndices, int drawcount, long pBaseVertex) {
|
||||
throw new UnsupportedOperationException("glMultiDrawElementsBaseVertex requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glMultiDrawElementsIndirect(int mode, int type, long indirect, int drawcount, int stride) {
|
||||
throw new UnsupportedOperationException("glMultiDrawElementsIndirect requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long glFenceSync(int condition, int flags) {
|
||||
throw new UnsupportedOperationException("glFenceSync requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glClientWaitSync(long sync, int flags, long timeout) {
|
||||
throw new UnsupportedOperationException("glClientWaitSync requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGetSynci(long sync, int pname, IntBuffer length) {
|
||||
throw new UnsupportedOperationException("glGetSynci requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glWaitSync(long sync, int flags, long timeout) {
|
||||
throw new UnsupportedOperationException("glWaitSync requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDeleteSync(long sync) {
|
||||
throw new UnsupportedOperationException("glDeleteSync requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGenQueries() {
|
||||
throw new UnsupportedOperationException("glGenQueries requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDeleteQueries(int query) {
|
||||
throw new UnsupportedOperationException("glDeleteQueries requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBeginQuery(int target, int id) {
|
||||
throw new UnsupportedOperationException("glBeginQuery requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glEndQuery(int target) {
|
||||
throw new UnsupportedOperationException("glEndQuery requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long glGetQueryObjectui64(int id, int pname) {
|
||||
throw new UnsupportedOperationException("glGetQueryObjectui64 requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int setupDebugCallback(DebugMessageHandler handler) {
|
||||
throw new UnsupportedOperationException("setupDebugCallback requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableDebugCallback() {
|
||||
throw new UnsupportedOperationException("disableDebugCallback requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glObjectLabel(int identifier, int name, CharSequence label) {
|
||||
throw new UnsupportedOperationException("glObjectLabel requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glPushDebugGroup(int source, int id, CharSequence message) {
|
||||
throw new UnsupportedOperationException("glPushDebugGroup requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glPopDebugGroup() {
|
||||
throw new UnsupportedOperationException("glPopDebugGroup requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGenTextures() {
|
||||
throw new UnsupportedOperationException("glGenTextures requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glGenTextures(int[] textures) {
|
||||
throw new UnsupportedOperationException("glGenTextures requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDeleteTextures(int texture) {
|
||||
throw new UnsupportedOperationException("glDeleteTextures requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDeleteTextures(int[] textures) {
|
||||
throw new UnsupportedOperationException("glDeleteTextures requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBindTexture(int target, int texture) {
|
||||
throw new UnsupportedOperationException("glBindTexture requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glActiveTexture(int texture) {
|
||||
throw new UnsupportedOperationException("glActiveTexture requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGetTexLevelParameteri(int target, int level, int pname) {
|
||||
throw new UnsupportedOperationException("glGetTexLevelParameteri requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glCopyTexSubImage2D(int target, int level, int xoffset, int yoffset, int x, int y, int width, int height) {
|
||||
throw new UnsupportedOperationException("glCopyTexSubImage2D requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glPixelStorei(int pname, int param) {
|
||||
throw new UnsupportedOperationException("glPixelStorei requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGenFramebuffers() {
|
||||
throw new UnsupportedOperationException("glGenFramebuffers requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDeleteFramebuffers(int framebuffer) {
|
||||
throw new UnsupportedOperationException("glDeleteFramebuffers requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBindFramebuffer(int target, int framebuffer) {
|
||||
throw new UnsupportedOperationException("glBindFramebuffer requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glCheckFramebufferStatus(int target) {
|
||||
throw new UnsupportedOperationException("glCheckFramebufferStatus requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glFramebufferTexture2D(int target, int attachment, int textarget, int texture, int level) {
|
||||
throw new UnsupportedOperationException("glFramebufferTexture2D requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glEnable(int cap) {
|
||||
throw new UnsupportedOperationException("glEnable requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDisable(int cap) {
|
||||
throw new UnsupportedOperationException("glDisable requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBlendFunc(int sfactor, int dfactor) {
|
||||
throw new UnsupportedOperationException("glBlendFunc requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glBlendFuncSeparate(int srcRGB, int dstRGB, int srcAlpha, int dstAlpha) {
|
||||
throw new UnsupportedOperationException("glBlendFuncSeparate requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDepthFunc(int func) {
|
||||
throw new UnsupportedOperationException("glDepthFunc requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDepthMask(boolean flag) {
|
||||
throw new UnsupportedOperationException("glDepthMask requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glColorMask(boolean red, boolean green, boolean blue, boolean alpha) {
|
||||
throw new UnsupportedOperationException("glColorMask requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glViewport(int x, int y, int width, int height) {
|
||||
throw new UnsupportedOperationException("glViewport requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glClear(int mask) {
|
||||
throw new UnsupportedOperationException("glClear requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glClearColor(float red, float green, float blue, float alpha) {
|
||||
throw new UnsupportedOperationException("glClearColor requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGetError() {
|
||||
throw new UnsupportedOperationException("glGetError requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glMatrixMode(int mode) {
|
||||
throw new UnsupportedOperationException("glMatrixMode requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glLoadMatrixf(FloatBuffer m) {
|
||||
throw new UnsupportedOperationException("glLoadMatrixf requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGetInteger(int pname) {
|
||||
throw new UnsupportedOperationException("glGetInteger requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glGetIntegerv(int pname, int[] params) {
|
||||
throw new UnsupportedOperationException("glGetIntegerv requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean glGetBoolean(int pname) {
|
||||
throw new UnsupportedOperationException("glGetBoolean requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String glGetString(int pname) {
|
||||
throw new UnsupportedOperationException("glGetString requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int glGetAttribLocation(int program, CharSequence name) {
|
||||
throw new UnsupportedOperationException("glGetAttribLocation requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MemoryStack stackPush() {
|
||||
throw new UnsupportedOperationException("stackPush requires a GL context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nmemAlloc(long size) {
|
||||
return MemoryUtilities.nmemAlloc(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nmemCalloc(long count, long size) {
|
||||
return MemoryUtilities.nmemCalloc(count, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nmemAlignedAlloc(long alignment, long size) {
|
||||
throw new UnsupportedOperationException("nmemAlignedAlloc is not provided by GTNHLib MemoryUtilities");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nmemRealloc(long ptr, long size) {
|
||||
return MemoryUtilities.nmemRealloc(ptr, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nmemFree(long ptr) {
|
||||
MemoryUtilities.nmemFree(ptr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nmemAlignedFree(long ptr) {
|
||||
throw new UnsupportedOperationException("nmemAlignedFree is not provided by GTNHLib MemoryUtilities");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer memAlloc(int size) {
|
||||
return MemoryUtilities.memAlloc(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer memCalloc(int size) {
|
||||
return MemoryUtilities.memCalloc(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer memRealloc(ByteBuffer buffer, int size) {
|
||||
return MemoryUtilities.memRealloc(buffer, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void memFree(Buffer buffer) {
|
||||
MemoryUtilities.memFree(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer memByteBuffer(long address, int capacity) {
|
||||
return MemoryUtilities.memByteBuffer(address, capacity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long memAddress(Buffer buffer) {
|
||||
return MemoryUtilities.memAddress(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long memAddress(Buffer buffer, int position) {
|
||||
throw new UnsupportedOperationException("memAddress(Buffer, int) has no generic MemoryUtil overload");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void memSet(long address, int value, long bytes) {
|
||||
MemoryUtilities.memSet(address, value, bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void memCopy(long src, long dst, long bytes) {
|
||||
MemoryUtilities.memCopy(src, dst, bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void memPutByte(long address, byte value) {
|
||||
MemoryUtilities.memPutByte(address, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void memPutShort(long address, short value) {
|
||||
MemoryUtilities.memPutShort(address, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void memPutInt(long address, int value) {
|
||||
MemoryUtilities.memPutInt(address, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void memPutFloat(long address, float value) {
|
||||
MemoryUtilities.memPutFloat(address, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void memPutLong(long address, long value) {
|
||||
MemoryUtilities.memPutLong(address, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void memPutAddress(long address, long value) {
|
||||
MemoryUtilities.memPutAddress(address, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte memGetByte(long address) {
|
||||
return MemoryUtilities.memGetByte(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public short memGetShort(long address) {
|
||||
return MemoryUtilities.memGetShort(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int memGetInt(long address) {
|
||||
return MemoryUtilities.memGetInt(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float memGetFloat(long address) {
|
||||
return MemoryUtilities.memGetFloat(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long memGetLong(long address) {
|
||||
return MemoryUtilities.memGetLong(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long memGetAddress(long address) {
|
||||
return MemoryUtilities.memGetAddress(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer memSlice(ByteBuffer buffer, int offset, int capacity) {
|
||||
return MemoryUtilities.memSlice(buffer, offset, capacity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
com.mitchej123.lwjgl.HeadlessTestLWJGLService
|
||||
Reference in new issue
Block a user