Compare commits

...
12 Commits
123 changed files with 4271 additions and 776 deletions

No files matched your search

+5 -2
View File
@@ -26,7 +26,7 @@ jobs:
- name: Install a software Vulkan driver
run: |
sudo apt-get update -y
sudo apt-get install -y xvfb mesa-vulkan-drivers
sudo apt-get install -y xvfb mesa-vulkan-drivers libgl1-mesa-dri
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
with:
@@ -36,7 +36,7 @@ jobs:
25
- uses: gradle/actions/setup-gradle@v5
- name: Run SDL-GPU tests
run: xvfb-run -a --server-args="-screen 0 1366x768x24" ./gradlew --build-cache --stacktrace --continue :sdl-gpu:glsmSdlTest :sdl-gpu:sdlGpuDeviceTest
run: xvfb-run -a --server-args="-screen 0 1366x768x24" ./gradlew --build-cache --stacktrace --continue :sdl-gpu:glsmSdlTest :sdl-gpu:sdlGpuDeviceTest :lwjgl3-backend:glesTest
- name: Check LWJGL did not extract natives to the shared temp dir
run: test ! -e "/tmp/lwjgl_$USER"
- name: Attach SDL-GPU test results
@@ -48,3 +48,6 @@ jobs:
sdl-gpu/build/test-results/
sdl-gpu/build/reports/tests/
sdl-gpu/hs_err_pid*.log
lwjgl3-backend/build/test-results/
lwjgl3-backend/build/reports/tests/
lwjgl3-backend/hs_err_pid*.log
+17
View File
@@ -67,6 +67,22 @@ artifacts {
}
}
// LWJGL2-API stubs for test JVMs that strip the lwjgl-2 jars (glsmSdlTest, glesTest).
val lwjgl2StubsSourceSet = sourceSets.create("lwjgl2Stubs") {
java.srcDir("src/lwjgl2Stubs/java")
}
val lwjgl2Stubs by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
}
artifacts {
add(lwjgl2Stubs.name, lwjgl2StubsSourceSet.java.classesDirectory) {
builtBy(tasks.named("lwjgl2StubsClasses"))
}
}
repositories {
maven {
name = "Mojang"
@@ -111,6 +127,7 @@ val spirvTestRuntime by configurations.creating {
dependencies {
// MC compile stubs
compileOnly(sourceSets["stubs"].output)
"lwjgl2StubsCompileOnly"(libs.lwjgl3.opengl)
// LWJGL
compileOnly(libs.lwjgl2)
@@ -1,5 +1,7 @@
package org.lwjgl.opengl;
import java.lang.reflect.Field;
public class ContextCapabilities {
public boolean OpenGL30 = true;
@@ -21,11 +23,28 @@ public class ContextCapabilities {
public boolean GL_ARB_sampler_objects = true;
public boolean GL_ARB_shader_image_load_store = true;
public boolean GL_ARB_shader_storage_buffer_object = true;
public boolean GL_ARB_stencil_texturing = false;
public boolean GL_ARB_tessellation_shader = false;
public boolean GL_ARB_texture_storage = false;
public boolean GL_EXT_direct_state_access = false;
public boolean GL_EXT_gpu_shader4 = true;
public boolean GL_EXT_shader_image_load_store = true;
public boolean GL_NVX_gpu_memory_info = false;
public ContextCapabilities() {
if ("lwjgl3".equals(System.getProperty("glsm.stubs.capabilities"))) copyFromCurrentContext();
}
private void copyFromCurrentContext() {
final GLCapabilities caps = GL.getCapabilities();
for (Field field : ContextCapabilities.class.getFields()) {
try {
field.setBoolean(this, GLCapabilities.class.getField(field.getName()).getBoolean(caps));
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("LWJGL3 GLCapabilities has no " + field.getName(), e);
}
}
}
}
@@ -0,0 +1,13 @@
package org.lwjgl.opengl;
public final class GLContext {
private static ContextCapabilities capabilities;
private GLContext() {}
public static ContextCapabilities getCapabilities() {
if (capabilities == null) capabilities = new ContextCapabilities();
return capabilities;
}
}
@@ -21,6 +21,7 @@ public final class SystemProperties {
public static final String GL_PROFILE = System.getProperty("angelica.glProfile", "");
public static final boolean DISABLE_NVIDIA_WORKAROUND = Boolean.getBoolean("angelica.disableNvidiaWorkaround");
public static final boolean DISABLE_LTW_WORKAROUND = Boolean.getBoolean("angelica.disableLtwWorkaround");
public static final boolean EMULATE_CLIP_DISTANCES = Boolean.getBoolean("angelica.emulateClipDistances");
// Redirector
public static final UnmappedGLMode UNMAPPED_GL = parseEnum("angelica.unmappedGL", UnmappedGLMode.WARN, UnmappedGLMode.WARN, UnmappedGLMode.class);
@@ -45,6 +46,8 @@ public final class SystemProperties {
public static final Boolean TRACY_FINE_ZONES_OVERRIDE = parseBooleanOverride("angelica.tracy.fineZones");
public static final String TRACY_DIR = System.getProperty("angelica.tracy.dir", "angelica" + File.separator + "natives" + File.separator + "tracy");
public static final Integer TRACY_MAX_SRC_LOCS_OVERRIDE = Integer.getInteger("angelica.tracy.maxSrcLocs");
public static final Boolean TRACY_FRAME_IMAGES_OVERRIDE = parseBooleanOverride("angelica.tracy.frameImages");
public static final Integer TRACY_FRAME_IMAGE_RATE_OVERRIDE = Integer.getInteger("angelica.tracy.frameImageRate");
// Profiling
public static final String PROFILE_OPTS = System.getProperty("angelica.profile.opts", "event=wall,interval=5ms,alloc=512k,lock=10ms");
@@ -25,11 +25,13 @@ public final class CaptureGate {
public static final boolean FORCED = SystemProperties.DEBUG_MARKERS || SystemProperties.LWJGL_DEBUG;
public static final boolean TOOL_ATTACHED = detectCaptureTool();
public static boolean markersThisFrame = true;
public static boolean tracyConnected;
private CaptureGate() {}
public static void refresh() {
markersThisFrame = gateValue(FORCED, TOOL_ATTACHED, Tracy.isConnected());
tracyConnected = Tracy.isConnected();
markersThisFrame = gateValue(FORCED, TOOL_ATTACHED, tracyConnected);
}
static boolean gateValue(boolean forced, boolean toolAttached, boolean tracyConnected) {
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.stacks.BooleanStateStack;
public final class ClipPlaneBooleanState extends BooleanStateStack {
@@ -13,12 +14,22 @@ public final class ClipPlaneBooleanState extends BooleanStateStack {
this.index = index;
}
@Override
protected boolean forwardsToBackend() {
return !RenderSystem.isClipDistanceEmulated();
}
@Override
protected void onEnabledChanged() {
final int before = owner.clipPlaneEnabledMask;
if (isEnabled()) {
owner.clipPlaneEnabledMask |= (1 << index);
} else {
owner.clipPlaneEnabledMask &= ~(1 << index);
}
owner.clipPlaneGeneration++;
if ((before == 0) != (owner.clipPlaneEnabledMask == 0) && RenderSystem.isClipDistanceEmulated() && GLSMHooks.CLIP_PLANES_IN_USE.hasListeners()) {
GLSMHooks.CLIP_PLANES_IN_USE.post(GLSMHooks.clipPlanesInUseEvent);
}
}
}
@@ -249,7 +249,7 @@ public class CompatUniformManager {
final int cpGen = glCtx.clipPlaneGeneration;
if (cpGen != pu.lastClipPlaneGen) {
pu.lastClipPlaneGen = cpGen;
uploadClipPlanes(locs);
uploadClipPlanes(locs, glCtx.clipPlaneEnabledMask);
}
}
@@ -468,16 +468,14 @@ public class CompatUniformManager {
RENDER_BACKEND.uniform1f(locs[LOC_MAT_BASE + MF_SHININESS], mat.shininess);
}
private static void uploadClipPlanes(int[] locs) {
private static void uploadClipPlanes(int[] locs, int enabledMask) {
if (locs[LOC_CLIP_PLANES_ENABLED] != -1) {
RENDER_BACKEND.uniform1i(locs[LOC_CLIP_PLANES_ENABLED], GLSMRasterizer.anyClipPlaneEnabled() ? 1 : 0);
RENDER_BACKEND.uniform1i(locs[LOC_CLIP_PLANES_ENABLED], enabledMask != 0 ? 1 : 0);
}
if (locs[LOC_CLIP_PLANES] != -1) {
final ClipPlaneState cps = GLSMRasterizer.getClipPlaneState();
clipPlaneBuf.clear();
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
cps.putEyePlane(i, clipPlaneBuf);
}
cps.putEffectivePlanes(enabledMask, clipPlaneBuf);
clipPlaneBuf.flip();
RENDER_BACKEND.uniform4(locs[LOC_CLIP_PLANES], clipPlaneBuf);
}
@@ -1,7 +1,7 @@
package com.gtnewhorizons.angelica.glsm;
/*
* Ported from Mesa's util_idalloc (src/util/u_idalloc.c).
* Ported from Mesa's util_idalloc and util_idalloc_sparse (src/util/u_idalloc.c).
*
* Original copyright and license:
*
@@ -32,17 +32,15 @@ package com.gtnewhorizons.angelica.glsm;
*/
/**
* Bitmap ID allocator for display list IDs.
*
* <p>Bitmap-based: each bit in the {@code data} array represents one ID.
* Bit set = allocated, bit clear = free. ID 0 is reserved (never returned).
* Sparse bitmap ID allocator for display list IDs; ID 0 is reserved (never returned).
*/
public class DisplayListIDAllocator {
private int[] data;
private int numElements; // number of uint32 words allocated
private int numSetElements; // highest word index with any bit set + 1
private int lowestFreeIdx; // optimization: skip fully-allocated words
private static final int SEGMENT_SHIFT = 22;
private static final int SEGMENT_MASK = (1 << SEGMENT_SHIFT) - 1;
private static final int WORDS_PER_SEGMENT = (1 << SEGMENT_SHIFT) / 32;
private final Segment[] segments = new Segment[512];
public DisplayListIDAllocator() {
this(32); // 1024 IDs initially
@@ -50,138 +48,191 @@ public class DisplayListIDAllocator {
public DisplayListIDAllocator(int initialWords) {
if (initialWords < 1) initialWords = 1;
this.data = new int[initialWords];
this.numElements = initialWords;
this.numSetElements = 0;
this.lowestFreeIdx = 0;
final Segment first = new Segment(initialWords);
// Reserve ID 0 (never returned), matching Mesa's skip_zero pattern
data[0] = 1;
numSetElements = 1;
first.data[0] = 1;
first.numSetElements = 1;
segments[0] = first;
}
private Segment segment(int index) {
Segment s = segments[index];
if (s == null) {
s = new Segment(1);
segments[index] = s;
}
return s;
}
public synchronized int allocRange(int count) {
if (count <= 0) throw new IllegalArgumentException("count must be > 0");
if (count == 1) return allocSingle();
return allocMultiple(count);
}
private int allocSingle() {
for (int i = lowestFreeIdx; i < numElements; i++) {
if (data[i] == 0xFFFFFFFF) continue;
int bit = Integer.numberOfTrailingZeros(~data[i]);
data[i] |= 1 << bit;
lowestFreeIdx = i;
numSetElements = Math.max(numSetElements, i + 1);
return i * 32 + bit;
}
// No slots available, grow. Record old end, then mark first bit there.
int oldNumElements = numElements;
resize(Math.max(numElements, 1) * 2);
lowestFreeIdx = oldNumElements;
data[oldNumElements] |= 1;
numSetElements = Math.max(numSetElements, oldNumElements + 1);
return oldNumElements * 32;
}
private int allocMultiple(int num) {
int numWords = divRoundUp(num, 32);
int base = findFreeBlock(lowestFreeIdx);
boolean found = false;
while (!found) {
int i;
for (i = base; i < numElements && i - base < numWords && data[i] == 0; i++)
;
if (i - base == numWords) {
found = true;
} else if (i == numElements) {
// Hit end — base is valid: data[base..end] are zero, resize extends the run into newly zeroed words.
resize(numElements * 2 + numWords);
found = true;
} else {
// Non-zero word at i, skip past it
base = i + 1;
if (count == 1) {
for (int i = 0; i < segments.length; i++) {
final Segment s = segment(i);
if (s.lowestFreeIdx < WORDS_PER_SEGMENT) return (i << SEGMENT_SHIFT) | s.allocSingle();
}
throw new IllegalStateException("all display list IDs are in use");
}
// Mark bits as used
int fullWords = num / 32;
int remainder = num % 32;
for (int i = base; i < base + fullWords; i++) {
data[i] = 0xFFFFFFFF;
final int numWords = Segment.divRoundUp(count, 32);
for (int i = 0; i < segments.length; i++) {
final Segment s = segment(i);
if (s.lowestFreeIdx + numWords > WORDS_PER_SEGMENT) continue;
final int base = s.allocMultiple(count);
if (base + count <= 1 << SEGMENT_SHIFT) return (i << SEGMENT_SHIFT) | base;
for (int j = 0; j < count; j++) s.free(base + j);
}
if (remainder != 0) {
data[base + fullWords] |= (1 << remainder) - 1;
}
if (lowestFreeIdx == base) {
lowestFreeIdx = base + num / 32;
}
numSetElements = Math.max(numSetElements, base + numWords);
return base * 32;
throw new IllegalStateException("no free consecutive range of " + count + " display list IDs");
}
private int findFreeBlock(int start) {
for (int i = start; i < numElements; i++) {
if (data[i] == 0) return i;
}
return numElements;
public synchronized void reserve(int id) {
if (id <= 0) return;
segment(id >>> SEGMENT_SHIFT).reserve(id & SEGMENT_MASK);
}
public synchronized void free(int id) {
if (id <= 0) return; // ID 0 is reserved
int idx = id / 32;
if (idx >= numElements) return;
lowestFreeIdx = Math.min(idx, lowestFreeIdx);
data[idx] &= ~(1 << (id % 32));
if (numSetElements == idx + 1) {
while (numSetElements > 0 && data[numSetElements - 1] == 0) {
numSetElements--;
}
}
final Segment s = segments[id >>> SEGMENT_SHIFT];
if (s != null) s.free(id & SEGMENT_MASK);
}
public synchronized void freeRange(int base, int count) {
if (count <= 0 || base <= 0) return;
int startIdx = base / 32;
if (startIdx >= numElements) return;
int end = base + count;
for (int id = base; id < end; id++) {
int idx = id / 32;
if (idx >= numElements) break;
data[idx] &= ~(1 << (id % 32));
}
lowestFreeIdx = Math.min(startIdx, lowestFreeIdx);
while (numSetElements > 0 && data[numSetElements - 1] == 0) {
numSetElements--;
final long last = Math.min((long) base + count - 1, Integer.MAX_VALUE);
long id = base;
while (id <= last) {
final int index = (int) (id >>> SEGMENT_SHIFT);
final Segment s = segments[index];
if (s == null || (id & SEGMENT_MASK) >= (long) s.numElements * 32) {
id = (long) (index + 1) << SEGMENT_SHIFT;
continue;
}
s.free((int) id & SEGMENT_MASK);
id++;
}
}
public synchronized boolean isAllocated(int id) {
if (id <= 0) return false;
int idx = id / 32;
if (idx >= numSetElements) return false;
return (data[idx] & (1 << (id % 32))) != 0;
final Segment s = segments[id >>> SEGMENT_SHIFT];
return s != null && s.isAllocated(id & SEGMENT_MASK);
}
private void resize(int newNumElements) {
if (newNumElements <= numElements) return;
int[] newData = new int[newNumElements];
System.arraycopy(data, 0, newData, 0, numElements);
data = newData;
numElements = newNumElements;
}
private static final class Segment {
private int[] data;
private int numElements; // number of uint32 words allocated
private int numSetElements; // highest word index with any bit set + 1
private int lowestFreeIdx; // optimization: skip fully-allocated words
private static int divRoundUp(int a, int b) {
return (a + b - 1) / b;
Segment(int initialWords) {
this.data = new int[initialWords];
this.numElements = initialWords;
}
int allocSingle() {
for (int i = lowestFreeIdx; i < numElements; i++) {
if (data[i] == 0xFFFFFFFF) continue;
int bit = Integer.numberOfTrailingZeros(~data[i]);
data[i] |= 1 << bit;
lowestFreeIdx = i;
numSetElements = Math.max(numSetElements, i + 1);
return i * 32 + bit;
}
// No slots available, grow. Record old end, then mark first bit there.
int oldNumElements = numElements;
resize(Math.max(numElements, 1) * 2);
lowestFreeIdx = oldNumElements;
data[oldNumElements] |= 1;
numSetElements = Math.max(numSetElements, oldNumElements + 1);
return oldNumElements * 32;
}
int allocMultiple(int num) {
int numWords = divRoundUp(num, 32);
int base = findFreeBlock(lowestFreeIdx);
boolean found = false;
while (!found) {
int i;
for (i = base; i < numElements && i - base < numWords && data[i] == 0; i++)
;
if (i - base == numWords) {
found = true;
} else if (i == numElements) {
// Hit end — base is valid: data[base..end] are zero, resize extends the run into newly zeroed words.
resize(numElements * 2 + numWords);
found = true;
} else {
// Non-zero word at i, skip past it
base = i + 1;
}
}
// Mark bits as used
int fullWords = num / 32;
int remainder = num % 32;
for (int i = base; i < base + fullWords; i++) {
data[i] = 0xFFFFFFFF;
}
if (remainder != 0) {
data[base + fullWords] |= (1 << remainder) - 1;
}
if (lowestFreeIdx == base) {
lowestFreeIdx = base + num / 32;
}
numSetElements = Math.max(numSetElements, base + numWords);
return base * 32;
}
int findFreeBlock(int start) {
for (int i = start; i < numElements; i++) {
if (data[i] == 0) return i;
}
return numElements;
}
void free(int id) {
int idx = id / 32;
if (idx >= numElements) return;
lowestFreeIdx = Math.min(idx, lowestFreeIdx);
data[idx] &= ~(1 << (id % 32));
if (numSetElements == idx + 1) {
while (numSetElements > 0 && data[numSetElements - 1] == 0) {
numSetElements--;
}
}
}
boolean isAllocated(int id) {
int idx = id / 32;
if (idx >= numSetElements) return false;
return (data[idx] & (1 << (id % 32))) != 0;
}
void reserve(int id) {
final int idx = id / 32;
if (idx >= numElements) resize((idx + 1) * 2);
data[idx] |= 1 << (id % 32);
numSetElements = Math.max(numSetElements, idx + 1);
}
private void resize(int newNumElements) {
if (newNumElements <= numElements) return;
int[] newData = new int[newNumElements];
System.arraycopy(data, 0, newData, 0, numElements);
data = newData;
numElements = newNumElements;
}
private static int divRoundUp(int a, int b) {
return (a + b - 1) / b;
}
}
}
@@ -1046,9 +1046,12 @@ public class DisplayListManager {
}
final boolean locked = GLStateManager.acquireDrawLock();
try {
final CompiledDisplayList previous = displayListCache.put(glListId, compiled);
if (previous != null && previous != CompiledDisplayList.EMPTY && previous != compiled) {
previous.delete();
synchronized (GLSMDraws.displayListIdAllocator) {
final CompiledDisplayList previous = displayListCache.put(glListId, compiled);
if (previous != null && previous != CompiledDisplayList.EMPTY && previous != compiled) {
previous.delete();
}
GLSMDraws.displayListIdAllocator.reserve(glListId);
}
} finally {
if (locked) GLStateManager.releaseDrawLock();
@@ -11,11 +11,8 @@ public final class GLESCaps {
private GLESCaps() {}
public static boolean isCapAllowed(int cap, boolean isGLES, boolean hasClipCullDistance) {
public static boolean isCapAllowed(int cap, boolean isGLES) {
if (!isGLES) return true;
if (cap >= GL11.GL_CLIP_PLANE0 && cap < GL11.GL_CLIP_PLANE0 + GLStateManager.MAX_CLIP_PLANES) {
return hasClipCullDistance;
}
return switch (cap) {
case GL11.GL_LINE_SMOOTH,
GL11.GL_POLYGON_SMOOTH,
@@ -481,7 +481,7 @@ public final class GLSMDraws {
DisplayListManager.glEndList();
}
private static final DisplayListIDAllocator displayListIdAllocator = new DisplayListIDAllocator();
static final DisplayListIDAllocator displayListIdAllocator = new DisplayListIDAllocator();
public static int glGenLists(int range) {
rejectExitCapture();
@@ -30,7 +30,6 @@ public final class GLSMEnable {
// Handle clip planes dynamically (supports up to MAX_CLIP_PLANES)
if (cap >= GL11.GL_CLIP_PLANE0 && cap < GL11.GL_CLIP_PLANE0 + MAX_CLIP_PLANES) {
glCtx.clipPlaneStates[cap - GL11.GL_CLIP_PLANE0].enable();
glCtx.clipPlaneGeneration++;
return;
}
@@ -119,7 +118,6 @@ public final class GLSMEnable {
// Handle clip planes dynamically (supports up to MAX_CLIP_PLANES)
if (cap >= GL11.GL_CLIP_PLANE0 && cap < GL11.GL_CLIP_PLANE0 + MAX_CLIP_PLANES) {
glCtx.clipPlaneStates[cap - GL11.GL_CLIP_PLANE0].disable();
glCtx.clipPlaneGeneration++;
return;
}
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.DisplayListManager.RecordMode;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.glsm.stacks.IntegerStateStack;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
@@ -326,6 +327,11 @@ public final class GLSMFramebuffer {
RENDER_BACKEND.readPixels(x, y, width, height, format, GLSMTextures.remapPixelTypeForGLES(format, type), pixels);
}
public static ScreenCapture createScreenCapture(int width, int height) {
rejectExitCapture();
return RENDER_BACKEND.createScreenCapture(width, height);
}
public static int getDrawFramebufferGeneration() {
return ctx().drawFramebufferGeneration;
}
@@ -88,8 +88,11 @@ public final class GLSMRasterizer {
static void probePointSizeRange(GLContextState glCtx) {
final FloatBuffer range = glCtx.queryScratch;
range.clear();
RENDER_BACKEND.getFloat(GL11.GL_POINT_SIZE_RANGE, range);
final float smooth = range.get(1);
float smooth = 0.0f;
if (!RenderSystem.isGLES()) {
RENDER_BACKEND.getFloat(GL11.GL_POINT_SIZE_RANGE, range);
smooth = range.get(1);
}
RENDER_BACKEND.getFloat(GL12.GL_ALIASED_POINT_SIZE_RANGE, range);
maxPointSize = Math.max(smooth, range.get(1));
}
@@ -506,6 +509,14 @@ public final class GLSMRasterizer {
return glCtx.clipPlaneEnabledMask != 0;
}
public static boolean clipEmulationActive() {
return RenderSystem.isClipDistanceEmulated() && anyClipPlaneEnabled();
}
public static boolean clipEmulationActive(GLContextState glCtx) {
return RenderSystem.isClipDistanceEmulated() && anyClipPlaneEnabled(glCtx);
}
public static void glSampleCoverage(float value, boolean invert) {
if (DisplayListManager.isRecording()) {
throw DisplayListManager.unsupportedInList("glSampleCoverage");
@@ -1211,6 +1211,14 @@ public final class GLSMTextures {
public static void restorePixelUnpackState(PixelStoreState applied) {
PixelStoreState.applyDiff(applied, ctx().pixelUnpackState, false);
}
public static void forcePixelPackState(PixelStoreState target) {
PixelStoreState.applyDiff(ctx().pixelPackState, target, true);
}
public static void restorePixelPackState(PixelStoreState applied) {
PixelStoreState.applyDiff(applied, ctx().pixelPackState, true);
}
public static boolean glIsTexture(int texture) { rejectExitCapture(); return !GLSMTextureLifetime.isDeferredDelete(texture) && RENDER_BACKEND.isTexture(texture); }
public static boolean glIsSampler(int sampler) { rejectExitCapture(); return RENDER_BACKEND.isSampler(sampler); }
@@ -0,0 +1,106 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.glsm.hooks.BatchStateGuard;
import com.gtnewhorizons.angelica.glsm.states.PixelStoreState;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL21;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
public final class GLScreenCapture extends ScreenCapture {
private final int[] textures = new int[RING];
private final int[] framebuffers = new int[RING];
private final int[] buffers = new int[RING];
public GLScreenCapture(int width, int height) {
super(width, height);
}
@Override
protected boolean start(int slot, int srcFramebuffer, int srcWidth, int srcHeight) {
final int width = width();
final int height = height();
final int previousRead = GLSMFramebuffer.getReadFramebuffer();
final int previousDraw = GLSMFramebuffer.getDrawFramebuffer();
final int previousPack = GLStateManager.ctx().boundPixelPackBuffer;
final boolean scissor = GLSMRasterizer.getScissorTest().isEnabled();
BatchStateGuard.suspend();
try {
if (framebuffers[slot] == 0) create(slot);
if (scissor) GLSMEnable.glDisable(GL11.GL_SCISSOR_TEST);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, srcFramebuffer);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, framebuffers[slot]);
GLSMFramebuffer.glBlitFramebuffer(0, 0, srcWidth, srcHeight, 0, 0, width, height, GL11.GL_COLOR_BUFFER_BIT, GL11.GL_LINEAR);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, framebuffers[slot]);
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, buffers[slot]);
GLSMTextures.forcePixelPackState(PixelStoreState.DEFAULT);
GLSMFramebuffer.glReadPixels(0, 0, width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, 0L);
return true;
} finally {
GLSMTextures.restorePixelPackState(PixelStoreState.DEFAULT);
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, previousPack);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, previousRead);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, previousDraw);
if (scissor) GLSMEnable.glEnable(GL11.GL_SCISSOR_TEST);
BatchStateGuard.resume();
}
}
private void create(int slot) {
final int previousTexture = GLSMTextures.getBoundTextureForServerState();
final int texture;
try {
texture = GLSMTextures.glGenTextures();
GLSMTextures.glBindTexture(GL11.GL_TEXTURE_2D, texture);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 0);
GLSMTextures.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, width(), height(), 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null);
} finally {
GLSMTextures.glBindTexture(GL11.GL_TEXTURE_2D, previousTexture);
}
textures[slot] = texture;
framebuffers[slot] = GLSMFramebuffer.glGenFramebuffers();
GLSMFramebuffer.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, framebuffers[slot]);
GLSMFramebuffer.glFramebufferTexture2D(GL30.GL_DRAW_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, texture, 0);
buffers[slot] = GLSMBuffers.glGenBuffers();
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, buffers[slot]);
GLSMBuffers.glBufferData(GL21.GL_PIXEL_PACK_BUFFER, (long) width() * height() * 4, GL15.GL_STREAM_READ);
}
@Override
protected long map(int slot) {
final int previousPack = GLStateManager.ctx().boundPixelPackBuffer;
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, buffers[slot]);
try {
return GLSMBuffers.glMapBufferRangeAddress(GL21.GL_PIXEL_PACK_BUFFER, 0L, (long) width() * height() * 4, GL30.GL_MAP_READ_BIT);
} finally {
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, previousPack);
}
}
@Override
protected void unmap(int slot) {
final int previousPack = GLStateManager.ctx().boundPixelPackBuffer;
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, buffers[slot]);
GLSMBuffers.glUnmapBuffer(GL21.GL_PIXEL_PACK_BUFFER);
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, previousPack);
}
@Override
protected void deleteSlot(int slot) {
if (buffers[slot] != 0) GLSMBuffers.glDeleteBuffers(buffers[slot]);
if (framebuffers[slot] != 0) GLSMFramebuffer.glDeleteFramebuffers(framebuffers[slot]);
if (textures[slot] != 0) GLSMTextures.glDeleteTextures(textures[slot]);
buffers[slot] = 0;
framebuffers[slot] = 0;
textures[slot] = 0;
}
}
@@ -63,7 +63,7 @@ public class RenderSystem {
private static volatile boolean isGLES;
private static volatile int glesVersion;
private static volatile boolean glesDetected;
private static volatile boolean hasClipCullDistance;
private static boolean clipDistanceEmulated;
private static volatile boolean isLTW;
private static volatile boolean ltwDetected;
@@ -132,7 +132,12 @@ public class RenderSystem {
supportsClearTexture = GLStateManager.capabilities.OpenGL44 || GLStateManager.capabilities.GL_ARB_clear_texture;
supportsStencilTexturing = GLStateManager.capabilities.OpenGL43 || GLStateManager.capabilities.GL_ARB_stencil_texturing;
supportsTextureStorage = isGLES ? glesVersion >= 300 : GLStateManager.capabilities.OpenGL42 || GLStateManager.capabilities.GL_ARB_texture_storage;
hasClipCullDistance = !isGLES || hasExtension("GL_EXT_clip_cull_distance");
final boolean esClipExt = !isGLES || hasExtension("GL_EXT_clip_cull_distance");
final int maxClipDistances = esClipExt ? RENDER_BACKEND.getInteger(GL30.GL_MAX_CLIP_DISTANCES) : 0;
clipDistanceEmulated = SystemProperties.EMULATE_CLIP_DISTANCES || maxClipDistances < GLStateManager.MAX_CLIP_PLANES;
if (clipDistanceEmulated) {
GLStateManager.LOGGER.info("Clip distances emulated (GL_MAX_CLIP_DISTANCES={}, forced={})", maxClipDistances, SystemProperties.EMULATE_CLIP_DISTANCES);
}
// Cache maximum image units
if (supportsImageLoadStore) {
@@ -434,8 +439,8 @@ public class RenderSystem {
return supportsImageLoadStore;
}
public static boolean hasClipCullDistance() {
return hasClipCullDistance;
public static boolean isClipDistanceEmulated() {
return clipDistanceEmulated;
}
private static boolean hasExtension(String name) {
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm.backend;
import com.gtnewhorizons.angelica.glsm.GLScreenCapture;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import org.lwjgl.BufferUtils;
@@ -659,6 +660,11 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
GL11.glReadBuffer(mode);
}
@Override
public ScreenCapture createScreenCapture(int width, int height) {
return new GLScreenCapture(width, height);
}
@Override
public void readPixels(int x, int y, int width, int height, int format, int type, ByteBuffer pixels) {
GL11.glReadPixels(x, y, width, height, format, type, pixels);
@@ -479,6 +479,7 @@ public abstract class RenderBackend {
public abstract void readPixels(int x, int y, int width, int height, int format, int type, FloatBuffer pixels);
public abstract void readPixels(int x, int y, int width, int height, int format, int type, IntBuffer pixels);
public abstract void readPixels(int x, int y, int width, int height, int format, int type, long pixelBufferOffset);
public abstract ScreenCapture createScreenCapture(int width, int height);
public abstract void getTexImage(int target, int level, int format, int type, ByteBuffer pixels);
public abstract void getTexImage(int target, int level, int format, int type, IntBuffer pixels);
public abstract void getTexImage(int target, int level, int format, int type, long pixelBufferOffset);
@@ -0,0 +1,102 @@
package com.gtnewhorizons.angelica.glsm.backend;
import com.gtnewhorizons.angelica.glsm.GLSMBuffers;
import org.lwjgl.opengl.GL32;
/** Async downscaled RGBA8 copy of a framebuffer's color. */
public abstract class ScreenCapture {
public static final int RING = RenderBackend.MAX_FRAMES_IN_FLIGHT + 2;
private final int width;
private final int height;
private final long[] tags = new long[RING];
private final long[] syncs = new long[RING];
private int head;
private int tail;
private long mapped;
private boolean deleted;
protected ScreenCapture(int width, int height) {
if (width <= 0 || height <= 0) throw new IllegalArgumentException("capture size " + width + "x" + height);
this.width = width;
this.height = height;
}
public final int width() { return width; }
public final int height() { return height; }
public final boolean idle() { return head == tail; }
public final boolean begin(int srcFramebuffer, int srcWidth, int srcHeight, long tag) {
if (deleted || head - tail == RING || srcWidth <= 0 || srcHeight <= 0) return false;
final int slot = head % RING;
if (!start(slot, srcFramebuffer, srcWidth, srcHeight)) return false;
final long sync = GLSMBuffers.glFenceSync(GL32.GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (sync == 0L) return false;
syncs[slot] = sync;
tags[slot] = tag;
head++;
return true;
}
public final boolean poll() {
if (mapped != 0L) throw new IllegalStateException("poll() before release() of the previous capture");
while (tail != head) {
final int slot = tail % RING;
final int status = GLSMBuffers.glClientWaitSync(syncs[slot], 0, 0L);
if (status == GL32.GL_TIMEOUT_EXPIRED) return false;
dropSync(slot);
if (status != GL32.GL_WAIT_FAILED) {
mapped = map(slot);
if (mapped != 0L) return true;
}
tail++;
}
return false;
}
public final long address() {
if (mapped == 0L) throw new IllegalStateException("no polled capture");
return mapped;
}
public final long tag() {
if (mapped == 0L) throw new IllegalStateException("no polled capture");
return tags[tail % RING];
}
public final void release() {
if (mapped == 0L) throw new IllegalStateException("no polled capture");
mapped = 0L;
unmap(tail % RING);
tail++;
}
public final void delete() {
if (deleted) return;
if (mapped != 0L) release();
deleted = true;
for (int slot = 0; slot < RING; slot++) {
dropSync(slot);
deleteSlot(slot);
}
head = tail = 0;
}
private void dropSync(int slot) {
if (syncs[slot] == 0L) return;
GLSMBuffers.glDeleteSync(syncs[slot]);
syncs[slot] = 0L;
}
/** Returns false when nothing was recorded; the slot then stays free. */
protected abstract boolean start(int slot, int srcFramebuffer, int srcWidth, int srcHeight);
protected abstract long map(int slot);
protected abstract void unmap(int slot);
/** Called for every slot, including ones never started. */
protected abstract void deleteSlot(int slot);
}
@@ -0,0 +1,17 @@
package com.gtnewhorizons.angelica.glsm.ffp;
public final class ClipDistanceGlsl {
public static final String VS_OUT_DIST0 = "out vec4 angelica_ClipDist0;";
public static final String VS_OUT_DIST1 = "out vec4 angelica_ClipDist1;";
public static final String FS_IN_DIST0 = "in vec4 angelica_ClipDist0;";
public static final String FS_IN_DIST1 = "in vec4 angelica_ClipDist1;";
public static final String FS_DISCARD = "if (any(lessThan(angelica_ClipDist0, vec4(0.0))) || any(lessThan(angelica_ClipDist1, vec4(0.0)))) discard;\n";
private ClipDistanceGlsl() {}
public static void appendVsAssign(StringBuilder sb, String[] distances) {
sb.append(" angelica_ClipDist0 = vec4(").append(distances[0]).append(", ").append(distances[1]).append(", ").append(distances[2]).append(", ").append(distances[3]).append(");\n");
sb.append(" angelica_ClipDist1 = vec4(").append(distances[4]).append(", ").append(distances[5]).append(", ").append(distances[6]).append(", ").append(distances[7]).append(");\n");
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLSMCurrentAttribs;
import com.gtnewhorizons.angelica.glsm.GLSMLighting;
import com.gtnewhorizons.angelica.glsm.GLSMMatrices;
import com.gtnewhorizons.angelica.glsm.states.LightState;
@@ -15,12 +16,75 @@ public final class FFPVertexLighting {
private static final Vector4f scratchEyePos = new Vector4f();
private static final Matrix3f scratchNormalMat = new Matrix3f();
private static final Vector3f scratchVec = new Vector3f();
private static final Vector3f scratchFactor = new Vector3f();
private static final Vector3f base = new Vector3f();
private static final Vector3f[] lightDirs = new Vector3f[VertexKey.FFP_LIGHT_COUNT];
private static final Vector3f[] lightDiffuse = new Vector3f[VertexKey.FFP_LIGHT_COUNT];
private static int litCount;
private static boolean normalize;
private static float rescale;
static {
for (int i = 0; i < VertexKey.FFP_LIGHT_COUNT; i++) {
lightDirs[i] = new Vector3f();
lightDiffuse[i] = new Vector3f();
}
}
private FFPVertexLighting() {}
public static boolean modulatesVertexColor(Vector3f destFactor) {
destFactor.set(1.0f);
if (!prepare(false)) return false;
factor(ShaderManager.getCurrentNormal(), scratchFactor);
final float r = scratchFactor.x;
final float g = scratchFactor.y;
final float b = scratchFactor.z;
if (r == 1.0f && g == 1.0f && b == 1.0f) return false;
if (!Float.isFinite(r + g + b)) return false;
destFactor.set(r, g, b);
return true;
}
public static boolean bakeColors(int[] rawBuffer, int vertexCount, boolean hasNormals, boolean hasColor) {
if (vertexCount <= 0 || !prepare(true)) return false;
if (!hasNormals) factor(ShaderManager.getCurrentNormal(), scratchFactor);
float cr = 0.0f, cg = 0.0f, cb = 0.0f;
int alphaBits = 0;
if (!hasColor) {
final var color = GLSMCurrentAttribs.getColor();
cr = clamp01(color.getRed());
cg = clamp01(color.getGreen());
cb = clamp01(color.getBlue());
alphaBits = toByte(clamp01(color.getAlpha())) << 24;
}
boolean haveNormal = false;
int lastNormal = 0;
for (int v = 0, i = 0; v < vertexCount; v++, i += 8) {
if (hasNormals) {
final int packed = rawBuffer[i + 6];
if (!haveNormal || packed != lastNormal) {
factor((byte) packed / 127.0f, (byte) (packed >> 8) / 127.0f, (byte) (packed >> 16) / 127.0f, scratchFactor);
lastNormal = packed;
haveNormal = true;
}
}
if (hasColor) {
final int packed = rawBuffer[i + 5];
cr = (packed & 0xFF) / 255.0f;
cg = ((packed >>> 8) & 0xFF) / 255.0f;
cb = ((packed >>> 16) & 0xFF) / 255.0f;
alphaBits = packed & 0xFF000000;
}
rawBuffer[i + 5] = alphaBits | (toByte(clamp01(cb * scratchFactor.z)) << 16) | (toByte(clamp01(cg * scratchFactor.y)) << 8) | toByte(clamp01(cr * scratchFactor.x));
}
return true;
}
private static boolean prepare(boolean directionalOnly) {
if (!GLSMLighting.getLightingState().isEnabled()) return false;
if (!GLSMLighting.getColorMaterial().isEnabled()) return false;
if (VertexKey.encodeColorMaterialMode(GLSMLighting.getColorMaterialParameter().getValue()) != VertexKey.CM_AMBIENT_AND_DIFFUSE) return false;
@@ -32,44 +96,58 @@ public final class FFPVertexLighting {
final boolean materialHasSpecular = material.shininess > 0.0f && (material.specular.x != 0.0f || material.specular.y != 0.0f || material.specular.z != 0.0f);
final Vector4f lightModelAmbient = GLSMLighting.getLightModel().ambient;
float r = lightModelAmbient.x;
float g = lightModelAmbient.y;
float b = lightModelAmbient.z;
base.set(lightModelAmbient.x, lightModelAmbient.y, lightModelAmbient.z);
final var lightEnables = GLSMLighting.getLightStates();
final var lightData = GLSMLighting.getLightDataStates();
boolean normalReady = false;
litCount = 0;
for (int i = 0; i < VertexKey.FFP_LIGHT_COUNT; i++) {
if (!lightEnables[i].isEnabled()) continue;
final LightState light = lightData[i];
if (materialHasSpecular && (light.specular.x != 0.0f || light.specular.y != 0.0f || light.specular.z != 0.0f))
return false;
if (!normalReady) {
transformCurrentNormal();
normalReady = true;
}
final float nDotVP = Math.max(scratchNormal.dot(lightDirection(light)), 0.0f);
r += light.ambient.x + nDotVP * light.diffuse.x;
g += light.ambient.y + nDotVP * light.diffuse.y;
b += light.ambient.z + nDotVP * light.diffuse.z;
if (directionalOnly && light.position.w != 0.0f) return false;
base.add(light.ambient.x, light.ambient.y, light.ambient.z);
lightDirs[litCount].set(lightDirection(light));
lightDiffuse[litCount].set(light.diffuse.x, light.diffuse.y, light.diffuse.z);
litCount++;
}
if (r == 1.0f && g == 1.0f && b == 1.0f) return false;
if (!Float.isFinite(r + g + b)) return false;
destFactor.set(r, g, b);
if (litCount > 0) {
GLSMMatrices.getModelViewMatrix().normal(scratchNormalMat);
normalize = GLSMLighting.getNormalizeState().isEnabled();
rescale = !normalize && GLSMLighting.getRescaleNormalState().isEnabled() ? Uniforms.rescaleFactor(scratchNormalMat, scratchVec) : 1.0f;
}
return true;
}
private static void transformCurrentNormal() {
GLSMMatrices.getModelViewMatrix().normal(scratchNormalMat);
scratchNormal.set(ShaderManager.getCurrentNormal());
private static void factor(Vector3f normal, Vector3f dest) {
factor(normal.x, normal.y, normal.z, dest);
}
private static void factor(float nx, float ny, float nz, Vector3f dest) {
dest.set(base);
if (litCount == 0) return;
scratchNormal.set(nx, ny, nz);
scratchNormalMat.transform(scratchNormal);
if (GLSMLighting.getNormalizeState().isEnabled()) {
if (normalize) {
scratchNormal.normalize();
} else if (GLSMLighting.getRescaleNormalState().isEnabled()) {
scratchNormal.mul(Uniforms.rescaleFactor(scratchNormalMat, scratchVec));
} else {
scratchNormal.mul(rescale);
}
for (int i = 0; i < litCount; i++) {
final float nDotVP = Math.max(scratchNormal.dot(lightDirs[i]), 0.0f);
dest.fma(nDotVP, lightDiffuse[i]);
}
}
private static float clamp01(float value) {
return Math.min(Math.max(value, 0.0f), 1.0f);
}
private static int toByte(float unit) {
return (int) (unit * 255.0f + 0.5f);
}
private static Vector3f lightDirection(LightState light) {
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.glsm.ffp;
import java.util.Arrays;
import com.gtnewhorizons.angelica.glsm.GLContextState;
import com.gtnewhorizons.angelica.glsm.GLSMRasterizer;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.TexEnvState;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
@@ -79,6 +80,7 @@ public final class FragmentKey {
private static final int GLOBAL_BITS = 14;
private static final int BIT_COMBINED_GLINT = 61;
private static final int BIT_GLINT_REPLACE_ALPHA = 62;
private static final int BIT_CLIP_DISCARD = 63;
private static final int BIT_FOG_MODE = 0; // 2 bits
private static final int BIT_ALPHA_TEST = 2; // 1 bit
private static final int BIT_ALPHA_FUNC = 3; // 3 bits
@@ -126,6 +128,7 @@ public final class FragmentKey {
long global = 0;
if (CombinedGlint.isActive()) global |= 1L << BIT_COMBINED_GLINT;
if (CombinedGlint.replacesAlpha()) global |= 1L << BIT_GLINT_REPLACE_ALPHA;
if (GLSMRasterizer.clipEmulationActive(glCtx)) global |= 1L << BIT_CLIP_DISCARD;
// Fog
global |= activeFogMode(glCtx);
@@ -259,6 +262,7 @@ public final class FragmentKey {
public int fogMode() { return (int) (packed[0] & 0x3); }
public boolean combinedGlint() { return (packed[0] & (1L << BIT_COMBINED_GLINT)) != 0; }
public boolean clipDiscard() { return (packed[0] & (1L << BIT_CLIP_DISCARD)) != 0; }
public boolean glintReplaceAlpha() { return (packed[0] & (1L << BIT_GLINT_REPLACE_ALPHA)) != 0; }
public int textureSupersamplingSamples() { return 1 << (int) ((packed[0] >> BIT_TEXTURE_SUPERSAMPLING) & 0x7); }
public boolean alphaTestEnabled() { return ((packed[0] >> BIT_ALPHA_TEST) & 1) != 0; }
@@ -34,6 +34,9 @@ public final class FragmentShaderGenerator {
}
sb.append("void main() {\n");
if (key.clipDiscard()) {
sb.append(ClipDistanceGlsl.FS_DISCARD);
}
emitLineStipple(sb, key);
emitTextureSampling(sb, key);
emitTexEnvChain(sb, key);
@@ -49,6 +52,9 @@ public final class FragmentShaderGenerator {
private static void emitInputs(StringBuilder sb, FragmentKey key) {
sb.append("// Inputs from vertex shader\n");
sb.append("in vec4 v_Color;\n");
if (key.clipDiscard()) {
sb.append(ClipDistanceGlsl.FS_IN_DIST0).append('\n').append(ClipDistanceGlsl.FS_IN_DIST1).append('\n');
}
if (key.overlayInstanced()) {
sb.append("in vec4 v_Overlay;\n");
}
@@ -2,7 +2,6 @@ package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLContextState;
import com.gtnewhorizons.angelica.glsm.GLSMBuffers;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState;
import com.gtnewhorizons.angelica.glsm.states.FogState;
import com.gtnewhorizons.angelica.glsm.states.LightState;
@@ -330,9 +329,7 @@ public class Uniforms {
private void stageClipPlanes(GLContextState glCtx) {
final ClipPlaneState cps = glCtx.clipPlaneState;
clipPlaneBuf.clear();
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
cps.putEyePlane(i, clipPlaneBuf);
}
cps.putEffectivePlanes(glCtx.clipPlaneEnabledMask, clipPlaneBuf);
final long src = memAddress0(clipPlaneBuf);
for (int i = 0; i < 32 * 4; i += 4) {
putInt(FFPUniformBlock.CLIP_PLANES + i, memGetInt(src + i));
@@ -6,6 +6,7 @@ import com.gtnewhorizons.angelica.glsm.GLContextState;
import com.gtnewhorizons.angelica.glsm.GLSMCurrentAttribs;
import com.gtnewhorizons.angelica.glsm.GLSMRasterizer;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.states.TexGenState;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import org.joml.Matrix4f;
@@ -53,6 +54,7 @@ public final class VertexKey {
private static final int BIT_UNIT23_UV_FROM_UNIT0 = BIT_UNIT_TEXMAT_BASE + MAX_UNITS;
private static final int BIT_LINE_STIPPLE = BIT_UNIT23_UV_FROM_UNIT0 + 1;
private static final int BIT_INSTANCING = BIT_LINE_STIPPLE + 1;
private static final int BIT_CLIP_EMULATED = BIT_INSTANCING + 3;
private static final long INSTANCING_MASK = 0x7L;
static {
@@ -105,6 +107,7 @@ public final class VertexKey {
public int texGenModeQ() { return (int)((packed >> BIT_TEXGEN_Q) & 0x7); }
public boolean texGenEnabled() { return texGenModeS() != TG_NONE || texGenModeT() != TG_NONE || texGenModeR() != TG_NONE || texGenModeQ() != TG_NONE; }
public boolean clipPlanesEnabled() { return bit(BIT_CLIP_PLANES); }
public boolean clipEmulated() { return bit(BIT_CLIP_EMULATED); }
public boolean lineStipple() { return bit(BIT_LINE_STIPPLE); }
public boolean lineQuads() { return bit(BIT_LINE_QUADS); }
public Instancing instancing() { return Instancing.VALUES[(int) ((packed >> BIT_INSTANCING) & INSTANCING_MASK)]; }
@@ -238,6 +241,7 @@ public final class VertexKey {
if (GLSMRasterizer.anyClipPlaneEnabled(glCtx)) {
bits |= (1L << BIT_CLIP_PLANES);
if (RenderSystem.isClipDistanceEmulated()) bits |= 1L << BIT_CLIP_EMULATED;
}
if (glCtx.lineStippleActive) {
@@ -279,6 +283,6 @@ public final class VertexKey {
@Override
public String toString() {
return String.format("FFPVertexKey[0x%012X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%d%d%d%d texmat=%d%d%d%d col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b lquads=%b inst=%s]", packed,lightingEnabled(), lightEnabled(0), lightEnabled(1), colorMaterialEnabled(), fogEnabled(), unitTexCoordEnabled(0)?1:0, unitTexCoordEnabled(1)?1:0, unitTexCoordEnabled(2)?1:0, unitTexCoordEnabled(3)?1:0, unitTexMatEnabled(0)?1:0, unitTexMatEnabled(1)?1:0, unitTexMatEnabled(2)?1:0, unitTexMatEnabled(3)?1:0, hasVertexColor(), hasVertexNormal(), hasVertexTexCoord(), hasVertexLightmap(), texGenModeS(), texGenModeT(), texGenModeR(), texGenModeQ(), clipPlanesEnabled(), lineQuads(), instancing());
return String.format("FFPVertexKey[0x%012X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%d%d%d%d texmat=%d%d%d%d col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b clipEmu=%b lquads=%b inst=%s]", packed,lightingEnabled(), lightEnabled(0), lightEnabled(1), colorMaterialEnabled(), fogEnabled(), unitTexCoordEnabled(0)?1:0, unitTexCoordEnabled(1)?1:0, unitTexCoordEnabled(2)?1:0, unitTexCoordEnabled(3)?1:0, unitTexMatEnabled(0)?1:0, unitTexMatEnabled(1)?1:0, unitTexMatEnabled(2)?1:0, unitTexMatEnabled(3)?1:0, hasVertexColor(), hasVertexNormal(), hasVertexTexCoord(), hasVertexLightmap(), texGenModeS(), texGenModeT(), texGenModeR(), texGenModeQ(), clipPlanesEnabled(), clipEmulated(), lineQuads(), instancing());
}
}
@@ -111,6 +111,9 @@ public final class VertexShaderGenerator {
if (key.lineStipple()) {
sb.append("flat out vec2 v_LineStart;\n");
}
if (key.clipEmulated()) {
sb.append(ClipDistanceGlsl.VS_OUT_DIST0).append('\n').append(ClipDistanceGlsl.VS_OUT_DIST1).append('\n');
}
if (key.separateSpecular()) {
sb.append("out vec3 v_SpecularColor;\n");
}
@@ -398,10 +401,19 @@ public final class VertexShaderGenerator {
private static void emitClipDistances(StringBuilder sb, VertexKey key) {
sb.append(" // Clip distances\n");
final boolean emulated = key.clipEmulated();
final String[] distances = emulated ? new String[8] : null;
for (int i = 0; i < 8; i++) {
final String own = "dot(u_ClipPlane[" + i + "], eyePos)";
final String distance = key.lineQuads() ? acrossLine(own, "dot(u_ClipPlane[" + i + "], lineEyeOther)") : own;
sb.append(" gl_ClipDistance[").append(i).append("] = ").append(distance).append(";\n");
if (emulated) {
distances[i] = distance;
} else {
sb.append(" gl_ClipDistance[").append(i).append("] = ").append(distance).append(";\n");
}
}
if (emulated) {
ClipDistanceGlsl.appendVsAssign(sb, distances);
}
}
@@ -31,6 +31,7 @@ public final class GLSMHooks {
public static final EventBus<ProgramDeleteEvent> PROGRAM_DELETE = EventBus.create(ProgramDeleteEvent.class);
public static final EventBus<BlendFuncChangeEvent> BLEND_FUNC_CHANGE = EventBus.create(BlendFuncChangeEvent.class);
public static final EventBus<FogStateChangeEvent> FOG_STATE_CHANGE = EventBus.create(FogStateChangeEvent.class);
public static final EventBus<ClipPlanesInUseEvent> CLIP_PLANES_IN_USE = EventBus.create(ClipPlanesInUseEvent.class);
public static final EventBus<LightmapCoordsEvent> LIGHTMAP_COORDS = EventBus.create(LightmapCoordsEvent.class);
public static final EventBus<AlphaStateChangeEvent> ALPHA_STATE_CHANGE = EventBus.create(AlphaStateChangeEvent.class);
public static final EventBus<ShaderColorChangeEvent> SHADER_COLOR_CHANGE = EventBus.create(ShaderColorChangeEvent.class);
@@ -46,6 +47,7 @@ public final class GLSMHooks {
public static final ProgramDeleteEvent programDeleteEvent = new ProgramDeleteEvent();
public static final BlendFuncChangeEvent blendFuncChangeEvent = new BlendFuncChangeEvent();
public static final FogStateChangeEvent fogStateChangeEvent = new FogStateChangeEvent();
public static final ClipPlanesInUseEvent clipPlanesInUseEvent = new ClipPlanesInUseEvent();
public static final LightmapCoordsEvent lightmapCoordsEvent = new LightmapCoordsEvent();
public static final AlphaStateChangeEvent alphaStateChangeEvent = new AlphaStateChangeEvent();
public static final ShaderColorChangeEvent shaderColorChangeEvent = new ShaderColorChangeEvent();
@@ -0,0 +1,5 @@
package com.gtnewhorizons.angelica.glsm.hooks.events;
import net.minecraftforge.eventbus.api.event.MutableEvent;
public final class ClipPlanesInUseEvent extends MutableEvent {}
@@ -152,11 +152,11 @@ public final class Tracy {
return backend;
}
private static final String ABI_SENTINEL = "sectionEnter";
private static final String ABI_SENTINEL = "frameImage";
private static boolean implementsCurrentAbi(TracyBackend backend) {
try {
backend.getClass().getMethod(ABI_SENTINEL, int.class, String.class);
backend.getClass().getMethod(ABI_SENTINEL, long.class, int.class, int.class, int.class, boolean.class);
return true;
} catch (NoSuchMethodException e) {
LOGGER.warn("Tracy backend {} was built against an older TracyBackend interface; profiling disabled", backend.getClass().getName());
@@ -337,6 +337,12 @@ public final class Tracy {
if (ptr != 0) BACKEND.frameMark(ptr);
}
/** Copies {@code rgba} before returning; width and height must be multiples of 4. */
public static void frameImage(long rgba, int width, int height, int offset, boolean flip) {
if (!ENABLED || SHUTTING_DOWN) return;
BACKEND.frameImage(rgba, width, height, offset, flip);
}
public static void plotInt(String name, long value) {
plotInt(name, value, TracyBackend.PLOT_FORMAT_NUMBER);
}
@@ -32,6 +32,7 @@ public interface TracyBackend {
void frameMark();
void frameMark(long namePtr);
void frameImage(long rgba, int width, int height, int offset, boolean flip);
long internFrameName(String name);
long internPlotName(String name, int format);
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IVertexArrayObject;
import com.gtnewhorizons.angelica.glsm.GLSMBuffers;
import com.gtnewhorizons.angelica.glsm.GLSMDraws;
import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
import com.gtnewhorizons.angelica.glsm.streaming.LineQuads;
import org.lwjgl.opengl.GL11;
@@ -99,26 +100,31 @@ public final class DisplayListVBO {
public void render() {
if (vao == null) return;
if (lineQuads != null && GLSMDraws.ffpWidensLineQuads()) {
renderLineQuads();
return;
}
vao.bind();
if (pendingExtVbo != 0) {
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, pendingExtVbo);
ImmediateExtendedAttribHandler.setupExtAttribPointers(0L, ImmediateExtendedAttribHandler.EXT_STRIDE);
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
pendingExtVbo = 0;
}
if (count <= 0) { vao.unbind(); return; }
VAOManager.setCurrentVertexFlags(vertexFlags);
FfpExtendedAttribs.beginInternalDraw();
final int prevVAO = GLSMVertexInput.getBoundVAO();
try {
vao.draw(drawMode, start, count);
if (lineQuads != null && GLSMDraws.ffpWidensLineQuads()) {
renderLineQuads();
return;
}
vao.bind();
if (pendingExtVbo != 0) {
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, pendingExtVbo);
ImmediateExtendedAttribHandler.setupExtAttribPointers(0L, ImmediateExtendedAttribHandler.EXT_STRIDE);
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
pendingExtVbo = 0;
}
if (count <= 0) { vao.unbind(); return; }
VAOManager.setCurrentVertexFlags(vertexFlags);
FfpExtendedAttribs.beginInternalDraw();
try {
vao.draw(drawMode, start, count);
} finally {
FfpExtendedAttribs.endInternalDraw();
}
vao.unbind();
} finally {
FfpExtendedAttribs.endInternalDraw();
GLSMVertexInput.glBindVertexArray(prevVAO);
}
vao.unbind();
}
private void renderLineQuads() {
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.GLSMDraws;
import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
public final class BatchedIndexedDrawCmd implements DisplayListCommand {
private IndexedDrawBatch batch;
@@ -25,8 +26,13 @@ public final class BatchedIndexedDrawCmd implements DisplayListCommand {
@Override
public void execute() {
if (batch == null) return;
batch.bind();
GLSMDraws.glDrawElements(drawMode, indexCount, indexType, indexByteOffset);
final int prevVAO = GLSMVertexInput.getBoundVAO();
try {
batch.bind();
GLSMDraws.glDrawElements(drawMode, indexCount, indexType, indexByteOffset);
} finally {
GLSMVertexInput.glBindVertexArray(prevVAO);
}
}
@Override
@@ -1,9 +1,12 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage;
import com.gtnewhorizons.angelica.glsm.GLContextState;
import com.gtnewhorizons.angelica.glsm.GLSMBuffers;
import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
import org.lwjgl.opengl.GL15;
public final class IndexedDrawBatch {
@@ -34,17 +37,29 @@ public final class IndexedDrawBatch {
GLSMVertexInput.glBindVertexArray(v);
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, sharedVBO);
final int stride = layout.stride();
int vertexFlags = 0;
for (int i = 0; i < layout.locations.length; i++) {
final int loc = layout.locations[i];
GLSMVertexInput.glVertexAttribPointer(loc, layout.sizes[i], layout.types[i], layout.normalized[i], stride, layout.offset(i));
GLSMVertexInput.glEnableVertexAttribArray(loc);
vertexFlags |= vertexFlag(loc);
}
// glVertexAttribPointer doesn't set these, and the FFP shader only reads the inputs they name. Saved with the VAO.
VAOManager.setCurrentVertexFlags(vertexFlags);
GLSMBuffers.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEBO);
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
vao = v;
vaoCtx = glCtx;
}
private static int vertexFlag(int location) {
if (location == Usage.COLOR.getAttributeLocation()) return VertexFlags.COLOR_BIT;
if (location == Usage.NORMAL.getAttributeLocation()) return VertexFlags.NORMAL_BIT;
if (location == Usage.PRIMARY_UV.getAttributeLocation()) return VertexFlags.TEXTURE_BIT;
if (location == Usage.SECONDARY_UV.getAttributeLocation()) return VertexFlags.BRIGHTNESS_BIT;
return 0;
}
public void delete() {
if (deleted) return;
deleted = true;
@@ -59,7 +59,7 @@ final class GLSMOwners {
"disableFog", "enableFog", "fogColor", "getFogColor", "getFogMode", "getFogState", "glFog", "glFogf",
"glFogi"},
{"com/gtnewhorizons/angelica/glsm/GLSMFramebuffer",
"attachTextureForReading", "framebufferCompletenessIsMeaningful", "getDrawBuffer", "getDrawFramebuffer",
"attachTextureForReading", "createScreenCapture", "framebufferCompletenessIsMeaningful", "getDrawBuffer", "getDrawFramebuffer",
"getDrawFramebufferGeneration", "getReadFramebuffer", "glBindFramebuffer", "glBindRenderbuffer",
"glBlitFramebuffer", "glCheckFramebufferStatus", "glClear", "glDeleteFramebuffers", "glDeleteRenderbuffers",
"glDrawBuffer", "glDrawBuffers", "glFramebufferRenderbuffer", "glFramebufferTexture",
@@ -106,22 +106,22 @@ final class GLSMOwners {
"glGetIntegeri", "glGetIntegeri_v", "glGetQueryObjecti", "glGetQueryObjectui", "glGetQueryObjectui64",
"glGetString", "glGetStringi", "glIsQuery", "glQueryCounter"},
{"com/gtnewhorizons/angelica/glsm/GLSMRasterizer",
"anyClipPlaneEnabled", "disableCull", "disableScissorTest", "enableCull", "enableScissorTest",
"getAutoNormalState", "getClipPlaneState", "getClipPlaneStates", "getCullState", "getDitherState",
"getEdgeFlagState", "getHintState", "getLineSmoothState", "getLineState", "getLineStippleState",
"getMap1Color4State", "getMap1IndexState", "getMap1NormalState", "getMap1TextureCoord1State",
"getMap1TextureCoord2State", "getMap1TextureCoord3State", "getMap1TextureCoord4State",
"getMap1Vertex3State", "getMap1Vertex4State", "getMap2Color4State", "getMap2IndexState",
"getMap2NormalState", "getMap2TextureCoord1State", "getMap2TextureCoord2State", "getMap2TextureCoord3State",
"getMap2TextureCoord4State", "getMap2Vertex3State", "getMap2Vertex4State", "getMultisampleState",
"getPointSmoothState", "getPointSpriteState", "getPointState", "getPolygonOffsetFillState",
"getPolygonOffsetLineState", "getPolygonOffsetPointState", "getPolygonSmoothState", "getPolygonState",
"getPolygonStippleState", "getSampleAlphaToCoverageState", "getSampleAlphaToOneState",
"getSampleCoverageState", "getScissorState", "getScissorTest", "getShadeModelState", "getViewportState",
"glClipPlane", "glCullFace", "glFrontFace", "glGetClipPlane", "glHint", "glLineStipple", "glLineWidth",
"glPointParameter", "glPointParameterf", "glPointParameteri", "glPointSize", "glPolygonMode",
"glPolygonOffset", "glPolygonOffsetClamp", "glPolygonStipple", "glSampleCoverage", "glScissor",
"glShadeModel", "glViewport"},
"anyClipPlaneEnabled", "clipEmulationActive", "disableCull", "disableScissorTest", "enableCull",
"enableScissorTest", "getAutoNormalState", "getClipPlaneState", "getClipPlaneStates", "getCullState",
"getDitherState", "getEdgeFlagState", "getHintState", "getLineSmoothState", "getLineState",
"getLineStippleState", "getMap1Color4State", "getMap1IndexState", "getMap1NormalState",
"getMap1TextureCoord1State", "getMap1TextureCoord2State", "getMap1TextureCoord3State",
"getMap1TextureCoord4State", "getMap1Vertex3State", "getMap1Vertex4State", "getMap2Color4State",
"getMap2IndexState", "getMap2NormalState", "getMap2TextureCoord1State", "getMap2TextureCoord2State",
"getMap2TextureCoord3State", "getMap2TextureCoord4State", "getMap2Vertex3State", "getMap2Vertex4State",
"getMultisampleState", "getPointSmoothState", "getPointSpriteState", "getPointState",
"getPolygonOffsetFillState", "getPolygonOffsetLineState", "getPolygonOffsetPointState",
"getPolygonSmoothState", "getPolygonState", "getPolygonStippleState", "getSampleAlphaToCoverageState",
"getSampleAlphaToOneState", "getSampleCoverageState", "getScissorState", "getScissorTest",
"getShadeModelState", "getViewportState", "glClipPlane", "glCullFace", "glFrontFace", "glGetClipPlane",
"glHint", "glLineStipple", "glLineWidth", "glPointParameter", "glPointParameterf", "glPointParameteri",
"glPointSize", "glPolygonMode", "glPolygonOffset", "glPolygonOffsetClamp", "glPolygonStipple",
"glSampleCoverage", "glScissor", "glShadeModel", "glViewport"},
{"com/gtnewhorizons/angelica/glsm/GLSMTexEnvGen",
"glGetTexEnv", "glGetTexEnvf", "glGetTexEnvi", "glGetTexGen", "glGetTexGend", "glGetTexGenf",
"glGetTexGeni", "glTexEnv", "glTexEnvf", "glTexEnvi", "glTexGen", "glTexGend", "glTexGenf", "glTexGeni"},
@@ -129,7 +129,7 @@ final class GLSMOwners {
"deferredTexturesAwaitingRelease", "onFrameEnd"},
{"com/gtnewhorizons/angelica/glsm/GLSMTextures",
"abandonTextureStaging", "beginTextureStaging", "commitTextureStaging", "disableTexture", "enableTexture",
"forcePixelUnpackState", "getActiveTextureUnit", "getActiveTextureUnitForServerState",
"forcePixelPackState", "forcePixelUnpackState", "getActiveTextureUnit", "getActiveTextureUnitForServerState",
"getActiveTextureUnitStack", "getBoundTextureForServerState", "getClientActiveTextureUnit",
"getImageUnitBinding", "getMaxBoundTextureUnit", "getSamplerBinding", "getSamplerUnitCount", "getTextures",
"glActiveTexture", "glActiveTextureARB", "glBindImageTexture", "glBindImageTextures",
@@ -142,7 +142,7 @@ final class GLSMOwners {
"glTexImage1D", "glTexImage2D", "glTexImage3D", "glTexParameter", "glTexParameterf", "glTexParameteri",
"glTexStorage2D", "glTexSubImage1D", "glTexSubImage2D", "glTexSubImage3D", "lookupTexParameteri",
"markTextureAsAtlas", "remapTexClamp", "remapTexClampBuffer", "remapTexImageForGLES",
"restorePixelUnpackState", "setActiveTexture", "setFilter", "setTextureSupersamplingSamples",
"restorePixelPackState", "restorePixelUnpackState", "setActiveTexture", "setFilter", "setTextureSupersamplingSamples",
"trackMaxBoundTextureUnit", "trimTextureStaging", "unbindAllSamplers", "updateTexParameterfCache",
"updateTexParameteriCache"},
{"com/gtnewhorizons/angelica/glsm/GLSMVertexInput",
@@ -15,8 +15,10 @@ import org.taumc.glsl.grammar.GLSLParser;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class GlslVulkanPreprocess {
@@ -56,7 +58,9 @@ public final class GlslVulkanPreprocess {
private GlslVulkanPreprocess() {}
private record CacheKey(String source, int glShaderType, boolean separateReadOnlyImages) {}
public record Result(String rewrittenSource, Set<String> boolUniforms, Set<String> explicitVsInputs) {}
public record Result(String rewrittenSource, Set<String> boolUniforms, Set<String> explicitVsInputs, Map<String, UniformBlock> uniformBlocks) {}
public record UniformBlock(String qualifierText, @Nullable String instanceName) {}
/** Replace {@code [startIdx, stopIdx]} with {@code replacement}. */
public record Edit(int startIdx, int stopIdx, String replacement) {}
@@ -109,7 +113,7 @@ public final class GlslVulkanPreprocess {
final List<Edit> edits = new ArrayList<>();
final Metadata meta = collectEdits(new ShaderIndex(root, source), glShaderType, debugName, separateReadOnlyImages, edits);
final Result out = new Result(applyEdits(source, edits), meta.boolUniforms(), meta.explicitVsInputs());
final Result out = new Result(applyEdits(source, edits), meta.boolUniforms(), meta.explicitVsInputs(), meta.uniformBlocks());
synchronized (CACHE) {
CACHE.putAndMoveToFirst(key, out);
while (CACHE.size() > CACHE_MAX) CACHE.removeLast();
@@ -117,11 +121,12 @@ public final class GlslVulkanPreprocess {
return out;
}
public record Metadata(Set<String> boolUniforms, Set<String> explicitVsInputs, boolean needsSamplerless) {}
public record Metadata(Set<String> boolUniforms, Set<String> explicitVsInputs, boolean needsSamplerless, Map<String, UniformBlock> uniformBlocks) {}
public static Metadata collectEdits(ShaderIndex index, int glShaderType, String debugName, boolean separateReadOnlyImages, List<Edit> edits) {
final boolean isVertex = glShaderType == GL20.GL_VERTEX_SHADER;
final Set<String> bools = new HashSet<>();
Map<String, UniformBlock> uniformBlocks = Map.of();
final Set<String> explicitInputs = new HashSet<>();
final Set<String> readOnlyImages = new HashSet<>();
boolean needsSamplerless = false;
@@ -130,6 +135,14 @@ public final class GlslVulkanPreprocess {
final List<Integer> unlocatedVsInputStarts = new ArrayList<>();
for (GLSLParser.DeclarationContext declaration : index.globalDeclarations()) {
if (declaration.LEFT_BRACE() != null && ShaderIndex.hasStorageQualifier(declaration.type_qualifier(), "uniform")) {
final GLSLParser.Type_qualifierContext blockQualifier = declaration.type_qualifier();
final String qualifierText = index.source().substring(startIdx(blockQualifier), stopIdx(blockQualifier) + 1);
final String instanceName = declaration.IDENTIFIER().size() > 1 ? declaration.IDENTIFIER(1).getText() : null;
if (uniformBlocks.isEmpty()) uniformBlocks = new HashMap<>();
uniformBlocks.put(declaration.IDENTIFIER(0).getText(), new UniformBlock(qualifierText, instanceName));
continue;
}
final GLSLParser.Init_declarator_listContext list = declaration.init_declarator_list();
if (list == null) continue;
final GLSLParser.Single_declarationContext single = list.single_declaration();
@@ -223,7 +236,7 @@ public final class GlslVulkanPreprocess {
}
}
return new Metadata(bools, explicitInputs, needsSamplerless);
return new Metadata(bools, explicitInputs, needsSamplerless, uniformBlocks);
}
public static boolean renamesIdentifier(String name) {
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.glsm.shader;
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.UniformBlock;
import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTree;
@@ -26,8 +27,10 @@ import org.taumc.glsl.grammar.GLSLParserBaseListener;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.startIdx;
@@ -65,13 +68,13 @@ public final class SpirvShaderTranslator {
try {
final String emitted = emitGlslEs(cr.spirv(), debugName, glShaderType, pre.explicitVsInputs());
if (emitted == null) return null;
return postProcessEsOutput(emitted, pre.boolUniforms());
return postProcessEsOutput(emitted, pre.boolUniforms(), pre.uniformBlocks());
} finally {
memFree(cr.spirv());
}
}
private static @Nullable String postProcessEsOutput(String glslEs, Set<String> boolUniformNames) {
private static @Nullable String postProcessEsOutput(String glslEs, Set<String> boolUniformNames, Map<String, UniformBlock> uniformBlocks) {
final GLSLParser.Translation_unitContext root;
try {
root = GlslTransformUtils.parseFullQuiet(glslEs);
@@ -80,21 +83,42 @@ public final class SpirvShaderTranslator {
return null;
}
final DefaultBlockInfo block = findDefaultBlock(root);
final List<Edit> edits = new ArrayList<>();
final BlockFinder finder = new BlockFinder(glslEs, uniformBlocks);
ParseTreeWalker.DEFAULT.walk(finder, root);
if (block != null) {
edits.add(new Edit(startIdx(block.structExternalDecl), stopIdx(block.structExternalDecl), buildLooseUniforms(block.members, boolUniformNames)));
final List<Edit> edits = new ArrayList<>();
BlockInfo defaultBlock = null;
final Map<String, Set<String>> bareMembersByVar = new HashMap<>();
for (BlockInfo block : finder.blocks) {
final String structReplacement;
if (block.source == null) {
defaultBlock = block;
structReplacement = buildLooseUniforms(block.members, boolUniformNames);
bareMembersByVar.put(block.varName, block.memberNames());
} else if (block.source.instanceName() == null) {
structReplacement = block.source.qualifierText() + " " + block.structName + " " + block.body + ";";
bareMembersByVar.put(block.varName, block.memberNames());
} else {
structReplacement = block.source.qualifierText() + " " + block.structName + " " + block.body + " " + block.declaratorText + ";";
}
edits.add(new Edit(startIdx(block.structExternalDecl), stopIdx(block.structExternalDecl), structReplacement));
edits.add(new Edit(startIdx(block.uniformExternalDecl), stopIdx(block.uniformExternalDecl), ""));
}
final UseSiteRewriter rewriter = new UseSiteRewriter(block, boolUniformNames, edits);
final Set<String> defaultMembers = defaultBlock == null ? Set.of() : bareMembersByVar.get(defaultBlock.varName);
final UseSiteRewriter rewriter = new UseSiteRewriter(defaultBlock, defaultMembers, boolUniformNames, bareMembersByVar, edits);
ParseTreeWalker.DEFAULT.walk(rewriter, root);
return GlslVulkanPreprocess.applyEdits(glslEs, edits);
}
private record DefaultBlockInfo(ParserRuleContext structExternalDecl, ParserRuleContext uniformExternalDecl, String blockVarName, List<MemberInfo> members) {}
private record BlockInfo(String structName, @Nullable UniformBlock source, ParserRuleContext structExternalDecl, String body, List<MemberInfo> members, ParserRuleContext uniformExternalDecl, String varName, String declaratorText) {
Set<String> memberNames() {
final Set<String> names = new HashSet<>();
for (MemberInfo m : members) names.add(m.name);
return names;
}
}
private record MemberInfo(String typePrefix, String name) {}
@@ -111,31 +135,38 @@ public final class SpirvShaderTranslator {
return sb.toString();
}
private static @Nullable DefaultBlockInfo findDefaultBlock(GLSLParser.Translation_unitContext root) {
final DefaultBlockFinder finder = new DefaultBlockFinder();
ParseTreeWalker.DEFAULT.walk(finder, root);
if (finder.structExternalDecl == null || finder.uniformExternalDecl == null) return null;
return new DefaultBlockInfo(finder.structExternalDecl, finder.uniformExternalDecl, finder.blockVarName, finder.members);
private static boolean isDefaultBlockStruct(String structName) {
return structName.startsWith("_RESERVED_IDENTIFIER_FIXUP_") && structName.endsWith("DefaultUniformBlock");
}
private static final class DefaultBlockFinder extends GLSLParserBaseListener {
String structName;
ParserRuleContext structExternalDecl;
ParserRuleContext uniformExternalDecl;
String blockVarName;
final List<MemberInfo> members = new ArrayList<>();
private static final class BlockFinder extends GLSLParserBaseListener {
private record PendingStruct(@Nullable UniformBlock source, ParserRuleContext externalDecl, String body, List<MemberInfo> members) {}
private final String glslEs;
private final Map<String, UniformBlock> sourceBlocks;
private final Map<String, PendingStruct> pending = new HashMap<>();
final List<BlockInfo> blocks = new ArrayList<>();
BlockFinder(String glslEs, Map<String, UniformBlock> sourceBlocks) {
this.glslEs = glslEs;
this.sourceBlocks = sourceBlocks;
}
@Override
public void enterStruct_specifier(GLSLParser.Struct_specifierContext ctx) {
if (structExternalDecl != null) return;
final TerminalNode id = ctx.IDENTIFIER();
if (id == null) return;
final String idText = id.getText();
if (!idText.startsWith("_RESERVED_IDENTIFIER_FIXUP_") || !idText.endsWith("DefaultUniformBlock")) return;
final UniformBlock source = sourceBlocks.get(idText);
if (source == null && !isDefaultBlockStruct(idText)) return;
structName = idText;
structExternalDecl = findExternalDeclaration(ctx);
final List<MemberInfo> members = source != null && source.instanceName() != null ? List.of() : collectMembers(ctx);
final String body = glslEs.substring(ctx.LEFT_BRACE().getSymbol().getStartIndex(), ctx.RIGHT_BRACE().getSymbol().getStopIndex() + 1);
pending.putIfAbsent(idText, new PendingStruct(source, findExternalDeclaration(ctx), body, members));
}
private static List<MemberInfo> collectMembers(GLSLParser.Struct_specifierContext ctx) {
final List<MemberInfo> members = new ArrayList<>();
for (GLSLParser.Struct_declarationContext sd : ctx.struct_declaration_list().struct_declaration()) {
final StringBuilder typePrefix = new StringBuilder();
if (sd.type_qualifier() != null) typePrefix.append(textWithSpaces(sd.type_qualifier())).append(' ');
@@ -146,11 +177,12 @@ public final class SpirvShaderTranslator {
members.add(new MemberInfo(typePrefix + arraySuffix, decl.IDENTIFIER().getText()));
}
}
return members;
}
@Override
public void enterInit_declarator_list(GLSLParser.Init_declarator_listContext ctx) {
if (structName == null || uniformExternalDecl != null) return;
if (pending.isEmpty()) return;
final GLSLParser.Single_declarationContext single = ctx.single_declaration();
if (single == null || single.typeless_declaration() == null) return;
final GLSLParser.Fully_specified_typeContext fst = single.fully_specified_type();
@@ -159,10 +191,14 @@ public final class SpirvShaderTranslator {
if (fst.type_specifier() == null || fst.type_specifier().type_specifier_nonarray() == null) return;
final GLSLParser.Type_specifier_nonarrayContext ts = fst.type_specifier().type_specifier_nonarray();
if (ts.type_name() == null) return;
if (!structName.equals(ts.type_name().getText())) return;
final String structName = ts.type_name().getText();
final PendingStruct struct = pending.remove(structName);
if (struct == null) return;
blockVarName = single.typeless_declaration().IDENTIFIER().getText();
uniformExternalDecl = findExternalDeclaration(ctx);
final GLSLParser.Typeless_declarationContext declarator = single.typeless_declaration();
final String declaratorText = glslEs.substring(startIdx(declarator), stopIdx(declarator) + 1);
blocks.add(new BlockInfo(structName, struct.source, struct.externalDecl, struct.body, struct.members,
findExternalDeclaration(ctx), declarator.IDENTIFIER().getText(), declaratorText));
}
private static String textWithSpaces(ParseTree tree) {
@@ -192,17 +228,18 @@ public final class SpirvShaderTranslator {
}
private static final class UseSiteRewriter extends GLSLParserBaseListener {
private final @Nullable DefaultBlockInfo block;
private final @Nullable BlockInfo block;
private final Set<String> boolNames;
private final Map<String, Set<String>> bareMembersByVar;
private final List<Edit> edits;
private final Set<String> memberNames;
UseSiteRewriter(@Nullable DefaultBlockInfo block, Set<String> boolNames, List<Edit> edits) {
UseSiteRewriter(@Nullable BlockInfo block, Set<String> memberNames, Set<String> boolNames, Map<String, Set<String>> bareMembersByVar, List<Edit> edits) {
this.block = block;
this.memberNames = memberNames;
this.boolNames = boolNames;
this.bareMembersByVar = bareMembersByVar;
this.edits = edits;
this.memberNames = new HashSet<>();
if (block != null) for (MemberInfo m : block.members) memberNames.add(m.name);
}
@Override
@@ -240,11 +277,11 @@ public final class SpirvShaderTranslator {
}
}
if (block != null && ctx.getChildCount() == 3 && ctx.DOT() != null && ctx.field_selection() != null) {
final ParseTree inner = ctx.getChild(0);
if (inner.getText().equals(block.blockVarName)) {
if (ctx.getChildCount() == 3 && ctx.DOT() != null && ctx.field_selection() != null) {
final Set<String> bareMembers = bareMembersByVar.get(ctx.getChild(0).getText());
if (bareMembers != null) {
final String member = ctx.field_selection().getText();
if (memberNames.contains(member) && !coveredByAncestorBoolCompare(ctx)) {
if (bareMembers.contains(member) && !coveredByAncestorBoolCompare(ctx)) {
edits.add(new Edit(startIdx(ctx), stopIdx(ctx), member));
}
}
@@ -254,8 +291,8 @@ public final class SpirvShaderTranslator {
private @Nullable String resolveBoolOperand(ParseTree operand) {
final String text = operand.getText();
if (boolNames.contains(text)) return text;
if (block != null && text.startsWith(block.blockVarName + ".")) {
final String member = text.substring(block.blockVarName.length() + 1);
if (block != null && text.startsWith(block.varName + ".")) {
final String member = text.substring(block.varName.length() + 1);
if (boolNames.contains(member) && memberNames.contains(member)) return member;
}
return null;
@@ -23,8 +23,12 @@ public class BooleanState implements ISettableState<BooleanState> {
this.ffpStateOnly = ffpStateOnly;
}
protected boolean forwardsToBackend() {
return !ffpStateOnly;
}
public void applyToBackend() {
if (ffpStateOnly) return;
if (!forwardsToBackend()) return;
if (enabled) {
RENDER_BACKEND.enable(glCap);
} else {
@@ -51,7 +55,7 @@ public class BooleanState implements ISettableState<BooleanState> {
stateUnknown = false;
this.enabled = enabled;
onEnabledChanged();
if (!ffpStateOnly) {
if (forwardsToBackend()) {
if (enabled) {
RENDER_BACKEND.enable(this.glCap);
} else {
@@ -27,10 +27,16 @@ public class ClipPlaneState {
dest[3] = tempPlane.w;
}
/** Writes the 4-component eye-space plane equation into {@code buf} at its current position. */
public void putEyePlane(int index, FloatBuffer buf) {
final float[] p = eyePlanes[index];
buf.put(p[0]).put(p[1]).put(p[2]).put(p[3]);
// Vulkan and clip emulation have no per-distance enable: a disabled plane must read as distance 0.
public void putEffectivePlanes(int enabledMask, FloatBuffer buf) {
for (int i = 0; i < eyePlanes.length; i++) {
if ((enabledMask & (1 << i)) != 0) {
final float[] p = eyePlanes[i];
buf.put(p[0]).put(p[1]).put(p[2]).put(p[3]);
} else {
buf.put(0f).put(0f).put(0f).put(0f);
}
}
}
public void getEyePlane(int index, DoubleBuffer buf) {
@@ -0,0 +1,92 @@
package com.gtnewhorizons.angelica.glsm.texture;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL40;
/** Emulates GL proxy textures for backends that lack them. */
public final class ProxyTexture {
private final int maxTextureSize;
private final int max3DTextureSize;
private final int maxCubeMapTextureSize;
private final int maxArrayTextureLayers;
private int target;
private int width;
private int height;
private int depth;
private int internalFormat;
public ProxyTexture(int maxTextureSize, int max3DTextureSize, int maxCubeMapTextureSize, int maxArrayTextureLayers) {
this.maxTextureSize = maxTextureSize;
this.max3DTextureSize = max3DTextureSize;
this.maxCubeMapTextureSize = maxCubeMapTextureSize;
this.maxArrayTextureLayers = maxArrayTextureLayers;
}
public void record(int target, int level, int internalFormat, int width, int height, int depth, boolean formatSupported) {
final boolean layersInHeight = layersInHeight(target);
final boolean layersInDepth = layersInDepth(target);
final int dimLimit = dimensionLimit(target);
final int w = levelZeroEquivalentSize(width, level);
final int h = layersInHeight ? height : levelZeroEquivalentSize(height, level);
final int d = layersInDepth ? depth : levelZeroEquivalentSize(depth, level);
final int hLimit = layersInHeight ? maxArrayTextureLayers : dimLimit;
final int dLimit = layersInDepth ? maxArrayTextureLayers : dimLimit;
final boolean fits = w >= 1 && w <= dimLimit && h >= 1 && h <= hLimit && d >= 1 && d <= dLimit && formatSupported;
if (fits) {
accept(target, w, h, d, internalFormat);
} else {
reject(target);
}
}
public void reject(int target) {
accept(target, 0, 0, 0, 0);
}
private void accept(int target, int width, int height, int depth, int internalFormat) {
this.target = target;
this.width = width;
this.height = height;
this.depth = depth;
this.internalFormat = internalFormat;
}
public int levelParameter(int target, int level, int pname) {
if (this.target != target) return 0;
return switch (pname) {
case GL11.GL_TEXTURE_WIDTH -> TextureInfo.mipLevelSize(width, level);
case GL11.GL_TEXTURE_HEIGHT -> layersInHeight(target) ? height : TextureInfo.mipLevelSize(height, level);
case GL12.GL_TEXTURE_DEPTH -> layersInDepth(target) ? depth : TextureInfo.mipLevelSize(depth, level);
case GL11.GL_TEXTURE_INTERNAL_FORMAT -> internalFormat;
default -> 0;
};
}
public int dimensionLimit(int target) {
return switch (target) {
case GL12.GL_PROXY_TEXTURE_3D -> max3DTextureSize;
case GL13.GL_PROXY_TEXTURE_CUBE_MAP, GL40.GL_PROXY_TEXTURE_CUBE_MAP_ARRAY -> maxCubeMapTextureSize;
default -> maxTextureSize;
};
}
public static int levelZeroEquivalentSize(int size, int level) {
if (size <= 0 || level < 0 || level >= Integer.SIZE - 1) return 0;
final long scaled = (long) size << level;
return scaled > Integer.MAX_VALUE ? 0 : (int) scaled;
}
private static boolean layersInHeight(int target) {
return target == GL30.GL_PROXY_TEXTURE_1D_ARRAY;
}
private static boolean layersInDepth(int target) {
return target == GL30.GL_PROXY_TEXTURE_2D_ARRAY || target == GL32.GL_PROXY_TEXTURE_2D_MULTISAMPLE_ARRAY || target == GL40.GL_PROXY_TEXTURE_CUBE_MAP_ARRAY;
}
}
@@ -15,6 +15,12 @@ public class TextureInfo {
private static final int UNSET_INT = Integer.MIN_VALUE;
private static final float UNSET_FLOAT = Float.NEGATIVE_INFINITY;
public static int mipLevelSize(int base, int level) {
if (base <= 0 || level < 0) return 0;
if (level >= Integer.SIZE - 1) return 1;
return Math.max(1, base >> level);
}
@Getter protected final int id;
@Getter @Setter protected int target = GL11.GL_TEXTURE_2D;
protected int internalFormat = -1;
@@ -241,4 +241,28 @@ class DisplayListIdAllocatorTest {
int base2 = alloc.allocRange(64);
assertEquals(base1, base2, "Freed range should be reused");
}
@Test
void reserveRoutesByIdWithoutDisturbingLowIds() {
final DisplayListIDAllocator alloc = new DisplayListIDAllocator();
alloc.reserve(-5);
alloc.reserve(0);
alloc.reserve(Integer.MAX_VALUE);
assertTrue(alloc.isAllocated(Integer.MAX_VALUE));
assertFalse(alloc.isAllocated(Integer.MAX_VALUE & ((1 << 22) - 1)));
assertEquals(1, alloc.allocRange(1));
alloc.free(Integer.MAX_VALUE);
assertFalse(alloc.isAllocated(Integer.MAX_VALUE));
}
@Test
void rangeAllocationDoesNotSpanSegments() {
final DisplayListIDAllocator alloc = new DisplayListIDAllocator();
assertEquals(32, alloc.allocRange((1 << 22) - 64));
assertEquals(1 << 22, alloc.allocRange(64));
assertFalse(alloc.isAllocated((1 << 22) - 32));
assertTrue(alloc.isAllocated(1 << 22));
alloc.freeRange((1 << 22) - 1, 2);
assertFalse(alloc.isAllocated(1 << 22));
}
}
@@ -40,7 +40,7 @@ class GLESCapsTest {
};
private static boolean onGles(int cap) {
return GLESCaps.isCapAllowed(cap, true, true);
return GLESCaps.isCapAllowed(cap, true);
}
@Test
@@ -60,25 +60,10 @@ class GLESCapsTest {
@Test
void desktopContextAllowsEveryCap() {
for (int cap : DESKTOP_ONLY) {
assertTrue(GLESCaps.isCapAllowed(cap, false, false), () -> "cap 0x" + Integer.toHexString(cap) + " is legal on desktop core and the gate must never suppress it there");
assertTrue(GLESCaps.isCapAllowed(cap, false), () -> "cap 0x" + Integer.toHexString(cap) + " is legal on desktop core and the gate must never suppress it there");
}
for (int cap : VALID_ON_GLES) {
assertTrue(GLESCaps.isCapAllowed(cap, false, false));
assertTrue(GLESCaps.isCapAllowed(cap, false));
}
}
@Test
void clipPlanesFollowClipCullDistance() {
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
final int cap = GL11.GL_CLIP_PLANE0 + i;
assertTrue(GLESCaps.isCapAllowed(cap, true, true), () -> "clip plane " + cap + " works where EXT_clip_cull_distance is present and must not be suppressed");
assertFalse(GLESCaps.isCapAllowed(cap, true, false), () -> "clip plane " + cap + " is invalid on GLES without EXT_clip_cull_distance");
}
}
@Test
void capsOutsideTheClipPlaneRangeAreUnaffectedByClipCullDistance() {
assertTrue(GLESCaps.isCapAllowed(GL11.GL_BLEND, true, false));
assertTrue(GLESCaps.isCapAllowed(GL11.GL_CLIP_PLANE0 + GLStateManager.MAX_CLIP_PLANES, true, false));
}
}
@@ -531,6 +531,12 @@ public class GLSM_ClientArrays_GLTest {
GLSMDraws.glNewList(list, GL11.GL_COMPILE);
GLSMAttribStack.glPushClientAttrib(GL11.GL_CLIENT_VERTEX_ARRAY_BIT);
GLSMAttribStack.glPopClientAttrib();
GLSMDraws.glBegin(GL11.GL_QUADS);
GLSMDraws.glVertex2f(-1f, -1f);
GLSMDraws.glVertex2f(1f, -1f);
GLSMDraws.glVertex2f(1f, 1f);
GLSMDraws.glVertex2f(-1f, 1f);
GLSMDraws.glEnd();
GLSMDraws.glEndList();
GLSMVertexInput.glBindVertexArray(other);
@@ -14,6 +14,7 @@ import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.assertModelv
import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.modelviewAfterCall;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -209,6 +210,25 @@ class GLSM_DisplayList_Stress_Test {
assertEquals(10.0f, modelviewAfterCall(list).get(12), 0.0001f, "Recompilation should translate by 10");
}
@Test
void compiledNameOutsideGeneratedRangeIsNotReissued() {
final int base = GLSMDraws.glGenLists(1);
int extra = base + 1;
while (GLSMDraws.glIsList(extra)) extra++;
testLists.add(base);
testLists.add(extra);
GLSMDraws.glNewList(extra, GL11.GL_COMPILE);
GLSMMatrices.glTranslatef(5.0f, 0.0f, 0.0f);
GLSMDraws.glEndList();
final int next = GLSMDraws.glGenLists(1);
testLists.add(next);
assertNotEquals(extra, next, "glGenLists must not reissue a name installed by glEndList");
GLSMDraws.glDeleteLists(extra, 1);
assertFalse(GLSMDraws.glIsList(extra), "glDeleteLists must release a reserved name");
}
@Test
void testComplexTransformSequence() {
// Test a complex sequence of transforms that could expose numerical issues
@@ -0,0 +1,125 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL21;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@GLCoreTest
class GLScreenCapture_GLTest {
private static final int SRC_W = 64;
private static final int SRC_H = 32;
private static final int DST_W = 16;
private static final int DST_H = 8;
private record Target(int texture, int framebuffer) {
void delete() {
GLSMFramebuffer.glDeleteFramebuffers(framebuffer);
GLSMTextures.glDeleteTextures(texture);
}
}
private static Target quadrantTarget() {
final int texture = GLSMTextures.glGenTextures();
GLSMTextures.glBindTexture(GL11.GL_TEXTURE_2D, texture);
GLSMTextures.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, SRC_W, SRC_H, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null);
GLSMTextures.glBindTexture(GL11.GL_TEXTURE_2D, 0);
final int framebuffer = GLSMFramebuffer.glGenFramebuffers();
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, framebuffer);
GLSMFramebuffer.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, texture, 0);
GlTestSupport.fillQuadrants(SRC_W, SRC_H);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
return new Target(texture, framebuffer);
}
@Test
void downscalesFramebufferQuadrantsBottomUp() {
final Target target = quadrantTarget();
final ScreenCapture capture = GLSMFramebuffer.createScreenCapture(DST_W, DST_H);
try {
assertTrue(capture.begin(target.framebuffer(), SRC_W, SRC_H, 42L));
GL11.glFinish();
assertTrue(capture.poll());
assertEquals(42L, capture.tag());
GlTestSupport.assertQuadrantsBottomUp(capture.address(), DST_W, DST_H);
capture.release();
} finally {
capture.delete();
target.delete();
}
}
@Test
void ringRefusesWhenFullAndReturnsInBeginOrder() {
final Target target = quadrantTarget();
final ScreenCapture capture = GLSMFramebuffer.createScreenCapture(DST_W, DST_H);
try {
for (int i = 0; i < ScreenCapture.RING; i++) assertTrue(capture.begin(target.framebuffer(), SRC_W, SRC_H, i), "capture " + i + " was not recorded");
assertFalse(capture.begin(target.framebuffer(), SRC_W, SRC_H, 99L), "a full ring must refuse a new capture");
GL11.glFinish();
for (int i = 0; i < ScreenCapture.RING; i++) {
assertTrue(capture.poll(), "capture " + i + " not ready after glFinish");
assertEquals(i, capture.tag(), "captures must come back in begin order");
capture.release();
}
assertFalse(capture.poll());
assertTrue(capture.idle());
assertTrue(capture.begin(target.framebuffer(), SRC_W, SRC_H, 100L), "a drained ring must accept captures again");
} finally {
capture.delete();
target.delete();
}
}
@Test
void restoresBindingsAndPixelStore() {
final Target target = quadrantTarget();
final int readFbo = GLSMFramebuffer.glGenFramebuffers();
final int drawFbo = GLSMFramebuffer.glGenFramebuffers();
final int packBuffer = GLSMBuffers.glGenBuffers();
GLSMFramebuffer.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, readFbo);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, drawFbo);
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, packBuffer);
GLSMBuffers.glBufferData(GL21.GL_PIXEL_PACK_BUFFER, 16L, GL15.GL_STREAM_READ);
GLSMTextures.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
GLSMEnable.glEnable(GL11.GL_SCISSOR_TEST);
final ScreenCapture capture = GLSMFramebuffer.createScreenCapture(DST_W, DST_H);
try {
assertTrue(capture.begin(target.framebuffer(), SRC_W, SRC_H, 1L));
GL11.glFinish();
assertTrue(capture.poll());
capture.release();
assertEquals(readFbo, GLSMFramebuffer.getReadFramebuffer());
assertEquals(drawFbo, GLSMFramebuffer.getDrawFramebuffer());
assertEquals(packBuffer, GLStateManager.ctx().boundPixelPackBuffer);
assertEquals(1, GLStateManager.ctx().pixelPackState.alignment());
assertTrue(GLSMRasterizer.getScissorTest().isEnabled());
assertEquals(readFbo, GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING));
assertEquals(drawFbo, GL11.glGetInteger(GL30.GL_DRAW_FRAMEBUFFER_BINDING));
assertEquals(packBuffer, GL11.glGetInteger(GL21.GL_PIXEL_PACK_BUFFER_BINDING));
assertEquals(1, GL11.glGetInteger(GL11.GL_PACK_ALIGNMENT));
assertTrue(GL11.glIsEnabled(GL11.GL_SCISSOR_TEST));
} finally {
capture.delete();
GLSMEnable.glDisable(GL11.GL_SCISSOR_TEST);
GLSMTextures.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 4);
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, 0);
GLSMBuffers.glDeleteBuffers(packBuffer);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLSMFramebuffer.glDeleteFramebuffers(readFbo);
GLSMFramebuffer.glDeleteFramebuffers(drawFbo);
target.delete();
}
}
}
@@ -0,0 +1,124 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLSMAttribStack;
import com.gtnewhorizons.angelica.glsm.GLSMBuffers;
import com.gtnewhorizons.angelica.glsm.GLSMCurrentAttribs;
import com.gtnewhorizons.angelica.glsm.GLSMDraws;
import com.gtnewhorizons.angelica.glsm.GLSMEnable;
import com.gtnewhorizons.angelica.glsm.GLSMMatrices;
import com.gtnewhorizons.angelica.glsm.GLSMRasterizer;
import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
@GLCoreTest
class ClipPlaneGLTest {
private static final int SIZE = 64;
private static final int BACKGROUND = 0xFF000000;
private static final int QUAD = 0xFF0000FF;
@BeforeEach
void setUp() {
FfpFixture.resetFfpState();
FfpFixture.activateFfp();
GLSMRasterizer.glViewport(0, 0, SIZE, SIZE);
GLSMMatrices.glMatrixMode(GL11.GL_PROJECTION);
GLSMMatrices.glLoadIdentity();
GLSMMatrices.glMatrixMode(GL11.GL_MODELVIEW);
GLSMMatrices.glLoadIdentity();
GLSMEnable.glDisable(GL11.GL_DEPTH_TEST);
}
@AfterEach
void tearDown() {
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0 + i);
}
FfpFixture.resetFfpState();
}
@ParameterizedTest(name = "emulated={0}")
@ValueSource(booleans = {false, true})
void disabledPlaneDoesNotClip(boolean emulated) {
try (var ignored = ClipTestSupport.emulation(emulated)) {
ClipTestSupport.setPlane(1, 0, 0, 0, -1);
GLSMEnable.glEnable(GL11.GL_CLIP_PLANE1);
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE1);
ClipTestSupport.setPlane(0, 1, 0, 0, 0);
GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0);
assertRightHalfOnly(drawQuad(), "plane1 disabled, plane0 enabled");
}
}
@ParameterizedTest(name = "emulated={0}")
@ValueSource(booleans = {false, true})
void attribPopRestoresClipping(boolean emulated) {
try (var ignored = ClipTestSupport.emulation(emulated)) {
ClipTestSupport.setPlane(0, 1, 0, 0, 0);
GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0);
assertDriverClipPlane0(!emulated, "after glEnable");
GLSMAttribStack.glPushAttrib(GL11.GL_ENABLE_BIT);
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0);
final int[] unclipped = drawQuad();
GLSMAttribStack.glPopAttrib();
assertDriverClipPlane0(!emulated, "after glPopAttrib");
GLSMAttribStack.replayStateToBackend();
assertDriverClipPlane0(!emulated, "after replayStateToBackend");
final int[] restored = drawQuad();
for (int i = 0; i < unclipped.length; i++) {
final int px = i;
assertEquals(QUAD, unclipped[i], () -> "plane disabled inside the pushed scope, pixel " + px % SIZE + "," + px / SIZE);
}
assertRightHalfOnly(restored, "after glPopAttrib");
}
}
private static void assertDriverClipPlane0(boolean expected, String when) {
assertEquals(expected, GL11.glIsEnabled(GL11.GL_CLIP_PLANE0), () -> when + ": clip plane reaches the driver only when clip distances are native");
}
private static void assertRightHalfOnly(int[] pixels, String what) {
for (int i = 0; i < pixels.length; i++) {
final int x = i % SIZE, y = i / SIZE;
assertEquals(x < SIZE / 2 ? BACKGROUND : QUAD, pixels[i], () -> what + ": first mismatch at pixel " + x + "," + y);
}
}
private static int[] drawQuad() {
final int vao = GLSMVertexInput.glGenVertexArrays();
GLSMVertexInput.glBindVertexArray(vao);
final int vbo = GLSMBuffers.glGenBuffers();
try {
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
final FloatBuffer positions = BufferUtils.createFloatBuffer(18);
positions.put(new float[] { -1, -1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0 }).flip();
GLSMBuffers.glBufferData(GL15.GL_ARRAY_BUFFER, positions, GL15.GL_STATIC_DRAW);
FfpFixture.attrib(0, 3, GL11.GL_FLOAT, false, 12, 0);
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
VAOManager.setCurrentVertexFlags(0);
FfpFixture.clear();
GLSMCurrentAttribs.glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
GLSMDraws.glDrawArrays(GL11.GL_TRIANGLES, 0, 6);
GLSMVertexInput.glBindVertexArray(0);
return FfpFixture.readRegion(SIZE);
} finally {
GLSMBuffers.glDeleteBuffers(vbo);
GLSMVertexInput.glDeleteVertexArrays(vao);
}
}
}
@@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.lwjgl.PointerBuffer;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.util.shaderc.Shaderc;
@@ -55,6 +56,30 @@ class FFPUniformBlockSpirvLayoutTest {
MemoryUtil.memFree(result.spirv());
}
@ParameterizedTest(name = "emulated={0}")
@ValueSource(booleans = {false, true})
void clipDistanceCapabilityFollowsEmulation(boolean emulated) {
long vbits = bit("BIT_CLIP_PLANES");
if (emulated) vbits |= bit("BIT_CLIP_EMULATED");
final String vs = VertexShaderGenerator.generate(VertexKey.fromPacked(vbits));
assertEquals(!emulated, declaresClipDistance(vs, Shaderc.shaderc_vertex_shader), "vertex ClipDistance capability");
}
private static boolean declaresClipDistance(String glsl, int shaderKind) {
final SpirvCompiler.Result result = SpirvCompiler.compile(glsl, shaderKind, "clip-emulation-test", SpirvCompiler.Options.vulkanForced460Core());
assertNotNull(result.spirv(), () -> "SPIR-V compile failed: " + result.error() + "\n" + glsl);
try {
final IntBuffer words = result.spirv().asIntBuffer();
for (int i = 5; i < words.limit(); i += Math.max(1, words.get(i) >>> 16)) {
final int word = words.get(i);
if ((word & 0xFFFF) == 17 && words.get(i + 1) == 32) return true;
}
return false;
} finally {
MemoryUtil.memFree(result.spirv());
}
}
private static VertexKey cubeKey(boolean lightmapUnit, boolean unit0TextureMatrix, boolean texGen) {
long bits = bit("BIT_HAS_VERTEX_COLOR") | bit("BIT_HAS_VERTEX_NORMAL") | bit("BIT_HAS_VERTEX_TEX") | bit("BIT_UNIT_TEX_BASE");
if (lightmapUnit) bits |= 1L << (position("BIT_UNIT_TEX_BASE") + 1);
@@ -27,10 +27,12 @@ import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.stream.Stream;
import static com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags.COLOR_BIT;
import static com.gtnewhorizons.angelica.glsm.GlTestSupport.floats;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -82,6 +84,7 @@ class FFPVertexLightingGLTest {
GLSMEnable.glDisable(GL11.GL_LIGHT1);
GLSMEnable.glDisable(GL11.GL_LIGHT2);
GLSMEnable.glDisable(GL11.GL_COLOR_MATERIAL);
GLSMCurrentAttribs.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
final ShaderManager sm = ShaderManager.getInstance();
if (sm.isActive()) sm.deactivate();
GLSMVertexInput.glBindVertexArray(0);
@@ -126,7 +129,7 @@ class FFPVertexLightingGLTest {
+ " light1=" + GLSMLighting.getLightStates()[1].isEnabled();
}
private void buildTriangle() {
private void buildTriangle(boolean withColor) {
vao = GLSMVertexInput.glGenVertexArrays();
GLSMVertexInput.glBindVertexArray(vao);
@@ -141,9 +144,13 @@ class FFPVertexLightingGLTest {
GLSMBuffers.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
GLSMVertexInput.glEnableVertexAttribArray(POSITION_LOC);
GLSMVertexInput.glVertexAttribPointer(POSITION_LOC, 3, GL11.GL_FLOAT, false, STRIDE, 0L);
GLSMVertexInput.glEnableVertexAttribArray(COLOR_LOC);
GLSMVertexInput.glVertexAttribPointer(COLOR_LOC, 4, GL11.GL_UNSIGNED_BYTE, true, STRIDE, 12L);
VAOManager.setCurrentVertexFlags(COLOR_BIT);
if (withColor) {
GLSMVertexInput.glEnableVertexAttribArray(COLOR_LOC);
GLSMVertexInput.glVertexAttribPointer(COLOR_LOC, 4, GL11.GL_UNSIGNED_BYTE, true, STRIDE, 12L);
VAOManager.setCurrentVertexFlags(COLOR_BIT);
} else {
VAOManager.setCurrentVertexFlags(0);
}
}
private static void putVertex(ByteBuffer buf, float x, float y) {
@@ -154,7 +161,11 @@ class FFPVertexLightingGLTest {
}
private float[] drawAndReadCenter() {
buildTriangle();
return drawAndReadCenter(true);
}
private float[] drawAndReadCenter(boolean withColor) {
buildTriangle(withColor);
final ShaderManager sm = ShaderManager.getInstance();
ShaderManager.enable();
sm.activate();
@@ -233,6 +244,64 @@ class FFPVertexLightingGLTest {
state.run();
assertFalse(FFPVertexLighting.modulatesVertexColor(factor), reason);
assertEquals(IDENTITY_FACTOR, factor, "a rejected solve must leave an identity factor");
assertBakeRejected(reason);
}
private static void assertBakeRejected(String reason) {
final int[] raw = new int[8];
Arrays.fill(raw, 0x5A3C1E0F);
final int[] before = raw.clone();
assertFalse(FFPVertexLighting.bakeColors(raw, 1, true, true), reason + ": bake must be rejected");
assertArrayEquals(before, raw, reason + ": a rejected bake must leave the buffer untouched");
}
@Test
void positionalLightIsNotBaked() {
GLSMLighting.glLight(GL11.GL_LIGHT0, GL11.GL_POSITION, floats(0.0f, 0.0f, 1.0f, 1.0f));
assertBakeRejected("a positional light has a per-vertex direction");
assertTrue(FFPVertexLighting.modulatesVertexColor(factor), "text keeps the object-origin approximation for positional lights");
}
static Stream<Arguments> bakeCases() {
return Stream.of(
Arguments.of("flattened face-on", -1.0e-4f, 0.0f, 0.0f, 1.0f, true, true),
Arguments.of("flattened top", -1.0e-4f, 0.0f, 1.0f, 0.0f, true, true),
Arguments.of("flattened side", -1.0e-4f, 1.0f, 0.0f, 0.0f, true, true),
Arguments.of("unflattened tilted", 1.0f, 0.3f, 0.8f, 0.5f, true, true),
Arguments.of("current color", -1.0e-4f, 0.0f, 1.0f, 0.0f, true, false),
Arguments.of("current normal", -1.0e-4f, 0.0f, 1.0f, 0.0f, false, true));
}
private static int packNormal(float x, float y, float z) {
return ((byte) (x * 127.0f) & 0xFF) | (((byte) (y * 127.0f) & 0xFF) << 8) | (((byte) (z * 127.0f) & 0xFF) << 16);
}
private static float unpackNormal(int packed, int shift) {
return (byte) (packed >> shift) / 127.0f;
}
@ParameterizedTest(name = "{0}")
@MethodSource("bakeCases")
void bakedColorMatchesGeneratedShader(String name, float zScale, float nx, float ny, float nz, boolean hasNormals, boolean hasColor) {
GLSMMatrices.glMatrixMode(GL11.GL_MODELVIEW);
GLSMMatrices.glLoadIdentity();
GLSMMatrices.glScalef(1.0f, 1.0f, zScale);
standardItemLighting();
if (!hasColor) GLSMCurrentAttribs.glColor4f(0.5f, 0.5f, 0.5f, 1.0f);
final int packedNormal = packNormal(nx, ny, nz);
final int[] raw = new int[8];
raw[5] = VERTEX_COLOR;
if (hasNormals) raw[6] = packedNormal;
GLSMCurrentAttribs.glNormal3f(unpackNormal(packedNormal, 0), unpackNormal(packedNormal, 8), unpackNormal(packedNormal, 16));
assertTrue(FFPVertexLighting.bakeColors(raw, 1, hasNormals, hasColor), name + ": expected a bakeable state, " + gateState());
final float[] actual = drawAndReadCenter(hasColor);
for (int c = 0; c < 3; c++) {
final int baked = (raw[5] >>> (c * 8)) & 0xFF;
final int shader = Math.round(actual[c] * 255.0f);
assertEquals(baked, shader, name + ": channel " + c + " baked=" + baked + " shader=" + shader);
}
}
@Test
@@ -3,21 +3,21 @@ package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLSMCurrentAttribs;
import com.gtnewhorizons.angelica.glsm.GLSMDepthStencilAlpha;
import com.gtnewhorizons.angelica.glsm.GLSMEnable;
import com.gtnewhorizons.angelica.glsm.GLSMFog;
import com.gtnewhorizons.angelica.glsm.GLSMLighting;
import com.gtnewhorizons.angelica.glsm.GLSMMatrices;
import com.gtnewhorizons.angelica.glsm.GLSMRasterizer;
import com.gtnewhorizons.angelica.glsm.GLSMTexEnvGen;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import java.nio.DoubleBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -32,12 +32,6 @@ class FfpUniformBlockDedupTest {
return GL30.glGetInteger(GL31.GL_UNIFORM_BUFFER_START, FFPUniformBlock.BINDING_POINT);
}
private static DoubleBuffer doubles(double... values) {
final DoubleBuffer buf = BufferUtils.createDoubleBuffer(values.length);
buf.put(values).flip();
return buf;
}
private static void resetState() {
GLSMMatrices.glMatrixMode(GL11.GL_TEXTURE);
GLSMMatrices.glLoadIdentity();
@@ -58,7 +52,8 @@ class FfpUniformBlockDedupTest {
GLSMLighting.glLight(GL11.GL_LIGHT0, GL11.GL_DIFFUSE, floats(1.0f, 1.0f, 1.0f, 1.0f));
GLSMLighting.glMaterialf(GL11.GL_FRONT, GL11.GL_SHININESS, 0.0f);
GLSMTexEnvGen.glTexGen(GL11.GL_S, GL11.GL_OBJECT_PLANE, floats(1.0f, 0.0f, 0.0f, 0.0f));
GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE0, doubles(0.0, 0.0, 0.0, 0.0));
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0);
ClipTestSupport.setPlane(0, 0.0, 0.0, 0.0, 0.0);
GLSMRasterizer.glLineStipple(1, (short) 0xFFFF);
GLSMRasterizer.glViewport(0, 0, 854, 480);
}
@@ -149,7 +144,10 @@ class FfpUniformBlockDedupTest {
addCategory(tests, "currentTexCoord", () -> GLSMCurrentAttribs.glTexCoord2f(0.125f, 0.375f));
addCategory(tests, "lightmapCoord", () -> GLStateManager.ctx().lastBrightnessX = 96.0f);
addCategory(tests, "texGenPlane", () -> GLSMTexEnvGen.glTexGen(GL11.GL_S, GL11.GL_OBJECT_PLANE, floats(0.5f, 0.0f, 0.0f, 0.25f)));
addCategory(tests, "clipPlane", () -> GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE0, doubles(0.0, 1.0, 0.0, 7.0)));
addCategory(tests, "clipPlane", () -> {
GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0);
ClipTestSupport.setPlane(0, 0.0, 1.0, 0.0, 7.0);
});
addCategory(tests, "alphaRef", () -> GLSMDepthStencilAlpha.glAlphaFunc(GL11.GL_GREATER, 0.35f));
addCategory(tests, "fogColor", () -> GLSMFog.glFog(GL11.GL_FOG_COLOR, floats(0.1f, 0.2f, 0.3f, 0.4f)));
addCategory(tests, "fogDensity", () -> GLSMFog.glFogf(GL11.GL_FOG_DENSITY, 0.75f));
@@ -13,6 +13,7 @@ import com.gtnewhorizons.angelica.glsm.recording.support.DisplayListTestFixture;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
import java.util.List;
@@ -112,7 +113,7 @@ class IndexedDrawReplayTest extends DisplayListTestFixture {
}
@Test
void execute_doesNotRestoreVAO_backToBackDraws() {
void execute_restoresRecordedVao_backToBackDraws() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
@@ -137,7 +138,8 @@ class IndexedDrawReplayTest extends DisplayListTestFixture {
assertSame(batchOf(cmds.getFirst()), batchOf(cmds.get(1)));
final int sharedVAO = vaoOf(batchOf(cmds.getFirst()));
assertNotEquals(0, sharedVAO);
assertEquals(sharedVAO, GLSMVertexInput.getBoundVAO());
assertEquals(vao, GLSMVertexInput.getBoundVAO());
assertEquals(vao, GL11.glGetInteger(GL30.GL_VERTEX_ARRAY_BINDING));
GLSMVertexInput.glBindVertexArray(0);
}
@@ -277,6 +277,39 @@ class SpirvShaderTranslatorTest {
assertFalse(es.contains("!= 0u"), "no bool use-site should still be comparing against 0u");
}
@Test
void uniformBlock_staysABlockUnderGLES() {
final String src = """
#version 330 core
layout(std140) uniform Blk {
mat4 u_M;
vec4 u_C;
};
layout(std140) uniform Named {
vec4 u_N;
} named;
uniform float u_Loose;
struct L { vec4 c; };
uniform L u_Light;
in vec4 color;
out vec4 fragColor;
void main() {
fragColor = u_M * color * u_C * u_Loose + u_Light.c + named.u_N;
}
""";
final String es = SpirvShaderTranslator.glslToGlslEs(src, GL20.GL_FRAGMENT_SHADER, "uniform_block.frag");
dumpOnFailure("uniform_block.frag", src, es);
assertNotNull(es);
assertTrue(Pattern.compile("layout\\s*\\(\\s*std140\\s*\\)\\s*uniform\\s+Blk\\s*\\{").matcher(es).find(), "a source uniform block must reach GLSL ES as a block, so glUniformBlockBinding finds it by name\n\n" + es);
assertFalse(Pattern.compile("uniform\\s+Blk\\s+_").matcher(es).find(), "the block must not be left as a plain struct uniform\n\n" + es);
assertFalse(Pattern.compile("_[0-9]+\\.u_M").matcher(es).find(), "an unnamed source block's members must be accessed bare\n\n" + es);
assertTrue(Pattern.compile("layout\\s*\\(\\s*std140\\s*\\)\\s*uniform\\s+Named\\s*\\{[^}]*\\}\\s*named\\s*;").matcher(es).find(), "an instance-named block keeps its instance name\n\n" + es);
assertTrue(es.contains("named.u_N"), "members of an instance-named block stay qualified\n\n" + es);
assertContainsDecl(es, "float", "u_Loose");
assertTrue(Pattern.compile("uniform\\s+L\\s+u_Light\\s*;").matcher(es).find(), "a struct uniform is not a block and must stay a plain uniform\n\n" + es);
}
@Test
void readonlyImage3D_staysAnImageUnderGLES() {
final String src = """
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
@@ -22,6 +23,35 @@ public final class GlTestSupport {
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
public static void fillQuadrants(int width, int height) {
GLSMRasterizer.glViewport(0, 0, width, height);
GLSMEnable.glEnable(GL11.GL_SCISSOR_TEST);
clearRect(0, 0, width / 2, height / 2, 1f, 0f, 0f);
clearRect(width / 2, 0, width / 2, height / 2, 0f, 1f, 0f);
clearRect(0, height / 2, width / 2, height / 2, 0f, 0f, 1f);
clearRect(width / 2, height / 2, width / 2, height / 2, 1f, 1f, 1f);
GLSMEnable.glDisable(GL11.GL_SCISSOR_TEST);
}
public static void assertQuadrantsBottomUp(long rgba, int width, int height) {
assertRgba(rgba, width, width / 4, height / 4, 0xFF0000FF, "bottom-left");
assertRgba(rgba, width, width * 3 / 4, height / 4, 0x00FF00FF, "bottom-right");
assertRgba(rgba, width, width / 4, height * 3 / 4, 0x0000FFFF, "top-left");
assertRgba(rgba, width, width * 3 / 4, height * 3 / 4, 0xFFFFFFFF, "top-right");
}
private static void clearRect(int x, int y, int width, int height, float r, float g, float b) {
GLSMRasterizer.glScissor(x, y, width, height);
GLSMBlend.glClearColor(r, g, b, 1f);
GLSMFramebuffer.glClear(GL11.GL_COLOR_BUFFER_BIT);
}
private static void assertRgba(long rgba, int width, int x, int y, int expected, String where) {
final long at = rgba + ((long) y * width + x) * 4;
final int actual = (MemoryUtilities.memGetByte(at) & 0xFF) << 24 | (MemoryUtilities.memGetByte(at + 1) & 0xFF) << 16 | (MemoryUtilities.memGetByte(at + 2) & 0xFF) << 8 | (MemoryUtilities.memGetByte(at + 3) & 0xFF);
assertEquals(Integer.toHexString(expected), Integer.toHexString(actual), where + " quadrant at row " + y + " (rows bottom-up)");
}
public static int compile(int type, String source) {
final int shader = GLSMPrograms.glCreateShader(type);
GLSMPrograms.glShaderSource(shader, source);
@@ -0,0 +1,37 @@
package com.gtnewhorizons.angelica.glsm.testutil;
import com.gtnewhorizons.angelica.glsm.GLSMRasterizer;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.nio.DoubleBuffer;
public final class ClipTestSupport {
private ClipTestSupport() {}
public static void setPlane(int index, double a, double b, double c, double d) {
final DoubleBuffer eq = BufferUtils.createDoubleBuffer(4);
eq.put(a).put(b).put(c).put(d).flip();
GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE0 + index, eq);
}
public static Emulation emulation(boolean emulated) {
return new Emulation(emulated);
}
public static final class Emulation implements AutoCloseable {
private final boolean previous;
private Emulation(boolean emulated) {
previous = Reflect.getStatic(RenderSystem.class, "clipDistanceEmulated");
Reflect.setStatic(RenderSystem.class, "clipDistanceEmulated", emulated);
}
@Override
public void close() {
Reflect.setStatic(RenderSystem.class, "clipDistanceEmulated", previous);
}
}
}
+76
View File
@@ -1,3 +1,5 @@
import java.time.Duration
plugins {
`java-library`
`maven-publish`
@@ -75,6 +77,80 @@ dependencies {
compileOnly(libs.annotations)
}
val glesTag = "gles"
val glsmRedirectAgent: Configuration by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
}
val glsmLwjgl2Stubs: Configuration by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
}
dependencies {
glsmRedirectAgent(project(":glsm", "redirectAgent"))
glsmLwjgl2Stubs(project(":glsm", "lwjgl2Stubs"))
testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
testImplementation(testFixtures(project(":glsm")))
testImplementation(project(":glsm")) {
capabilities { requireCapability("${project.group}:glsm-stubs") }
}
listOf(libs.lwjgl3.sdl, libs.lwjgl3.shaderc, libs.lwjgl3.spvc).forEach {
testImplementation(it)
testRuntimeOnly(it) { artifact { classifier = lwjglNatives } }
}
testRuntimeOnly(libs.junit.platform.launcher)
testRuntimeOnly(libs.log4j.core)
testRuntimeOnly(libs.fastutil)
testRuntimeOnly(libs.joml)
testRuntimeOnly(libs.commons.lang3)
testRuntimeOnly(libs.guava)
testRuntimeOnly(libs.celeritas.common) { isTransitive = false }
testRuntimeOnly(libs.asm.tree.test)
}
tasks.test {
useJUnitPlatform {
excludeTags(glesTag)
}
}
val unpackLwjgl3Natives = registerLwjgl3NativesUnpack()
val glesTest by tasks.registering(Test::class) {
description = "Boots GLSM on the LWJGL3 GL backend in a real OpenGL ES 3.2 context."
group = "verification"
testClassesDirs = sourceSets["test"].output.classesDirs
classpath = glsmLwjgl2Stubs.plus(
sourceSets["test"].runtimeClasspath.filter { file ->
!file.name.startsWith("lwjgl-2.") && !file.name.startsWith("lwjgl_util-") && !file.name.startsWith("lwjgl-platform-")
}
)
useJUnitPlatform {
includeTags(glesTag)
}
systemProperty("glsm.stubs.capabilities", "lwjgl3")
javaAgent(glsmRedirectAgent)
loadLwjgl3NativesFrom(unpackLwjgl3Natives)
outputs.cacheIf { false }
timeout = Duration.ofMinutes(5)
systemProperty("junit.jupiter.execution.timeout.default", "60s")
systemProperty("junit.jupiter.execution.timeout.threaddump.enabled", "true")
testLogging {
events("passed", "skipped", "failed")
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
mustRunAfter(tasks.test)
}
if (!providers.environmentVariable("CI").isPresent) {
tasks.check { dependsOn(glesTest) }
}
tasks.named<JavaCompile>("compileJava") {
val classesDir = destinationDirectory
doLast { injectLwjgl3Aware(classesDir.get().asFile) }
@@ -6,8 +6,13 @@ import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.backend.DebugMessageHandler;
import com.gtnewhorizons.angelica.glsm.backend.MainThreadPump;
import com.gtnewhorizons.retrofuturabootstrap.MainStartOnFirstThread;
import com.gtnewhorizons.angelica.glsm.GLScreenCapture;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.texture.ProxyTexture;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import org.jetbrains.annotations.Nullable;
import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware;
import me.eigenraven.lwjgl3ify.client.MainThreadExec;
import org.lwjgl.PointerBuffer;
@@ -46,6 +51,7 @@ import org.lwjgl.sdl.SDLError;
import org.lwjgl.sdl.SDLEvents;
import org.lwjgl.sdl.SDLTimer;
import org.lwjgl.sdl.SDLVideo;
import org.lwjgl.system.FunctionProvider;
import org.lwjgl.system.JNI;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
@@ -95,14 +101,45 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
|| normalizedVendor.startsWith("advanced micro devices");
}
}
final FunctionProvider functions = GL.getFunctionProvider();
if (RenderSystem.isGLES()) {
pfnClearDepthf = GL.getFunctionProvider().getFunctionAddress("glClearDepthf");
pfnDepthRangef = GL.getFunctionProvider().getFunctionAddress("glDepthRangef");
pfnClearDepthf = functions.getFunctionAddress("glClearDepthf");
pfnDepthRangef = functions.getFunctionAddress("glDepthRangef");
proxyTexture = new ProxyTexture(GL11C.glGetInteger(GL11C.GL_MAX_TEXTURE_SIZE), GL11C.glGetInteger(GL12C.GL_MAX_3D_TEXTURE_SIZE),
GL11C.glGetInteger(GL13C.GL_MAX_CUBE_MAP_TEXTURE_SIZE), GL11C.glGetInteger(GL30C.GL_MAX_ARRAY_TEXTURE_LAYERS));
}
pfnGenSamplers = requireFunction(functions, "glGenSamplers");
pfnDeleteSamplers = requireFunction(functions, "glDeleteSamplers");
pfnBindSampler = requireFunction(functions, "glBindSampler");
pfnSamplerParameteri = requireFunction(functions, "glSamplerParameteri");
pfnSamplerParameterf = requireFunction(functions, "glSamplerParameterf");
pfnIsSampler = requireFunction(functions, "glIsSampler");
pfnVertexAttribDivisor = requireFunction(functions, "glVertexAttribDivisor");
}
private static long requireFunction(FunctionProvider functions, String name) {
final long address = functions.getFunctionAddress(name);
if (address == 0L) throw new IllegalStateException(name + " is not exported by the GL driver");
return address;
}
private long pfnClearDepthf;
private long pfnDepthRangef;
private @Nullable ProxyTexture proxyTexture;
// GL33C slots stay unloaded on an ES 3.2 context: LWJGL builds desktop caps only up to the reported 3.2.
private long pfnGenSamplers;
private long pfnDeleteSamplers;
private long pfnBindSampler;
private long pfnSamplerParameteri;
private long pfnSamplerParameterf;
private long pfnIsSampler;
private long pfnVertexAttribDivisor;
private boolean recordProxy(int target, int level, int internalFormat, int width, int height, int depth) {
if (proxyTexture == null || !TextureInfoCache.isProxyTarget(target)) return false;
proxyTexture.record(target, level, internalFormat, width, height, depth, true);
return true;
}
@Override
public void shutdown() {
@@ -251,7 +288,7 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
public void finish() {GL11C.glFinish();}
private static boolean capAllowed(int cap) {
return GLESCaps.isCapAllowed(cap, RenderSystem.isGLES(), RenderSystem.hasClipCullDistance());
return GLESCaps.isCapAllowed(cap, RenderSystem.isGLES());
}
@Override
@@ -563,26 +600,31 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
@Override
public void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
if (recordProxy(target, level, internalFormat, width, height, 1)) return;
GL11C.glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixels);
}
@Override
public void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, DoubleBuffer pixels) {
if (recordProxy(target, level, internalFormat, width, height, 1)) return;
GL11C.glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixels);
}
@Override
public void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, FloatBuffer pixels) {
if (recordProxy(target, level, internalFormat, width, height, 1)) return;
GL11C.glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixels);
}
@Override
public void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, IntBuffer pixels) {
if (recordProxy(target, level, internalFormat, width, height, 1)) return;
GL11C.glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixels);
}
@Override
public void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, long pixelBufferOffset) {
if (recordProxy(target, level, internalFormat, width, height, 1)) return;
GL11C.glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixelBufferOffset);
}
@@ -603,16 +645,19 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
@Override
public void texImage1D(int target, int level, int internalFormat, int width, int border, int format, int type, ByteBuffer pixels) {
if (recordProxy(target, level, internalFormat, width, 1, 1)) return;
GL11C.glTexImage1D(target, level, internalFormat, width, border, format, type, pixels);
}
@Override
public void texImage3D(int target, int level, int internalFormat, int width, int height, int depth, int border, int format, int type, ByteBuffer pixels) {
if (recordProxy(target, level, internalFormat, width, height, depth)) return;
GL12C.glTexImage3D(target, level, internalFormat, width, height, depth, border, format, type, pixels);
}
@Override
public void texImage3D(int target, int level, int internalFormat, int width, int height, int depth, int border, int format, int type, IntBuffer pixels) {
if (recordProxy(target, level, internalFormat, width, height, depth)) return;
GL12C.glTexImage3D(target, level, internalFormat, width, height, depth, border, format, type, pixels);
}
@@ -684,6 +729,7 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
@Override
public int getTexLevelParameteri(int target, int level, int pname) {
if (proxyTexture != null && TextureInfoCache.isProxyTarget(target)) return proxyTexture.levelParameter(target, level, pname);
return GL11C.glGetTexLevelParameteri(target, level, pname);
}
@@ -704,27 +750,33 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
@Override
public int genSamplers() {
return GL33C.glGenSamplers();
try (MemoryStack stack = MemoryStack.stackPush()) {
final IntBuffer ids = stack.mallocInt(1);
JNI.callPV(1, MemoryUtil.memAddress(ids), pfnGenSamplers);
return ids.get(0);
}
}
@Override
public void deleteSamplers(int sampler) {
GL33C.glDeleteSamplers(sampler);
try (MemoryStack stack = MemoryStack.stackPush()) {
JNI.callPV(1, MemoryUtil.memAddress(stack.ints(sampler)), pfnDeleteSamplers);
}
}
@Override
public void bindSampler(int unit, int sampler) {
GL33C.glBindSampler(unit, sampler);
JNI.callV(unit, sampler, pfnBindSampler);
}
@Override
public void samplerParameteri(int sampler, int pname, int param) {
GL33C.glSamplerParameteri(sampler, pname, param);
JNI.callV(sampler, pname, param, pfnSamplerParameteri);
}
@Override
public void samplerParameterf(int sampler, int pname, float param) {
GL33C.glSamplerParameterf(sampler, pname, param);
JNI.callV(sampler, pname, param, pfnSamplerParameterf);
}
@Override
@@ -792,6 +844,11 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
GL11C.glReadBuffer(mode);
}
@Override
public ScreenCapture createScreenCapture(int width, int height) {
return new GLScreenCapture(width, height);
}
@Override
public void readPixels(int x, int y, int width, int height, int format, int type, ByteBuffer pixels) {
GL11C.glReadPixels(x, y, width, height, format, type, pixels);
@@ -1364,7 +1421,7 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
@Override public boolean isFramebuffer(int framebuffer) { return GL30C.glIsFramebuffer(framebuffer); }
@Override public boolean isRenderbuffer(int renderbuffer) { return GL30C.glIsRenderbuffer(renderbuffer); }
@Override public boolean isSampler(int sampler) { return GL33C.glIsSampler(sampler); }
@Override public boolean isSampler(int sampler) { return JNI.callZ(sampler, pfnIsSampler); }
@Override public boolean isQuery(int query) { return GL15C.glIsQuery(query); }
@Override
@@ -1434,7 +1491,7 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
@Override
public void vertexAttribDivisor(int index, int divisor) {
GL33C.glVertexAttribDivisor(index, divisor);
JNI.callV(index, divisor, pfnVertexAttribDivisor);
}
@Override
@@ -0,0 +1,234 @@
package com.gtnewhorizons.angelica.lwjgl3;
import com.gtnewhorizons.angelica.glsm.GLSMCurrentAttribs;
import com.gtnewhorizons.angelica.glsm.GLSMBlend;
import com.gtnewhorizons.angelica.glsm.GLSMDraws;
import com.gtnewhorizons.angelica.glsm.GLSMEnable;
import com.gtnewhorizons.angelica.glsm.GLSMFramebuffer;
import com.gtnewhorizons.angelica.glsm.GLSMRasterizer;
import com.gtnewhorizons.angelica.glsm.GLSMTextures;
import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11C;
import org.lwjgl.opengl.GL12C;
import org.lwjgl.opengl.GL20C;
import org.lwjgl.opengl.GL30C;
import org.lwjgl.opengl.GL33C;
import org.lwjgl.sdl.SDLError;
import org.lwjgl.sdl.SDLInit;
import org.lwjgl.sdl.SDLVideo;
import org.lwjgl.system.Configuration;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Tag("gles")
@DisabledOnOs(OS.MAC)
class GlesSmokeTest {
private static final int SIZE = 64;
private static int fbo;
@BeforeAll
static void boot() {
Configuration.OPENGL_EXPLICIT_INIT.set(true);
assertTrue(SDLInit.SDL_Init(SDLInit.SDL_INIT_VIDEO), () -> "SDL_Init failed: " + SDLError.SDL_GetError());
SDLVideo.SDL_GL_SetAttribute(SDLVideo.SDL_GL_CONTEXT_PROFILE_MASK, SDLVideo.SDL_GL_CONTEXT_PROFILE_ES);
SDLVideo.SDL_GL_SetAttribute(SDLVideo.SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDLVideo.SDL_GL_SetAttribute(SDLVideo.SDL_GL_CONTEXT_MINOR_VERSION, 2);
SDLVideo.SDL_GL_SetAttribute(SDLVideo.SDL_GL_DEPTH_SIZE, 24);
SDLVideo.SDL_GL_SetAttribute(SDLVideo.SDL_GL_STENCIL_SIZE, 8);
final long window = SDLVideo.SDL_CreateWindow("glesSmoke", SIZE, SIZE, SDLVideo.SDL_WINDOW_OPENGL | SDLVideo.SDL_WINDOW_HIDDEN);
assertNotEquals(0L, window, () -> "SDL_CreateWindow failed: " + SDLError.SDL_GetError());
final long context = SDLVideo.SDL_GL_CreateContext(window);
assertNotEquals(0L, context, () -> "SDL_GL_CreateContext failed: " + SDLError.SDL_GetError());
assertTrue(SDLVideo.SDL_GL_MakeCurrent(window, context), () -> "SDL_GL_MakeCurrent failed: " + SDLError.SDL_GetError());
GL.create(SDLVideo::SDL_GL_GetProcAddress);
GL.createCapabilities();
// LWJGL's GL_CONTEXT_PROFILE_MASK probe in createCapabilities leaves INVALID_ENUM on an ES context.
while (GL11C.glGetError() != GL11C.GL_NO_ERROR) {}
final String version = GL11C.glGetString(GL11C.GL_VERSION);
assertTrue(version != null && version.startsWith("OpenGL ES 3."), () -> "not an ES 3.x context: " + version);
assertTrue(BackendManager.RENDER_BACKEND instanceof Lwjgl3GLRenderBackend, () -> "selected backend is " + BackendManager.RENDER_BACKEND.getName());
GLStateManager.initialize(GLSMInitConfig.builder().displaySize(SIZE, SIZE).build());
GLStateManager.setRunningSplash(false);
GLStateManager.markSplashComplete("glesSmoke");
assertTrue(RenderSystem.isGLES());
assertTrue(RenderSystem.supportsSamplerObjects());
fbo = createTarget();
assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError());
}
@Test
void samplerLifecycle() {
final int sampler = RenderSystem.genSampler();
assertNotEquals(0, sampler);
RenderSystem.samplerParameteri(sampler, GL11C.GL_TEXTURE_MIN_FILTER, GL11C.GL_LINEAR);
RenderSystem.samplerParameterf(sampler, GL12C.GL_TEXTURE_MAX_LOD, 4.0f);
RenderSystem.bindSamplerToUnit(0, sampler);
assertEquals(sampler, GL11C.glGetInteger(GL33C.GL_SAMPLER_BINDING));
assertTrue(BackendManager.RENDER_BACKEND.isSampler(sampler));
RenderSystem.destroySampler(sampler);
assertFalse(BackendManager.RENDER_BACKEND.isSampler(sampler));
assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError());
}
@Test
void vertexAttribDivisor() {
final int vao = GLSMVertexInput.glGenVertexArrays();
GLSMVertexInput.glBindVertexArray(vao);
GLSMVertexInput.glVertexAttribDivisor(5, 1);
assertEquals(1, GL20C.glGetVertexAttribi(5, GL33C.GL_VERTEX_ATTRIB_ARRAY_DIVISOR));
GLSMVertexInput.glBindVertexArray(0);
assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError());
}
@Test
void ffpQuadRenders() {
bindClearedTarget();
GLSMDraws.glBegin(GL11C.GL_QUADS);
GLSMCurrentAttribs.glColor4f(0.0f, 1.0f, 0.0f, 1.0f);
GLSMDraws.glVertex3f(-1.0f, -1.0f, 0.0f);
GLSMDraws.glVertex3f(1.0f, -1.0f, 0.0f);
GLSMDraws.glVertex3f(1.0f, 1.0f, 0.0f);
GLSMDraws.glVertex3f(-1.0f, 1.0f, 0.0f);
GLSMDraws.glEnd();
assertEquals(0xFF00FF00, readCenterPixel());
assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError());
}
@Test
void ffpTexturedQuadRenders() {
final int texel = 0xFF2060A0;
final int texture = GLSMTextures.glGenTextures();
GLSMTextures.glBindTexture(GL11C.GL_TEXTURE_2D, texture);
GLSMTextures.glTexParameteri(GL11C.GL_TEXTURE_2D, GL11C.GL_TEXTURE_MIN_FILTER, GL11C.GL_NEAREST);
GLSMTextures.glTexParameteri(GL11C.GL_TEXTURE_2D, GL11C.GL_TEXTURE_MAG_FILTER, GL11C.GL_NEAREST);
final IntBuffer pixels = MemoryUtil.memAllocInt(16);
try {
for (int i = 0; i < 16; i++) pixels.put(i, texel);
GLSMTextures.glTexImage2D(GL11C.GL_TEXTURE_2D, 0, GL11C.GL_RGBA, 4, 4, 0, GL12C.GL_BGRA, GL12C.GL_UNSIGNED_INT_8_8_8_8_REV, pixels);
} finally {
MemoryUtil.memFree(pixels);
}
bindClearedTarget();
GLSMTextures.glBindTexture(GL11C.GL_TEXTURE_2D, texture);
GLSMEnable.glEnable(GL11C.GL_TEXTURE_2D);
GLSMDraws.glBegin(GL11C.GL_QUADS);
GLSMCurrentAttribs.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
GLSMCurrentAttribs.glTexCoord2f(0.0f, 0.0f);
GLSMDraws.glVertex3f(-1.0f, -1.0f, 0.0f);
GLSMCurrentAttribs.glTexCoord2f(1.0f, 0.0f);
GLSMDraws.glVertex3f(1.0f, -1.0f, 0.0f);
GLSMCurrentAttribs.glTexCoord2f(1.0f, 1.0f);
GLSMDraws.glVertex3f(1.0f, 1.0f, 0.0f);
GLSMCurrentAttribs.glTexCoord2f(0.0f, 1.0f);
GLSMDraws.glVertex3f(-1.0f, 1.0f, 0.0f);
GLSMDraws.glEnd();
GLSMEnable.glDisable(GL11C.GL_TEXTURE_2D);
assertEquals(texel, readCenterPixel());
assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError());
}
@ParameterizedTest(name = "emulated={0}")
@ValueSource(booleans = {false, true})
void clipPlaneKeepsOnlyTheEnabledHalfSpace(boolean emulated) {
try (var ignored = ClipTestSupport.emulation(emulated)) {
try {
bindClearedTarget();
ClipTestSupport.setPlane(1, 0.0, 0.0, 0.0, -1.0);
ClipTestSupport.setPlane(0, 1.0, 0.0, 0.0, 0.0);
GLSMEnable.glEnable(GL30C.GL_CLIP_DISTANCE0);
GLSMDraws.glBegin(GL11C.GL_QUADS);
GLSMCurrentAttribs.glColor4f(0.0f, 1.0f, 0.0f, 1.0f);
GLSMDraws.glVertex3f(-1.0f, -1.0f, 0.0f);
GLSMDraws.glVertex3f(1.0f, -1.0f, 0.0f);
GLSMDraws.glVertex3f(1.0f, 1.0f, 0.0f);
GLSMDraws.glVertex3f(-1.0f, 1.0f, 0.0f);
GLSMDraws.glEnd();
assertEquals(0xFF000000, readPixel(SIZE / 4, SIZE / 2), "x < 0 must be clipped by plane 0");
assertEquals(0xFF00FF00, readPixel(3 * SIZE / 4, SIZE / 2), "x >= 0 must survive; plane 1 set but not enabled must not clip");
assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError());
} finally {
GLSMEnable.glDisable(GL30C.GL_CLIP_DISTANCE0);
}
}
}
@Test
void proxyTextureProbeFindsTheMaxTextureSize() {
final int maxSize = Integer.highestOneBit(GL11C.glGetInteger(GL11C.GL_MAX_TEXTURE_SIZE));
int found = -1;
for (int size = maxSize * 2; size > 0 && found < 0; size >>= 1) {
GLSMTextures.glTexImage2D(GL11C.GL_PROXY_TEXTURE_2D, 0, GL11C.GL_RGBA, size, size, 0, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, (ByteBuffer) null);
if (GLSMTextures.glGetTexLevelParameteri(GL11C.GL_PROXY_TEXTURE_2D, 0, GL11C.GL_TEXTURE_WIDTH) != 0) found = size;
}
assertEquals(maxSize, found);
assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError());
}
private static void bindClearedTarget() {
GLSMFramebuffer.glBindFramebuffer(GL30C.GL_FRAMEBUFFER, fbo);
GLSMRasterizer.glViewport(0, 0, SIZE, SIZE);
GLSMBlend.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GLSMFramebuffer.glClear(GL11C.GL_COLOR_BUFFER_BIT | GL11C.GL_DEPTH_BUFFER_BIT);
}
private static int readCenterPixel() {
return readPixel(SIZE / 2, SIZE / 2);
}
private static int readPixel(int x, int y) {
final ByteBuffer pixel = MemoryUtil.memAlloc(4);
try {
GLSMFramebuffer.glReadPixels(x, y, 1, 1, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, pixel);
return (pixel.get(3) & 0xFF) << 24 | (pixel.get(0) & 0xFF) << 16 | (pixel.get(1) & 0xFF) << 8 | pixel.get(2) & 0xFF;
} finally {
MemoryUtil.memFree(pixel);
}
}
private static int createTarget() {
final int color = GLSMTextures.glGenTextures();
GLSMTextures.glBindTexture(GL11C.GL_TEXTURE_2D, color);
GLSMTextures.glTexParameteri(GL11C.GL_TEXTURE_2D, GL11C.GL_TEXTURE_MIN_FILTER, GL11C.GL_NEAREST);
GLSMTextures.glTexParameteri(GL11C.GL_TEXTURE_2D, GL11C.GL_TEXTURE_MAG_FILTER, GL11C.GL_NEAREST);
GLSMTextures.glTexImage2D(GL11C.GL_TEXTURE_2D, 0, GL11C.GL_RGBA8, SIZE, SIZE, 0, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, (ByteBuffer) null);
final int depth = GLSMFramebuffer.glGenRenderbuffers();
GLSMFramebuffer.glBindRenderbuffer(GL30C.GL_RENDERBUFFER, depth);
GLSMFramebuffer.glRenderbufferStorage(GL30C.GL_RENDERBUFFER, GL30C.GL_DEPTH24_STENCIL8, SIZE, SIZE);
final int target = GLSMFramebuffer.glGenFramebuffers();
GLSMFramebuffer.glBindFramebuffer(GL30C.GL_FRAMEBUFFER, target);
GLSMFramebuffer.glFramebufferTexture2D(GL30C.GL_FRAMEBUFFER, GL30C.GL_COLOR_ATTACHMENT0, GL11C.GL_TEXTURE_2D, color, 0);
GLSMFramebuffer.glFramebufferRenderbuffer(GL30C.GL_FRAMEBUFFER, GL30C.GL_DEPTH_STENCIL_ATTACHMENT, GL30C.GL_RENDERBUFFER, depth);
GLSMFramebuffer.glBindFramebuffer(GL30C.GL_FRAMEBUFFER, 0);
return target;
}
}
+8
View File
@@ -0,0 +1,8 @@
# Clip-plane scene for -Dangelica.debug.testBlocks=true. ~ is the flight start, yaw 0 faces south (+z).
# Each angelica:test_clip_plane TESR keeps only the lower half of a gold cube (plane 0) and leaves a reject-all plane 1 disabled.
gamerule doMobSpawning false
gamerule doDaylightCycle false
time set 6000
fill ~-2 ~-1 ~4 ~2 ~0 ~4 angelica:test_clip_plane
+7 -1
View File
@@ -106,7 +106,13 @@ val glsmSdlAgent = sourceSets.create("glsmSdlAgent") {
java.srcDir("src/glsmSdlAgent/java")
}
val glsmLwjgl2Stubs: Configuration by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
}
dependencies {
glsmLwjgl2Stubs(project(":glsm", "lwjgl2Stubs"))
"glsmSdlAgentCompileOnly"(project(":glsm"))
"glsmSdlAgentCompileOnly"(testFixtures(project(":glsm")))
"glsmSdlAgentCompileOnly"(libs.asm.tree.test)
@@ -148,7 +154,7 @@ val glsmSdlTest by tasks.registering(Test::class) {
description = "Runs the headless GLSM-on-SDL FFP rig in its own JVM, with the SDL GPU gate engaged."
group = "verification"
testClassesDirs = sourceSets["test"].output.classesDirs
classpath = glsmSdlStubs.output.classesDirs.plus(
classpath = glsmSdlStubs.output.classesDirs.plus(glsmLwjgl2Stubs).plus(
sourceSets["test"].runtimeClasspath.filter { file ->
!file.name.startsWith("lwjgl-2.") && !file.name.startsWith("lwjgl_util-") && !file.name.startsWith("lwjgl-platform-")
}
@@ -1,12 +0,0 @@
package org.lwjgl.opengl;
public final class GLContext {
private static final ContextCapabilities CAPABILITIES = new ContextCapabilities();
private GLContext() {}
public static ContextCapabilities getCapabilities() {
return CAPABILITIES;
}
}
@@ -18,7 +18,9 @@ import me.eigenraven.lwjgl3ify.client.MainThreadExec;
import org.lwjglx.Lwjgl3ifyEventLoop;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import com.gtnewhorizons.angelica.glsm.texture.TextureStaging;
import com.gtnewhorizons.angelica.sdlgpu.compute.ComputeBinder;
@@ -42,6 +44,7 @@ import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineStore;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.VertexAttribs;
import com.gtnewhorizons.angelica.sdlgpu.resource.BufferParams;
import com.gtnewhorizons.angelica.sdlgpu.resource.ByteRegionCopy;
import com.gtnewhorizons.angelica.sdlgpu.resource.ColorSource;
import com.gtnewhorizons.angelica.sdlgpu.resource.CopyRectClip;
import com.gtnewhorizons.angelica.sdlgpu.resource.FBOClearTracker;
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
@@ -111,6 +114,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import org.lwjgl.util.spvc.Spvc;
import org.lwjgl.sdl.SDLGPU;
import org.lwjgl.sdl.SDL_GPUBlitInfo;
import org.lwjgl.sdl.SDLTimer;
@@ -168,7 +172,7 @@ public class SDLGPURenderBackend extends RenderBackend {
new VoxelizationDispatcher(computeBinder);
private final VertexAttribs vertexAttribs = new VertexAttribs();
private final TextureOps textureOps = new TextureOps(device, frameManager, resourceManager, fboClearTracker);
private final ReadbackShadows readbackShadows = new ReadbackShadows(device);
private final ReadbackShadows readbackShadows = new ReadbackShadows(device, resourceManager, fenceTracker);
private final PersistentBufferSync persistentSync = new PersistentBufferSync(frameManager, resourceManager,
new PersistentBufferSync.UploadSink() {
@Override public void enqueue(TransferThread.DeferredUpload upload) { enqueueUpload(upload); }
@@ -1378,8 +1382,7 @@ public class SDLGPURenderBackend extends RenderBackend {
}
resourceManager.dropArrayShadow(dstGlId);
resourceManager.dropEboShadow(dstGlId);
resourceManager.markBufferContentsDefined(dstGlId);
readbackShadows.invalidate(dstGlId, writeOffset);
onBufferContentsWritten(dstGlId);
final ByteBuffer dstUboShadow = resourceManager.getUboShadow(dstGlId);
if (dstUboShadow != null) {
@@ -1460,9 +1463,8 @@ public class SDLGPURenderBackend extends RenderBackend {
}
final long cp = frameManager.ensureCopyPass();
if (cp == 0) return;
if (resourceManager.getBufferGlUsage(dstGlId) == GL15.GL_STREAM_READ
&& readbackShadows.scheduleDownload(cp, srcHandle, readOffset, dstGlId, writeOffset, size)) {
return;
if (resourceManager.getBufferGlUsage(dstGlId) == GL15.GL_STREAM_READ && !isBoundAsSsbo(s(), dstGlId)) {
readbackShadows.scheduleDownload(cp, srcHandle, readOffset, dstGlId, writeOffset, size);
}
try (var stack = MemoryStack.stackPush()) {
final SDL_GPUBufferLocation src = SDL_GPUBufferLocation.calloc(stack).buffer(srcHandle).offset((int) readOffset);
@@ -1507,52 +1509,8 @@ public class SDLGPURenderBackend extends RenderBackend {
return TextureInfoCache.isProxyTarget(target);
}
private static boolean proxyLayersInHeight(int target) {
return target == GL30.GL_PROXY_TEXTURE_1D_ARRAY;
}
private static boolean proxyLayersInDepth(int target) {
return isProxyArrayTarget(target) && !proxyLayersInHeight(target);
}
private static boolean isProxyArrayTarget(int target) {
return target == GL30.GL_PROXY_TEXTURE_1D_ARRAY || target == GL30.GL_PROXY_TEXTURE_2D_ARRAY || target == GL32.GL_PROXY_TEXTURE_2D_MULTISAMPLE_ARRAY || target == GL40.GL_PROXY_TEXTURE_CUBE_MAP_ARRAY;
}
public static int proxyLevelSize(int base, int level) {
return PixelOps.mipLevelSize(base, level);
}
static int proxyDimensionLimit(int target) {
return switch (target) {
case GL12.GL_PROXY_TEXTURE_3D -> MAX_3D_TEXTURE_SIZE;
case GL13.GL_PROXY_TEXTURE_CUBE_MAP, GL40.GL_PROXY_TEXTURE_CUBE_MAP_ARRAY -> MAX_CUBE_MAP_TEXTURE_SIZE;
default -> MAX_TEXTURE_SIZE;
};
}
static int levelZeroEquivalentSize(int size, int level) {
if (size <= 0 || level < 0 || level >= Integer.SIZE - 1) return 0;
final long scaled = (long) size << level;
return scaled > Integer.MAX_VALUE ? 0 : (int) scaled;
}
private void recordProxyTexImage(ContextState st, int target, int level, int internalFormat, int width, int height, int depth) {
final boolean layersInHeight = proxyLayersInHeight(target);
final boolean layersInDepth = proxyLayersInDepth(target);
final int dimLimit = proxyDimensionLimit(target);
final int w = levelZeroEquivalentSize(width, level);
final int h = layersInHeight ? height : levelZeroEquivalentSize(height, level);
final int d = layersInDepth ? depth : levelZeroEquivalentSize(depth, level);
final int hLimit = layersInHeight ? MAX_ARRAY_TEXTURE_LAYERS : dimLimit;
final int dLimit = layersInDepth ? MAX_ARRAY_TEXTURE_LAYERS : dimLimit;
final boolean fits = w >= 1 && w <= dimLimit && h >= 1 && h <= hLimit && d >= 1 && d <= dLimit
&& resourceManager.isMappableTextureFormat(internalFormat);
if (fits) {
st.proxyTexture.accept(target, w, h, d, internalFormat);
} else {
st.proxyTexture.reject(target);
}
st.proxyTexture.record(target, level, internalFormat, width, height, depth, resourceManager.isMappableTextureFormat(internalFormat));
}
private static int boundTextureFor(ContextState st, int target) {
@@ -1816,23 +1774,13 @@ public class SDLGPURenderBackend extends RenderBackend {
}
@Override public int getTexLevelParameteri(int target, int level, int pname) {
final ContextState cs = s();
if (isProxyTarget(target)) {
final ContextState.ProxyTextureState proxy = cs.proxyTexture;
if (proxy.target != target) return 0;
return switch (pname) {
case GL11.GL_TEXTURE_WIDTH -> proxyLevelSize(proxy.width, level);
case GL11.GL_TEXTURE_HEIGHT -> proxyLayersInHeight(target) ? proxy.height : proxyLevelSize(proxy.height, level);
case GL12.GL_TEXTURE_DEPTH -> proxyLayersInDepth(target) ? proxy.depth : proxyLevelSize(proxy.depth, level);
case GL11.GL_TEXTURE_INTERNAL_FORMAT -> proxy.internalFormat;
default -> 0;
};
}
if (isProxyTarget(target)) return cs.proxyTexture.levelParameter(target, level, pname);
final int glId = cs.boundTextures[cs.activeTextureUnit];
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(glId);
if (meta == null) return 0;
return switch (pname) {
case GL11.GL_TEXTURE_WIDTH -> PixelOps.mipLevelSize(meta.width(), level);
case GL11.GL_TEXTURE_HEIGHT -> PixelOps.mipLevelSize(meta.height(), level);
case GL11.GL_TEXTURE_WIDTH -> TextureInfo.mipLevelSize(meta.width(), level);
case GL11.GL_TEXTURE_HEIGHT -> TextureInfo.mipLevelSize(meta.height(), level);
case GL11.GL_TEXTURE_INTERNAL_FORMAT -> {
final int stored = meta.glFormat();
yield TextureInfoCache.isGenericCompressedInternalFormat(stored) ? GL11.GL_RGBA8 : stored;
@@ -1987,8 +1935,8 @@ public class SDLGPURenderBackend extends RenderBackend {
if (meta != null) {
fbo.colorFormats[colorIdx] = meta.sdlFormat();
if (colorIdx == 0) {
fbo.width = PixelOps.mipLevelSize(meta.width(), level);
fbo.height = PixelOps.mipLevelSize(meta.height(), level);
fbo.width = TextureInfo.mipLevelSize(meta.width(), level);
fbo.height = TextureInfo.mipLevelSize(meta.height(), level);
}
}
fbo.colorAttachmentCount = Math.max(fbo.colorAttachmentCount, colorIdx + 1);
@@ -2195,6 +2143,32 @@ public class SDLGPURenderBackend extends RenderBackend {
if (capacity < needed) throw new IllegalArgumentException("readback of " + width + "x" + height + " format 0x" + Integer.toHexString(format) + " type 0x" + Integer.toHexString(type) + " needs " + needed + " bytes, destination has " + capacity);
}
@Override public ScreenCapture createScreenCapture(int width, int height) {
return new SDLScreenCapture(this, device, frameManager, resourceManager, textureOps, width, height);
}
ColorSource colorSourceForRead(int fboId) {
final FboState fbo = fboId != 0 ? resourceManager.getFbo(fboId) : null;
if (fbo == null) return null;
final ContextState cs = s();
final ColorSource source = resolveColorAttachment(fbo, cs.readColor);
if (source.texture == 0L || source.width <= 0 || source.height <= 0) return null;
fboClearTracker.materializePendingClearForRead(cs, frameManager.frame(), source.texture);
return source;
}
private ColorSource resolveColorAttachment(FboState fbo, ColorSource out) {
final int colorGlId = fbo.colorGlIds[fbo.readBufferIndex];
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(colorGlId);
out.texture = resourceManager.getTextureHandle(colorGlId);
out.level = fbo.colorLevels[fbo.readBufferIndex];
out.sdlFormat = meta != null ? meta.sdlFormat() : SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
out.glBaseFormat = meta != null ? PixelPack.glBaseFormat(meta.glFormat()) : GL11.GL_RGBA;
out.width = meta != null ? TextureInfo.mipLevelSize(meta.width(), out.level) : fbo.width;
out.height = meta != null ? TextureInfo.mipLevelSize(meta.height(), out.level) : fbo.height;
return out;
}
private void readPixelsInto(int x, int y, int width, int height, int format, int type, long dst, long capacity, int pbo, long pboOffset) {
PixelPack.validate(format, type);
if (width <= 0 || height <= 0) return;
@@ -2224,14 +2198,13 @@ public class SDLGPURenderBackend extends RenderBackend {
final FboState fbo = resourceManager.getFbo(cs.boundReadFboId);
if (fbo == null) return;
if (kind == READBACK_COLOR) {
final int colorGlId = fbo.colorGlIds[fbo.readBufferIndex];
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(colorGlId);
colorTex = resourceManager.getTextureHandle(colorGlId);
colorFormat = meta != null ? meta.sdlFormat() : SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
if (meta != null) colorBase = PixelPack.glBaseFormat(meta.glFormat());
srcLevel = fbo.colorLevels[fbo.readBufferIndex];
srcWidth = meta != null ? PixelOps.mipLevelSize(meta.width(), srcLevel) : fbo.width;
srcHeight = meta != null ? PixelOps.mipLevelSize(meta.height(), srcLevel) : fbo.height;
final ColorSource source = resolveColorAttachment(fbo, cs.readColor);
colorTex = source.texture;
colorFormat = source.sdlFormat;
colorBase = source.glBaseFormat;
srcLevel = source.level;
srcWidth = source.width;
srcHeight = source.height;
} else {
if (fbo.depthLevel != 0) throw new UnsupportedOperationException("glReadPixels of a depth/stencil attachment at level " + fbo.depthLevel + " is not supported on SDL-GPU");
depthTex = fbo.depthTexture;
@@ -2258,8 +2231,8 @@ public class SDLGPURenderBackend extends RenderBackend {
final int glId = boundTextureFor(cs, target);
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(glId);
if (meta == null || level < 0 || level >= meta.levels()) return;
final int width = PixelOps.mipLevelSize(meta.width(), level);
final int height = PixelOps.mipLevelSize(meta.height(), level);
final int width = TextureInfo.mipLevelSize(meta.width(), level);
final int height = TextureInfo.mipLevelSize(meta.height(), level);
if (width <= 0 || height <= 0) return;
checkReadbackCapacity(format, type, width, height, loadPackState(cs), capacity);
final int kind = readbackKind(format);
@@ -2348,7 +2321,7 @@ public class SDLGPURenderBackend extends RenderBackend {
} finally {
region.clear();
}
resourceManager.markBufferContentsDefined(pbo);
onBufferContentsWritten(pbo);
flushAndSubmitMidFrame();
}
@@ -2575,17 +2548,23 @@ public class SDLGPURenderBackend extends RenderBackend {
if (length != null && length.remaining() > 0) length.put(0, 0);
return;
}
if (s == null) return;
final byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
final int n = Math.min(bytes.length, out.remaining());
out.put(bytes, 0, n);
final byte[] bytes = (s == null ? "" : s).getBytes(StandardCharsets.UTF_8);
final int pos = out.position();
final int max = out.remaining();
final int n = Math.max(0, Math.min(bytes.length, max - 1));
for (int i = 0; i < n; i++) out.put(pos + i, bytes[i]);
if (max > 0) out.put(pos + n, (byte) 0);
if (length != null && length.remaining() > 0) length.put(0, n);
}
private static int glStringLength(String s) {
return s == null || s.isEmpty() ? 0 : s.getBytes(StandardCharsets.UTF_8).length + 1;
}
@Override public int getShaderi(int shader, int pname) {
return switch (pname) {
case GL20.GL_COMPILE_STATUS -> shaderManager.isCompiled(shader) ? 1 : 0;
case GL20.GL_SHADER_TYPE -> shaderManager.getShaderType(shader);
case GL20.GL_INFO_LOG_LENGTH -> shaderManager.getShaderInfoLog(shader).length();
case GL20.GL_INFO_LOG_LENGTH -> glStringLength(shaderManager.getShaderInfoLog(shader));
case GL20.GL_SHADER_SOURCE_LENGTH -> glStringLength(shaderManager.getShaderSource(shader));
default -> 0;
};
}
@@ -2593,7 +2572,7 @@ public class SDLGPURenderBackend extends RenderBackend {
return switch (pname) {
case GL20.GL_LINK_STATUS -> shaderManager.isLinked(program) ? 1 : 0;
case GL20.GL_VALIDATE_STATUS -> shaderManager.isLinked(program) ? 1 : 0;
case GL20.GL_INFO_LOG_LENGTH -> shaderManager.getProgramInfoLog(program).length();
case GL20.GL_INFO_LOG_LENGTH -> glStringLength(shaderManager.getProgramInfoLog(program));
case GL20.GL_ATTACHED_SHADERS -> shaderManager.getAttachedShaderCount(program);
case GL20.GL_ACTIVE_UNIFORMS -> {
final var entries = getOrBuildUniformEntries(program);
@@ -2633,6 +2612,7 @@ public class SDLGPURenderBackend extends RenderBackend {
final int loc = e.getValue();
int glType;
int arrayLen = 1;
boolean array = false;
if (prog.allSamplerNames.contains(name)) {
glType = GL20.GL_SAMPLER_2D;
} else {
@@ -2641,6 +2621,7 @@ public class SDLGPURenderBackend extends RenderBackend {
if (umi != null) {
glType = FormatMap.mapMemberInfoToGlType(umi);
arrayLen = Math.max(1, umi.arrayLen());
array = umi.arrayStride() > 0;
} else {
glType = GL20.GL_FLOAT_VEC4;
}
@@ -2648,7 +2629,7 @@ public class SDLGPURenderBackend extends RenderBackend {
glType = FormatMap.remapUnsignedIntToBool(glType);
}
}
result.add(new UniformEntry(name, glType, arrayLen));
result.add(new UniformEntry(array ? name + "[0]" : name, glType, arrayLen));
}
result.sort(Comparator.comparing(UniformEntry::name));
final List<UniformEntry> immutable = Collections.unmodifiableList(result);
@@ -2707,6 +2688,78 @@ public class SDLGPURenderBackend extends RenderBackend {
@Override public int getUniformLocation(int program, ByteBuffer name) {
return shaderManager.getUniformLocation(program, MemoryUtil.memASCII(name));
}
@Override public void getUniformfv(int program, int location, FloatBuffer params) {
final Readback rb = readback(program, location);
if (rb == null) return;
final int pos = params.position();
final int count = Math.min(rb.count(), params.remaining());
for (int i = 0; i < count; i++) {
final float raw = rb.raw(i);
final int bits = Float.floatToRawIntBits(raw);
params.put(pos + i, switch (rb.type()) {
case FLOAT -> raw;
case INT -> (float) bits;
case UINT -> (float) (bits & 0xFFFFFFFFL);
case BOOL -> bits != 0 ? 1f : 0f;
case OPAQUE -> rb.opaqueValue();
});
}
}
@Override public void getUniformiv(int program, int location, IntBuffer params) {
final Readback rb = readback(program, location);
if (rb == null) return;
final int pos = params.position();
final int count = Math.min(rb.count(), params.remaining());
for (int i = 0; i < count; i++) {
final float raw = rb.raw(i);
final int bits = Float.floatToRawIntBits(raw);
params.put(pos + i, switch (rb.type()) {
case FLOAT -> roundHalfAwayFromZero(raw);
case BOOL -> bits != 0 ? 1 : 0;
case INT, UINT -> bits;
case OPAQUE -> rb.opaqueValue();
});
}
}
private enum ReadType { FLOAT, INT, UINT, BOOL, OPAQUE }
private record Readback(float[] storage, int start, int count, ReadType type, int opaqueValue) {
float raw(int i) {
return storage != null && start + i < storage.length ? storage[start + i] : 0f;
}
}
private Readback readback(int program, int location) {
final ShaderManager.ProgramObject prog = shaderManager.getProgram(program);
if (prog == null || !prog.linked || location < 0 || location >= prog.uniformSlotCount) return null;
final int kind = prog.locationKind[location];
if (kind != ShaderManager.ProgramObject.LOCATION_KIND_PLAIN) {
return new Readback(null, 0, 1, ReadType.OPAQUE, opaqueUnit(prog, location, kind));
}
final ShaderManager.UniformMemberInfo info = prog.memberInfo(location);
if (info == null) return null;
final int perElement = info.vectorSize() * info.columns();
final int index = prog.arrayIndexBySlot[location];
final int base = location - index;
final float[] storage = s().uniformStaging(prog).uniformDataBySlot[base];
return new Readback(storage, index * perElement, perElement, readType(prog, base, info), 0);
}
private static ReadType readType(ShaderManager.ProgramObject prog, int base, ShaderManager.UniformMemberInfo info) {
if (info.baseType() == Spvc.SPVC_BASETYPE_FP32) return ReadType.FLOAT;
if (info.baseType() != Spvc.SPVC_BASETYPE_UINT32) return ReadType.INT;
final String name = prog.locationName[base];
return name != null && prog.boolUniforms.contains(name) ? ReadType.BOOL : ReadType.UINT;
}
private static int roundHalfAwayFromZero(float f) {
final double r = Math.floor(Math.abs((double) f) + 0.5);
return (int) (f < 0 ? -r : r);
}
private static int opaqueUnit(ShaderManager.ProgramObject prog, int location, int kind) {
final String name = prog.locationName[location];
if (name == null) return 0;
final int unit = kind == ShaderManager.ProgramObject.LOCATION_KIND_SAMPLER
? prog.samplerTextureUnits.getInt(name)
: prog.imageTextureUnits.getInt(name);
return Math.max(unit, 0);
}
@Override public void getShaderSource(int shader, IntBuffer length, ByteBuffer source) {
writeUtf8(shaderManager.getShaderSource(shader), length, source);
}
@@ -2955,7 +3008,7 @@ public class SDLGPURenderBackend extends RenderBackend {
case GL31.GL_COPY_WRITE_BUFFER -> st.boundCopyWriteBuffer = buffer;
case GL43.GL_SHADER_STORAGE_BUFFER -> {
st.boundSSBO = buffer;
resourceManager.markBufferContentsDefined(buffer);
onBufferContentsWritten(buffer);
}
}
}
@@ -3044,8 +3097,8 @@ public class SDLGPURenderBackend extends RenderBackend {
private void respecifyStore(ContextState cs, int target, int glId, long size, int usage) {
freePlainMappingFor(glId, true);
resourceManager.recordBufferGlParams(glId, usage, BufferParams.MUTABLE_STORE);
if (target == GL31.GL_UNIFORM_BUFFER) {
resourceManager.recordBufferGlParams(glId, usage, BufferParams.MUTABLE_STORE);
if (resourceManager.getBufferSize(glId) == size) return;
if (size > 0) {
resourceManager.recordBufferSizeOnly(glId, size);
@@ -3060,6 +3113,7 @@ public class SDLGPURenderBackend extends RenderBackend {
readbackShadows.release(glId);
resourceManager.deleteBuffer(glId);
resourceManager.createBuffer(glId, sdlUsage, size);
resourceManager.recordBufferGlParams(glId, usage, BufferParams.MUTABLE_STORE);
if (target == GL15.GL_ARRAY_BUFFER) cs.bumpAttribStateGen();
}
@@ -3099,8 +3153,8 @@ public class SDLGPURenderBackend extends RenderBackend {
private void respecifyStore(ContextState cs, int target, int glId, ByteBuffer data, int usage) {
final long dataSize = data.remaining();
freePlainMappingFor(glId, true);
resourceManager.recordBufferGlParams(glId, usage, BufferParams.MUTABLE_STORE);
if (target == GL31.GL_UNIFORM_BUFFER) {
resourceManager.recordBufferGlParams(glId, usage, BufferParams.MUTABLE_STORE);
if (dataSize > 0) {
resourceManager.recordBufferSizeOnly(glId, dataSize);
final ByteBuffer shadow = resourceManager.getOrAllocUboShadow(glId, (int) dataSize);
@@ -3116,6 +3170,7 @@ public class SDLGPURenderBackend extends RenderBackend {
readbackShadows.release(glId);
resourceManager.deleteBuffer(glId);
final long handle = resourceManager.createBuffer(glId, sdlUsage, dataSize);
resourceManager.recordBufferGlParams(glId, usage, BufferParams.MUTABLE_STORE);
if (target == GL15.GL_ARRAY_BUFFER) cs.bumpAttribStateGen();
if (target == GL15.GL_ELEMENT_ARRAY_BUFFER && dataSize > 0
&& (cs.primitiveRestartEnabled || resourceManager.hasEboShadow(glId))) {
@@ -3130,7 +3185,7 @@ public class SDLGPURenderBackend extends RenderBackend {
} else if (frameManager.getCommandBuffer() != 0) {
resourceManager.uploadToBuffer(frameManager.ensureCopyPass(), data, handle, 0, false);
}
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
}
}
@@ -3220,7 +3275,7 @@ public class SDLGPURenderBackend extends RenderBackend {
FrameManager.warnDroppedOutsideFrame(droppedSsboWriteWarned, "bufferSubData(GL_SHADER_STORAGE_BUFFER)");
droppedSsboWriteWarned = true;
}
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
return;
}
final boolean cycle = target == GL40.GL_DRAW_INDIRECT_BUFFER;
@@ -3229,7 +3284,7 @@ public class SDLGPURenderBackend extends RenderBackend {
} else if (frameManager.getCommandBuffer() != 0) {
resourceManager.uploadToBuffer(frameManager.ensureCopyPass(), data, handle, offset, cycle);
}
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
}
}
@@ -3294,7 +3349,7 @@ public class SDLGPURenderBackend extends RenderBackend {
} else if (frameManager.getCommandBuffer() != 0) {
resourceManager.uploadToBuffer(frameManager.ensureCopyPass(), staging, gpuHandle, m.offset, false);
}
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
}
final ByteBuffer old = resourceManager.putPboStaging(glId, staging);
if (old != null) MemoryUtil.memFree(old);
@@ -3308,7 +3363,7 @@ public class SDLGPURenderBackend extends RenderBackend {
shadow.position((int) m.offset).limit((int) (m.offset + m.length));
shadow.put(staging);
shadow.position(0).limit(shadow.capacity());
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
cs.uboRangeGen++;
}
MemoryUtil.memFree(staging);
@@ -3353,7 +3408,7 @@ public class SDLGPURenderBackend extends RenderBackend {
} else if (frameManager.getCommandBuffer() != 0) {
resourceManager.uploadToBuffer(frameManager.ensureCopyPass(), staging, gpuHandle, m.offset, false);
}
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
}
MemoryUtil.memFree(staging);
@@ -3408,6 +3463,10 @@ public class SDLGPURenderBackend extends RenderBackend {
MemoryUtil.memSet(staging, 0);
return;
}
if (readbackShadows.serve(glId, offset, staging, len)) {
staging.position(0).limit(len);
return;
}
final ByteBuffer direct = switch (target) {
case GL30.GL_PIXEL_UNPACK_BUFFER -> resourceManager.getPboStaging(glId);
@@ -3524,6 +3583,20 @@ public class SDLGPURenderBackend extends RenderBackend {
downloadBufferBlocking(handle, (int) offset, rem, data);
}
private void onBufferContentsWritten(int glId) {
resourceManager.markBufferContentsDefined(glId);
readbackShadows.invalidateAll(glId);
}
// An SSBO-bound buffer can be written by any later GPU pass, so it is never shadowed.
private static boolean isBoundAsSsbo(ContextState st, int glId) {
if (st.boundSSBO == glId) return true;
for (int i = 0; i < ContextState.MAX_INDEXED_BUFFERS; i++) {
if (st.boundSsboByIndex[i] == glId) return true;
}
return false;
}
private boolean readUboStore(int glId, long offset, ByteBuffer dst, int len) {
final ByteBuffer shadow = resourceManager.getUboShadow(glId);
if (shadow == null) return false;
@@ -4042,6 +4115,7 @@ public class SDLGPURenderBackend extends RenderBackend {
if (refuseImmutable(buffer, "glNamedBufferData on immutable storage")) return;
final int sdlUsage = FormatMap.mapBufferUsage(GL15.GL_ARRAY_BUFFER);
freePlainMappingFor(buffer, true);
readbackShadows.release(buffer);
resourceManager.deleteBuffer(buffer);
resourceManager.createBuffer(buffer, sdlUsage, size);
resourceManager.recordBufferGlParams(buffer, usage, BufferParams.MUTABLE_STORE);
@@ -4051,12 +4125,13 @@ public class SDLGPURenderBackend extends RenderBackend {
if (refuseImmutable(buffer, "glNamedBufferData on immutable storage")) return;
final int sdlUsage = FormatMap.mapBufferUsage(GL15.GL_ARRAY_BUFFER);
freePlainMappingFor(buffer, true);
readbackShadows.release(buffer);
resourceManager.deleteBuffer(buffer);
final long handle = resourceManager.createBuffer(buffer, sdlUsage, data.remaining());
resourceManager.recordBufferGlParams(buffer, usage, BufferParams.MUTABLE_STORE);
if (handle != 0 && frameManager.getCommandBuffer() != 0) {
resourceManager.uploadToBuffer(frameManager.ensureCopyPass(), data, handle, 0, false);
resourceManager.markBufferContentsDefined(buffer);
onBufferContentsWritten(buffer);
}
}
@Override public void namedBufferData(int buffer, FloatBuffer data, int usage) {
@@ -4067,7 +4142,7 @@ public class SDLGPURenderBackend extends RenderBackend {
final long handle = resourceManager.getBufferHandle(buffer);
if (handle != 0 && frameManager.getCommandBuffer() != 0) {
resourceManager.uploadToBuffer(frameManager.ensureCopyPass(), data, handle, offset, false);
resourceManager.markBufferContentsDefined(buffer);
onBufferContentsWritten(buffer);
}
}
@Override public void copyTextureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int x, int y, int width, int height) {
@@ -4119,8 +4194,8 @@ public class SDLGPURenderBackend extends RenderBackend {
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(texture);
if (meta == null) return 0;
return switch (pname) {
case GL11.GL_TEXTURE_WIDTH -> PixelOps.mipLevelSize(meta.width(), level);
case GL11.GL_TEXTURE_HEIGHT -> PixelOps.mipLevelSize(meta.height(), level);
case GL11.GL_TEXTURE_WIDTH -> TextureInfo.mipLevelSize(meta.width(), level);
case GL11.GL_TEXTURE_HEIGHT -> TextureInfo.mipLevelSize(meta.height(), level);
case GL11.GL_TEXTURE_INTERNAL_FORMAT -> {
final int stored = meta.glFormat();
yield TextureInfoCache.isGenericCompressedInternalFormat(stored) ? GL11.GL_RGBA8 : stored;
@@ -4137,6 +4212,7 @@ public class SDLGPURenderBackend extends RenderBackend {
case GL20.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS -> 32;
case GL20.GL_MAX_VERTEX_ATTRIBS -> 16;
case GL20.GL_MAX_DRAW_BUFFERS -> 8;
case GL30.GL_MAX_CLIP_DISTANCES -> device.hasClipDistance() ? GLStateManager.MAX_CLIP_PLANES : 0;
case GL30.GL_MAX_COLOR_ATTACHMENTS -> 8;
case GL15.GL_ARRAY_BUFFER_BINDING -> cs.boundArrayBuffer;
case GL15.GL_ELEMENT_ARRAY_BUFFER_BINDING -> cs.currentVao.elementBuffer;
@@ -4199,10 +4275,10 @@ public class SDLGPURenderBackend extends RenderBackend {
}
static final int MAX_UNIFORM_COMPONENTS_PER_STAGE = 4096 / Float.BYTES;
static final int MAX_ARRAY_TEXTURE_LAYERS = 256;
static final int MAX_TEXTURE_SIZE = 16384;
static final int MAX_CUBE_MAP_TEXTURE_SIZE = 16384;
static final int MAX_3D_TEXTURE_SIZE = 2048;
public static final int MAX_ARRAY_TEXTURE_LAYERS = 256;
public static final int MAX_TEXTURE_SIZE = 16384;
public static final int MAX_CUBE_MAP_TEXTURE_SIZE = 16384;
public static final int MAX_3D_TEXTURE_SIZE = 2048;
/** D3D12 and Metal both cap anisotropy at 16, and every desktop Vulkan driver reports 16. */
static final float MAX_ANISOTROPY = 16.0f;
@@ -4331,7 +4407,7 @@ public class SDLGPURenderBackend extends RenderBackend {
final int chunkBytes = (int) Math.min(size, defer ? CLEAR_CHUNK_BYTES : FILL_CHUNK_BYTES);
final int alignedChunk = (chunkBytes / elemBytes) * elemBytes;
if (alignedChunk == 0) return;
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
final boolean zero = data == null || isAllZero(data, elemBytes);
final ByteBuffer chunk = zero ? MemoryUtil.memCalloc(alignedChunk) : MemoryUtil.memAlloc(alignedChunk);
try {
@@ -4365,9 +4441,9 @@ public class SDLGPURenderBackend extends RenderBackend {
if (texHandle == 0 || frameManager.getCommandBuffer() == 0) return;
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(texture);
if (meta == null || level >= meta.levels()) return;
final int mipW = PixelOps.mipLevelSize(meta.width(), level);
final int mipH = PixelOps.mipLevelSize(meta.height(), level);
final int mipD = PixelOps.mipLevelSize(meta.depth(), level);
final int mipW = TextureInfo.mipLevelSize(meta.width(), level);
final int mipH = TextureInfo.mipLevelSize(meta.height(), level);
final int mipD = TextureInfo.mipLevelSize(meta.depth(), level);
final int bpp = PixelOps.sdlFormatTexelBytes(meta.sdlFormat());
if (mipW <= 0 || mipH <= 0 || bpp <= 0) return;
@@ -0,0 +1,74 @@
package com.gtnewhorizons.angelica.sdlgpu;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
import com.gtnewhorizons.angelica.sdlgpu.resource.ColorSource;
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import com.gtnewhorizons.angelica.sdlgpu.resource.TextureOps;
import org.lwjgl.opengl.GL11;
import static org.lwjgl.sdl.SDLGPU.*;
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_NONE;
final class SDLScreenCapture extends ScreenCapture {
private final SDLGPURenderBackend backend;
private final Device device;
private final FrameManager frameManager;
private final ResourceManager resourceManager;
private final TextureOps textureOps;
private final long[] textures = new long[RING];
private final long[] transferBuffers = new long[RING];
SDLScreenCapture(SDLGPURenderBackend backend, Device device, FrameManager frameManager, ResourceManager resourceManager, TextureOps textureOps, int width, int height) {
super(width, height);
this.backend = backend;
this.device = device;
this.frameManager = frameManager;
this.resourceManager = resourceManager;
this.textureOps = textureOps;
}
@Override
protected boolean start(int slot, int srcFramebuffer, int srcWidth, int srcHeight) {
if (frameManager.getCommandBuffer() == 0) return false;
final ColorSource source = backend.colorSourceForRead(srcFramebuffer);
if (source == null) return false;
final int w = width();
final int h = height();
if (textures[slot] == 0) {
textures[slot] = resourceManager.createScratchTexture(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, w, h, SDL_GPU_TEXTUREUSAGE_COLOR_TARGET);
}
if (transferBuffers[slot] == 0) {
transferBuffers[slot] = resourceManager.createTransferBuffer((long) w * h * 4, SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD);
if (transferBuffers[slot] == 0) return false;
}
textureOps.blitTexture(source.texture, source.level, 0, 0, Math.min(srcWidth, source.width), Math.min(srcHeight, source.height), textures[slot], 0, 0, 0, w, h, GL11.GL_LINEAR, SDL_FLIP_NONE);
final long copyPass = frameManager.ensureCopyPass();
if (copyPass == 0) return false;
resourceManager.recordTextureDownload(copyPass, textures[slot], 0, 0, 0, 0, w, h, 1, transferBuffers[slot]);
return true;
}
@Override
protected long map(int slot) {
return nSDL_MapGPUTransferBuffer(device.getDevice(), transferBuffers[slot], false);
}
@Override
protected void unmap(int slot) {
resourceManager.unmapTransferBuffer(transferBuffers[slot]);
}
@Override
protected void deleteSlot(int slot) {
resourceManager.releaseTextureDeferred(textures[slot]);
resourceManager.releaseTransferBuffer(transferBuffers[slot]);
textures[slot] = 0L;
transferBuffers[slot] = 0L;
}
}
@@ -45,6 +45,7 @@ public final class Device {
private long device;
private boolean claimed;
private boolean clipDistance = true;
private int supportedShaderFormats;
private String driverName;
private boolean fencePollEnabled;
@@ -116,6 +117,10 @@ public final class Device {
}
public boolean createDevice() {
return createDevice(!SystemProperties.EMULATE_CLIP_DISTANCES);
}
boolean createDevice(boolean requestClipDistance) {
if (device != 0) return true;
installLogCallback();
@@ -130,9 +135,9 @@ public final class Device {
}
final boolean gpuDebug = SystemProperties.LWJGL_DEBUG || SystemProperties.SDL_GPU_DEBUG;
device = createGPUDevice(gpuDebug, candidates);
device = createGPUDevice(gpuDebug, candidates, requestClipDistance);
if (device == 0 && VideoDriverRecovery.shouldRetryOnX11(SDL_GetCurrentVideoDriver(), vulkanLoaderAvailable(), System.getenv("DISPLAY"))) {
device = VideoDriverRecovery.retryUnderX11(() -> createGPUDevice(gpuDebug, candidates));
device = VideoDriverRecovery.retryUnderX11(() -> createGPUDevice(gpuDebug, candidates, requestClipDistance));
}
if (device == 0) {
logDeviceCreationFailure(REQUESTED_SHADER_FORMATS);
@@ -206,13 +211,22 @@ public final class Device {
}
}
private long createGPUDevice(boolean gpuDebug, String[] candidates) {
private long createGPUDevice(boolean gpuDebug, String[] candidates, boolean requestClipDistance) {
final boolean macos = Platform.get() == Platform.MACOSX;
for (final String driver : candidates) {
final boolean macVulkan = macos && isVulkan(driver);
if (macVulkan) useMoltenVk();
final long dev = SDL_CreateGPUDevice(REQUESTED_SHADER_FORMATS, gpuDebug, driver);
if (dev != 0) return dev;
long dev = createWithProperties(gpuDebug, driver, requestClipDistance);
boolean clip = requestClipDistance;
if (dev == 0 && clip && isVulkan(driver)) {
LOG.info("SDL GPU Vulkan device creation failed with shaderClipDistance ({}); the device may lack it, retrying without it, clip distances will be emulated", SDLError.SDL_GetError());
dev = createWithProperties(gpuDebug, driver, false);
clip = false;
}
if (dev != 0) {
clipDistance = clip;
return dev;
}
LOG.warn("Failed to create a '{}' SDL GPU device: {}", driver, SDLError.SDL_GetError());
if (macVulkan && MoltenVK.locate() == null) {
LOG.warn("Vulkan on macOS needs MoltenVK. Add {} to the mods folder or install MoltenVK.", MoltenVK.JAR_NAME);
@@ -221,6 +235,22 @@ public final class Device {
return 0;
}
private static long createWithProperties(boolean gpuDebug, String driver, boolean clipDistance) {
final int props = SDLProperties.SDL_CreateProperties();
try {
SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_SPIRV_BOOLEAN, (REQUESTED_SHADER_FORMATS & SDL_GPU_SHADERFORMAT_SPIRV) != 0);
SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_DXBC_BOOLEAN, (REQUESTED_SHADER_FORMATS & SDL_GPU_SHADERFORMAT_DXBC) != 0);
SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_DXIL_BOOLEAN, (REQUESTED_SHADER_FORMATS & SDL_GPU_SHADERFORMAT_DXIL) != 0);
SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_MSL_BOOLEAN, (REQUESTED_SHADER_FORMATS & SDL_GPU_SHADERFORMAT_MSL) != 0);
SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_DEBUGMODE_BOOLEAN, gpuDebug);
SDLProperties.SDL_SetStringProperty(props, SDL_PROP_GPU_DEVICE_CREATE_NAME_STRING, driver);
SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_FEATURE_CLIP_DISTANCE_BOOLEAN, clipDistance);
return SDL_CreateGPUDeviceWithProperties(props);
} finally {
SDLProperties.SDL_DestroyProperties(props);
}
}
public void claimWindow(long window) {
if (device == 0) {
throw new IllegalStateException("SDL GPU device not created");
@@ -298,6 +328,7 @@ public final class Device {
driverVersion = null;
driverInfo = null;
supportedShaderFormats = 0;
clipDistance = true;
}
private void logWindowDiagnostics(long window) {
@@ -351,6 +382,10 @@ public final class Device {
return device;
}
public boolean hasClipDistance() {
return clipDistance;
}
public String getDriverName() {
return driverName;
}
@@ -1,14 +1,17 @@
package com.gtnewhorizons.angelica.sdlgpu.frame;
import com.gtnewhorizons.angelica.glsm.GLContextState;
import com.gtnewhorizons.angelica.glsm.texture.ProxyTexture;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineCache;
import com.gtnewhorizons.angelica.sdlgpu.resource.ColorSource;
import com.gtnewhorizons.angelica.sdlgpu.resource.MappedRange;
import com.gtnewhorizons.angelica.sdlgpu.resource.PackState;
import com.gtnewhorizons.angelica.sdlgpu.shader.LogicOpVariant;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import com.gtnewhorizons.angelica.sdlgpu.shader.UniformStaging;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.Long2FloatOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
@@ -64,28 +67,10 @@ public final class ContextState {
public final PixelStoreState pixelStore = new PixelStoreState();
public final PackState packState = new PackState();
public final ColorSource readColor = new ColorSource();
public static final class ProxyTextureState {
public int target;
public int width;
public int height;
public int depth;
public int internalFormat;
public void accept(int target, int width, int height, int depth, int internalFormat) {
this.target = target;
this.width = width;
this.height = height;
this.depth = depth;
this.internalFormat = internalFormat;
}
public void reject(int target) {
accept(target, 0, 0, 0, 0);
}
}
public final ProxyTextureState proxyTexture = new ProxyTextureState();
public final ProxyTexture proxyTexture = new ProxyTexture(SDLGPURenderBackend.MAX_TEXTURE_SIZE, SDLGPURenderBackend.MAX_3D_TEXTURE_SIZE,
SDLGPURenderBackend.MAX_CUBE_MAP_TEXTURE_SIZE, SDLGPURenderBackend.MAX_ARRAY_TEXTURE_LAYERS);
public static final class VAOState {
public final boolean[] attribEnabled = new boolean[MAX_VERTEX_ATTRIBS];
@@ -199,6 +184,7 @@ public final class ContextState {
private final Reference2ObjectOpenHashMap<ShaderManager.ProgramObject, UniformStaging> uniformStagingByProgram = new Reference2ObjectOpenHashMap<>();
private ShaderManager.ProgramObject stagingProg;
private UniformStaging currentUniformStaging;
public final Int2ObjectOpenHashMap<float[]> uniformArrayScratch = new Int2ObjectOpenHashMap<>();
public UniformStaging uniformStaging(ShaderManager.ProgramObject prog) {
if (prog == stagingProg) return currentUniformStaging;
@@ -64,16 +64,20 @@ public final class FenceTracker {
if (pendingFenceSyncs.isEmpty()) return;
final long fence = frameManager.frame().lastAcquiredFence;
frameManager.frame().lastAcquiredFence = 0;
boolean fenceTaken = false;
for (int i = 0; i < pendingFenceSyncs.size(); i++) {
final long id = pendingFenceSyncs.getLong(i);
if (!fenceMap.containsKey(id)) continue;
if (fence != 0) {
fenceMap.put(id, fence);
fenceRefcounts.addTo(fence, 1);
fenceTaken = true;
} else {
fenceMap.remove(id);
}
}
pendingFenceSyncs.clear();
if (fence != 0 && !fenceTaken && device.getDevice() != 0) device.fenceReleaser().release(fence);
}
public int clientWaitSync(long sync, int flags, long timeout) {
@@ -1,10 +1,10 @@
package com.gtnewhorizons.angelica.sdlgpu.pipeline;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager.FrameState;
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
import com.gtnewhorizons.angelica.sdlgpu.resource.PixelOps;
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import com.gtnewhorizons.angelica.sdlgpu.resource.TextureOps;
import com.gtnewhorizons.angelica.sdlgpu.shader.LogicOpFormats;
@@ -121,13 +121,13 @@ public final class LogicOpEmulator {
if (fbo == null) return frameManager.getFbo0Width();
final int db = drawBuffer(fbo, loc);
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(fbo.colorGlIds[db]);
return meta != null ? PixelOps.mipLevelSize(meta.width(), fbo.colorLevels[db]) : fbo.width;
return meta != null ? TextureInfo.mipLevelSize(meta.width(), fbo.colorLevels[db]) : fbo.width;
}
private int targetHeight(FboState fbo, int loc) {
if (fbo == null) return frameManager.getFbo0Height();
final int db = drawBuffer(fbo, loc);
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(fbo.colorGlIds[db]);
return meta != null ? PixelOps.mipLevelSize(meta.height(), fbo.colorLevels[db]) : fbo.height;
return meta != null ? TextureInfo.mipLevelSize(meta.height(), fbo.colorLevels[db]) : fbo.height;
}
}
@@ -754,16 +754,32 @@ public final class PipelineApplier {
public float[] reuseOrAlloc(ContextState st, int location, int length) {
final ShaderManager.ProgramObject prog = st.boundProgramObj;
if (prog == null || location < 0 || location >= prog.uniformSlotCount) return null;
if (prog.floatLimitBySlot[location] < 0) return arrayScratch(st, length);
final float[] existing = st.uniformStaging(prog).uniformDataBySlot[location];
if (existing != null && existing.length == length) return existing;
return new float[length];
}
private static float[] arrayScratch(ContextState st, int length) {
float[] scratch = st.uniformArrayScratch.get(length);
if (scratch == null) {
scratch = new float[length];
st.uniformArrayScratch.put(length, scratch);
}
return scratch;
}
public void putUniformFv(ContextState st, int location, FloatBuffer value) {
final ShaderManager.ProgramObject prog = st.boundProgramObj;
if (prog == null || location < 0 || location >= prog.uniformSlotCount) return;
final int n = value.remaining();
final int pos = value.position();
if (prog.floatLimitBySlot[location] < 0) {
final float[] v = arrayScratch(st, n);
value.get(pos, v);
putUniform(st, location, v);
return;
}
final float[] existing = st.uniformStaging(prog).uniformDataBySlot[location];
if (existing != null && existing.length == n) {
if (contentEquals(existing, value, pos, n)) {
@@ -824,6 +840,7 @@ public final class PipelineApplier {
if (Tracy.ENABLED) frameManager.noteUniformBlockFlush(b, size);
}
private static final long ARRAY_HASH_SEED = 0x9E3779B97F4A7C15L;
private static final boolean VERIFY_PER_FRAME_BLOCK = SystemProperties.SDL_VERIFY_PER_FRAME_UNIFORM_BLOCK;
private final HashSet<String> perFrameBlockDriftWarned = new HashSet<>();
@@ -837,6 +854,12 @@ public final class PipelineApplier {
public void putUniform(ContextState st, int location, float[] data) {
final ShaderManager.ProgramObject prog = st.boundProgramObj;
if (prog == null || location < 0 || location >= prog.uniformSlotCount) return;
final int limit = prog.floatLimitBySlot[location];
if (limit < 0) {
putArrayElement(st, prog, st.uniformStaging(prog), location - prog.arrayIndexBySlot[location], location, data);
return;
}
if (data.length > limit) return;
final UniformStaging us = st.uniformStaging(prog);
final int len = data.length;
final long valueKey;
@@ -884,6 +907,62 @@ public final class PipelineApplier {
}
return;
}
writeStages(st, prog, us, location, data);
}
private void putArrayElement(ContextState st, ShaderManager.ProgramObject prog, UniformStaging us, int base, int location, float[] data) {
final ShaderManager.UniformMemberInfo baseInfo = prog.memberInfo(base);
if (baseInfo == null) return;
final int len = prog.arrayLenBySlot[location];
final int index = prog.arrayIndexBySlot[location];
final int count = Math.min(elementCount(baseInfo, data), len - index);
if (count <= 0) return;
final int floatsPerElement = baseInfo.vectorSize() * baseInfo.columns();
if (Tracy.ENABLED) st.slotWrites++;
final int start = index * floatsPerElement;
final int floats = count * floatsPerElement;
float[] storage = us.uniformDataBySlot[base];
final boolean fresh = storage == null;
long storageHash;
if (fresh) {
storage = us.uniformDataBySlot[base] = new float[len * floatsPerElement];
storageHash = ARRAY_HASH_SEED;
} else {
storageHash = us.uniformValueHashBySlot[base];
}
boolean changed = fresh;
for (int i = 0; i < floats; i++) {
final int oldBits = Float.floatToRawIntBits(storage[start + i]);
final int newBits = Float.floatToRawIntBits(data[i]);
if (oldBits == newBits) continue;
storageHash ^= arrayHashTerm(start + i, oldBits) ^ arrayHashTerm(start + i, newBits);
storage[start + i] = data[i];
changed = true;
}
if (!changed) {
if (Tracy.ENABLED) st.slotWritesElided++;
return;
}
us.uniformValueHashBySlot[base] = storageHash;
for (int b = 0; b < ShaderManager.BLOCK_COUNT; b++) {
final ShaderManager.UniformMemberInfo info = prog.blockInfoBySlot[b][location];
if (info == null) continue;
final ContextState.UniformBlockState block = st.uniformBlocks[b];
if (prog.blockSize[b] > block.size) block.size = prog.blockSize[b];
final FloatBuffer blockFb = block.staging(prog.blockSize[b]);
final int hashSlot = block.hashSlot(prog.blockInfoBySlot[b][base].offset());
if (hashSlot >= 0) block.setHash(hashSlot, storageHash);
writeMember(st, prog, blockFb, prog.blockSize[b], info, data, location, false);
block.dirty = true;
if (VERIFY_PER_FRAME_BLOCK && b == ShaderManager.BLOCK_PER_FRAME && block.flushedThisFrame) {
notePerFrameBlockDrift(prog, base);
}
return;
}
writeStages(st, prog, us, location, data);
}
private void writeStages(ContextState st, ShaderManager.ProgramObject prog, UniformStaging us, int location, float[] data) {
final ShaderManager.UniformMemberInfo vs = prog.vsInfoBySlot[location];
if (vs != null && us.vsUniformFb != null) {
writeMember(st, prog, us.vsUniformFb, prog.vertexUboSize, vs, data, location, true);
@@ -896,6 +975,10 @@ public final class PipelineApplier {
}
}
private static long arrayHashTerm(int index, int bits) {
return bits == 0 ? 0L : Hashing.fmix64(ARRAY_HASH_SEED, Hashing.packHiLo(index, bits));
}
public void storeMatrix(ContextState st, int location, boolean transpose, FloatBuffer value, int size) {
if (location < 0) return;
final int n = value.remaining();
@@ -914,16 +997,19 @@ public final class PipelineApplier {
putUniform(st, location, out);
}
private void writeMember(ContextState st, ShaderManager.ProgramObject prog, FloatBuffer ubo, int uboSize, ShaderManager.UniformMemberInfo info, float[] data, int loc, boolean isVs) {
private static int elementCount(ShaderManager.UniformMemberInfo info, float[] data) {
final int floatsPerElement = info.vectorSize() * info.columns();
if (floatsPerElement <= 0) return;
final int srcCount = data.length / floatsPerElement;
if (srcCount <= 0) return;
final int dataBytes = srcCount * floatsPerElement * 4;
return floatsPerElement <= 0 ? 0 : data.length / floatsPerElement;
}
private void writeMember(ContextState st, ShaderManager.ProgramObject prog, FloatBuffer ubo, int uboSize, ShaderManager.UniformMemberInfo info, float[] data, int loc, boolean isVs) {
final int count = Math.min(elementCount(info, data), info.arrayLen());
if (count <= 0) return;
final int dataBytes = count * info.vectorSize() * info.columns() * 4;
final int avail = uboSize - info.offset();
if (dataBytes > avail) {
throw new IllegalStateException("Uniform overflows UBO: prog=" + st.boundProgram + " loc=" + loc + " name='" + prog.locationToName.getOrDefault(loc, "?") + "' dataBytes=" + dataBytes + " availFromOffset=" + avail + " stage=" + (isVs ? "VS" : "FS"));
}
Std140Writer.write(ubo, info, data, srcCount);
Std140Writer.write(ubo, info, data, count);
}
}
@@ -0,0 +1,10 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
public final class ColorSource {
public long texture;
public int level;
public int width;
public int height;
public int sdlFormat;
public int glBaseFormat;
}
@@ -14,12 +14,6 @@ import static org.lwjgl.sdl.SDLGPU.*;
public final class PixelOps {
private PixelOps() {}
public static int mipLevelSize(int base, int level) {
if (base <= 0 || level < 0) return 0;
if (level >= Integer.SIZE - 1) return 1;
return Math.max(1, base >> level);
}
public static int defaultMipLevels(int width, int height) {
final int max = Math.max(1, Math.max(width, height));
return Math.max(1, 32 - Integer.numberOfLeadingZeros(max));
@@ -1,16 +1,16 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import com.gtnewhorizons.angelica.sdlgpu.frame.FenceTracker;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.sdl.SDLError;
import org.lwjgl.sdl.SDL_GPUBufferRegion;
import org.lwjgl.sdl.SDL_GPUTransferBufferCreateInfo;
import org.lwjgl.sdl.SDL_GPUTransferBufferLocation;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.opengl.GL32;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
@@ -25,25 +25,27 @@ public final class ReadbackShadows {
private static final class Slot {
long transferBuffer;
int capacity;
int validLen;
boolean valid;
long sync;
}
private final Device device;
private final Long2ObjectOpenHashMap<Slot> slots = new Long2ObjectOpenHashMap<>();
private final ResourceManager resourceManager;
private final FenceTracker fenceTracker;
private final Int2ObjectOpenHashMap<Long2ObjectOpenHashMap<Slot>> byBuffer = new Int2ObjectOpenHashMap<>();
public ReadbackShadows(Device device) {
public ReadbackShadows(Device device, ResourceManager resourceManager, FenceTracker fenceTracker) {
this.device = device;
this.resourceManager = resourceManager;
this.fenceTracker = fenceTracker;
}
private static long key(int glId, long offset) {
return ((long) glId << 32) | (offset & 0xFFFFFFFFL);
}
public boolean scheduleDownload(long copyPass, long srcHandle, long srcOffset, int dstGlId, long dstOffset, long size) {
if (copyPass == 0 || srcHandle == 0 || size <= 0 || size > Integer.MAX_VALUE) return false;
public void scheduleDownload(long copyPass, long srcHandle, long srcOffset, int dstGlId, long dstOffset, long size) {
if (copyPass == 0 || srcHandle == 0 || size <= 0 || size > Integer.MAX_VALUE) return;
final Slot slot = ensureSlot(dstGlId, dstOffset, (int) size);
if (slot == null) return false;
if (slot == null) return;
try (MemoryStack stack = stackPush()) {
final SDL_GPUBufferRegion src = SDL_GPUBufferRegion.calloc(stack)
@@ -55,18 +57,32 @@ public final class ReadbackShadows {
.offset(0);
SDL_DownloadFromGPUBuffer(copyPass, src, dst);
}
dropSync(slot);
slot.sync = fenceTracker.fenceSync();
slot.validLen = (int) size;
slot.valid = true;
return true;
}
public void invalidate(int glId, long offset) {
final Slot slot = slots.get(key(glId, offset));
if (slot != null) slot.valid = false;
public void invalidateAll(int glId) {
if (glId == 0) return;
final Long2ObjectOpenHashMap<Slot> slots = byBuffer.get(glId);
if (slots == null) return;
for (Slot slot : slots.values()) {
slot.valid = false;
dropSync(slot);
}
}
public boolean serve(int glId, long offset, ByteBuffer out, int len) {
final Slot slot = slots.get(key(glId, offset));
if (slot == null || !slot.valid || slot.transferBuffer == 0 || slot.capacity < len) return false;
final Long2ObjectOpenHashMap<Slot> slots = byBuffer.get(glId);
final Slot slot = slots != null ? slots.get(offset) : null;
if (slot == null || !slot.valid || slot.transferBuffer == 0 || slot.validLen < len) return false;
if (slot.sync != 0) {
final int wait = fenceTracker.clientWaitSync(slot.sync, GL32.GL_SYNC_FLUSH_COMMANDS_BIT, Long.MAX_VALUE);
if (wait != GL32.GL_ALREADY_SIGNALED && wait != GL32.GL_CONDITION_SATISFIED) return false;
dropSync(slot);
}
final long mapped = nSDL_MapGPUTransferBuffer(device.getDevice(), slot.transferBuffer, false);
if (mapped == 0) {
@@ -80,62 +96,63 @@ public final class ReadbackShadows {
}
public boolean hasSlotsFor(int glId) {
for (long k : slots.keySet()) {
if ((int) (k >>> 32) == glId) return true;
}
return false;
return byBuffer.containsKey(glId);
}
public void release(int glId) {
if (slots.isEmpty()) return;
final ObjectIterator<Long2ObjectMap.Entry<Slot>> it = slots.long2ObjectEntrySet().fastIterator();
while (it.hasNext()) {
final Long2ObjectMap.Entry<Slot> entry = it.next();
if ((int) (entry.getLongKey() >>> 32) == glId) {
releaseSlot(entry.getValue());
it.remove();
}
}
final Long2ObjectOpenHashMap<Slot> slots = byBuffer.remove(glId);
if (slots == null) return;
for (Slot slot : slots.values()) releaseSlot(slot);
}
public void dispose() {
for (Slot slot : slots.values()) releaseSlot(slot);
slots.clear();
for (Long2ObjectOpenHashMap<Slot> slots : byBuffer.values()) {
for (Slot slot : slots.values()) releaseSlot(slot);
}
byBuffer.clear();
}
private Slot ensureSlot(int glId, long offset, int size) {
final long k = key(glId, offset);
Slot slot = slots.get(k);
Long2ObjectOpenHashMap<Slot> slots = byBuffer.get(glId);
Slot slot = slots != null ? slots.get(offset) : null;
if (slot != null && slot.capacity >= size) return slot;
if (slot == null) {
if (slots == null) {
slots = new Long2ObjectOpenHashMap<>();
byBuffer.put(glId, slots);
}
slot = new Slot();
slots.put(k, slot);
slots.put(offset, slot);
} else {
releaseSlot(slot);
}
try (MemoryStack stack = stackPush()) {
final SDL_GPUTransferBufferCreateInfo ci = SDL_GPUTransferBufferCreateInfo.calloc(stack)
.usage(SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD)
.size(size);
slot.transferBuffer = SDL_CreateGPUTransferBuffer(device.getDevice(), ci);
}
slot.transferBuffer = resourceManager.createTransferBuffer(size, SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD);
if (slot.transferBuffer == 0) {
LOG.error("Failed to create readback transfer buffer size={}: {}", size, SDLError.SDL_GetError());
slots.remove(k);
slots.remove(offset);
if (slots.isEmpty()) byBuffer.remove(glId);
return null;
}
slot.capacity = size;
slot.validLen = 0;
slot.valid = false;
return slot;
}
private void dropSync(Slot slot) {
if (slot.sync == 0) return;
fenceTracker.deleteSync(slot.sync);
slot.sync = 0;
}
private void releaseSlot(Slot slot) {
dropSync(slot);
if (slot.transferBuffer != 0 && device.getDevice() != 0) {
SDL_ReleaseGPUTransferBuffer(device.getDevice(), slot.transferBuffer);
resourceManager.releaseTransferBuffer(slot.transferBuffer);
}
slot.transferBuffer = 0;
slot.capacity = 0;
slot.validLen = 0;
}
}
@@ -1572,9 +1572,13 @@ public final class ResourceManager {
}
public long createTransferBuffer(long size) {
return createTransferBuffer(size, SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD);
}
public long createTransferBuffer(long size, int usage) {
try (var stack = stackPush()) {
final SDL_GPUTransferBufferCreateInfo ci = SDL_GPUTransferBufferCreateInfo.calloc(stack)
.usage(SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD)
.usage(usage)
.size((int) size);
final long handle = SDL_CreateGPUTransferBuffer(device.getDevice(), ci);
@@ -2067,23 +2071,42 @@ public final class ResourceManager {
public void downloadFromTexture(long commandBuffer, long gpuTexture, int x, int y, int z, int w, int h, int d, int level, ByteBuffer output) {
final int size = output.remaining();
final long xfer = createTransferBuffer(size, SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD);
if (xfer == 0) return;
final long copyPass = SDL_BeginGPUCopyPass(commandBuffer);
if (copyPass == 0) {
LOG.error("Failed to begin copy pass for texture download: {}", SDLError.SDL_GetError());
releaseTransferBuffer(xfer);
return;
}
recordTextureDownload(copyPass, gpuTexture, level, x, y, z, w, h, d, xfer);
SDL_EndGPUCopyPass(copyPass);
final long fence = Submits.submitAndAcquireFence(commandBuffer);
if (fence == 0) {
LOG.error("Failed to submit download command buffer: {}", SDLError.SDL_GetError());
releaseTransferBuffer(xfer);
return;
}
try {
FenceWait.await(device, fence, FenceWait.FOREVER);
} finally {
device.fenceReleaser().release(fence);
}
final long mappedPtr = nSDL_MapGPUTransferBuffer(device.getDevice(), xfer, false);
if (mappedPtr != 0) {
final ByteBuffer mapped = MemoryUtil.memByteBuffer(mappedPtr, size);
output.put(mapped);
}
SDL_UnmapGPUTransferBuffer(device.getDevice(), xfer);
releaseTransferBuffer(xfer);
}
public void recordTextureDownload(long copyPass, long gpuTexture, int level, int x, int y, int z, int w, int h, int d, long xfer) {
try (var stack = stackPush()) {
final SDL_GPUTransferBufferCreateInfo xferCi = SDL_GPUTransferBufferCreateInfo.calloc(stack)
.usage(SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD)
.size(size);
final long xfer = SDL_CreateGPUTransferBuffer(device.getDevice(), xferCi);
if (xfer == 0) {
LOG.error("Failed to create download transfer buffer: {}", SDLError.SDL_GetError());
return;
}
final long copyPass = SDL_BeginGPUCopyPass(commandBuffer);
if (copyPass == 0) {
LOG.error("Failed to begin copy pass for texture download: {}", SDLError.SDL_GetError());
SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer);
return;
}
final SDL_GPUTextureRegion src = SDL_GPUTextureRegion.calloc(stack)
.texture(gpuTexture)
.mip_level(level)
@@ -2095,45 +2118,18 @@ public final class ResourceManager {
.offset(0);
SDL_DownloadFromGPUTexture(copyPass, src, dst);
SDL_EndGPUCopyPass(copyPass);
final long fence = Submits.submitAndAcquireFence(commandBuffer);
if (fence == 0) {
LOG.error("Failed to submit download command buffer: {}", SDLError.SDL_GetError());
SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer);
return;
}
try {
FenceWait.await(device, fence, FenceWait.FOREVER);
} finally {
device.fenceReleaser().release(fence);
}
final long mappedPtr = nSDL_MapGPUTransferBuffer(device.getDevice(), xfer, false);
if (mappedPtr != 0) {
final ByteBuffer mapped = MemoryUtil.memByteBuffer(mappedPtr, size);
output.put(mapped);
}
SDL_UnmapGPUTransferBuffer(device.getDevice(), xfer);
SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer);
}
}
public void downloadFromBuffer(long commandBuffer, long gpuBuffer, int offset, int size, ByteBuffer output) {
try (var stack = stackPush()) {
final SDL_GPUTransferBufferCreateInfo xferCi = SDL_GPUTransferBufferCreateInfo.calloc(stack)
.usage(SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD)
.size(size);
final long xfer = SDL_CreateGPUTransferBuffer(device.getDevice(), xferCi);
if (xfer == 0) {
LOG.error("Failed to create download transfer buffer for buffer readback: {}", SDLError.SDL_GetError());
return;
}
final long xfer = createTransferBuffer(size, SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD);
if (xfer == 0) return;
final long copyPass = SDL_BeginGPUCopyPass(commandBuffer);
if (copyPass == 0) {
LOG.error("Failed to begin copy pass for buffer download: {}", SDLError.SDL_GetError());
SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer);
releaseTransferBuffer(xfer);
return;
}
@@ -2152,7 +2148,7 @@ public final class ResourceManager {
final long fence = Submits.submitAndAcquireFence(commandBuffer);
if (fence == 0) {
LOG.error("Failed to submit buffer download command buffer: {}", SDLError.SDL_GetError());
SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer);
releaseTransferBuffer(xfer);
return;
}
try {
@@ -2167,7 +2163,7 @@ public final class ResourceManager {
output.put(mapped);
}
SDL_UnmapGPUTransferBuffer(device.getDevice(), xfer);
SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer);
releaseTransferBuffer(xfer);
}
}
private long defaultSampler;
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import com.gtnewhorizons.angelica.sdlgpu.device.Submits;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
@@ -156,11 +157,11 @@ public final class TextureOps {
}
public static int levelWidth(ResourceManager.TextureMeta meta, int level) {
return meta != null ? PixelOps.mipLevelSize(meta.width(), level) : 0;
return meta != null ? TextureInfo.mipLevelSize(meta.width(), level) : 0;
}
public static int levelHeight(ResourceManager.TextureMeta meta, int level) {
return meta != null ? PixelOps.mipLevelSize(meta.height(), level) : 0;
return meta != null ? TextureInfo.mipLevelSize(meta.height(), level) : 0;
}
private void copyFromDefaultFramebuffer(ContextState st, int destGlId, long destTex, int level, int xoffset, int yoffset, int x, int y, int width, int height) {
@@ -777,7 +777,7 @@ public final class ShaderManager {
else prog.fragmentUboSize = refl.uboSize();
}
for (UboMember m : refl.uboMembers()) {
final int loc = ensureLocation(prog, m.name());
final int loc = ensureLocation(prog, m.name(), m.arrayLen());
final UniformMemberInfo info = new UniformMemberInfo(
m.offset(), m.size(), m.arrayStride(), isVertex,
m.vectorSize(), m.columns(), m.baseType(), m.arrayLen()
@@ -795,7 +795,7 @@ public final class ShaderManager {
prog.blockSize[b] = Math.max(prog.blockSize[b], block.size());
prog.blockBinding[b] = block.binding();
for (UboMember m : block.members()) {
final int loc = ensureLocation(prog, m.name());
final int loc = ensureLocation(prog, m.name(), m.arrayLen());
if (prog.blockMemberInfo[b].containsKey(loc)) continue;
prog.blockMemberInfo[b].put(loc, new UniformMemberInfo(
m.offset(), m.size(), m.arrayStride(), false,
@@ -809,14 +809,34 @@ public final class ShaderManager {
}
private static int ensureLocation(ProgramObject prog, String name) {
return ensureLocation(prog, name, 1);
}
private static int ensureLocation(ProgramObject prog, String name, int arrayLen) {
final int existing = prog.nameToLocation.getInt(name);
if (existing != -1) return existing;
final int loc = prog.nextUniformLocation++;
if (existing != -1) {
final int registered = Math.max(1, prog.arrayLenByLocation.get(existing));
if (registered == arrayLen) return existing;
retireLocations(prog, existing, registered);
}
final int loc = prog.nextUniformLocation;
prog.nextUniformLocation += arrayLen;
prog.nameToLocation.put(name, loc);
prog.locationToName.put(loc, name);
for (int i = 1; i < arrayLen; i++) prog.locationToName.put(loc + i, name + "[" + i + "]");
if (arrayLen > 1) prog.arrayLenByLocation.put(loc, arrayLen);
return loc;
}
private static void retireLocations(ProgramObject prog, int base, int len) {
prog.arrayLenByLocation.remove(base);
for (int i = 0; i < len; i++) prog.locationToName.remove(base + i);
prog.locationToMemberInfo.remove(base);
prog.vertexMemberInfo.remove(base);
prog.fragmentMemberInfo.remove(base);
for (int b = 0; b < BLOCK_COUNT; b++) prog.blockMemberInfo[b].remove(base);
}
private static void applyVaryingMatch(ByteBuffer fragmentSpirv, StageReflection vs, StageReflection fs) {
if (vs.vsOutputs().isEmpty() || fs.fsInputs().isEmpty() || fragmentSpirv == null) return;
final HashMap<String, Integer> vsOutLocs = new HashMap<>(vs.vsOutputs().size() * 2);
@@ -1268,7 +1288,13 @@ public final class ShaderManager {
public int getUniformLocation(int program, String name) {
final ProgramObject prog = programObjects.get(program);
if (prog == null || !prog.linked) return -1;
return prog.nameToLocation.getInt(name);
final int exact = prog.nameToLocation.getInt(name);
if (exact != -1) return exact;
final int index = UniformNames.arrayIndex(name);
if (index < 0) return -1;
final int base = prog.nameToLocation.getInt(name.substring(0, name.lastIndexOf('[')));
if (base < 0 || base >= prog.uniformSlotCount || index >= prog.arrayLenBySlot[base]) return -1;
return base + index;
}
/**
@@ -1645,7 +1671,12 @@ public final class ShaderManager {
public record UniformMemberInfo(
int offset, int size, int arrayStride, boolean isVertex,
int vectorSize, int columns, int baseType, int arrayLen
) {}
) {
public UniformMemberInfo element(int i) {
return new UniformMemberInfo(offset + i * arrayStride, size - i * arrayStride, arrayStride, isVertex,
vectorSize, columns, baseType, arrayLen - i);
}
}
public record UboMember(
String name, int offset, int size, int arrayStride,
@@ -2069,11 +2100,24 @@ public final class ShaderManager {
return m != null ? m : blockMemberInfo[BLOCK_PER_PASS].get(loc);
}
public UniformMemberInfo memberInfo(int slot) {
for (int b = 0; b < BLOCK_COUNT; b++) {
final UniformMemberInfo info = blockInfoBySlot[b][slot];
if (info != null) return info;
}
final UniformMemberInfo vs = vsInfoBySlot[slot];
return vs != null ? vs : fsInfoBySlot[slot];
}
private static final UniformMemberInfo[] EMPTY_INFO = new UniformMemberInfo[0];
public UniformMemberInfo[] vsInfoBySlot;
public UniformMemberInfo[] fsInfoBySlot;
public int uniformSlotCount;
public final Int2IntOpenHashMap arrayLenByLocation = new Int2IntOpenHashMap();
public int[] arrayIndexBySlot = EMPTY_UNITS;
public int[] arrayLenBySlot = EMPTY_UNITS;
public int[] floatLimitBySlot = EMPTY_UNITS;
public final int[] vertexInputBaseType = new int[16];
public final int[] vertexInputVecSize = new int[16];
public final String[] vertexInputName = new String[16];
@@ -2200,24 +2244,51 @@ public final class ShaderManager {
locationKind[loc] = LOCATION_KIND_PLAIN;
}
}
arrayIndexBySlot = new int[n];
arrayLenBySlot = new int[n];
Arrays.fill(arrayLenBySlot, 1);
for (var it = arrayLenByLocation.int2IntEntrySet().fastIterator(); it.hasNext(); ) {
final var e = it.next();
final int base = e.getIntKey();
final int len = e.getIntValue();
for (int i = 0; i < len; i++) {
arrayLenBySlot[base + i] = len;
arrayIndexBySlot[base + i] = i;
}
}
for (int b = 0; b < BLOCK_COUNT; b++) {
blockInfoBySlot[b] = new UniformMemberInfo[n];
for (var it = blockMemberInfo[b].int2ObjectEntrySet().fastIterator(); it.hasNext(); ) {
final var e = it.next();
final int k = e.getIntKey();
if (k >= 0 && k < n) blockInfoBySlot[b][k] = e.getValue();
fillSlots(blockInfoBySlot[b], e.getIntKey(), e.getValue());
}
}
for (var it = vertexMemberInfo.int2ObjectEntrySet().fastIterator(); it.hasNext(); ) {
final var e = it.next();
final int k = e.getIntKey();
if (k >= 0 && k < n) vsInfoBySlot[k] = e.getValue();
fillSlots(vsInfoBySlot, e.getIntKey(), e.getValue());
}
for (var it = fragmentMemberInfo.int2ObjectEntrySet().fastIterator(); it.hasNext(); ) {
final var e = it.next();
final int k = e.getIntKey();
if (k >= 0 && k < n) fsInfoBySlot[k] = e.getValue();
fillSlots(fsInfoBySlot, e.getIntKey(), e.getValue());
}
floatLimitBySlot = new int[n];
for (int slot = 0; slot < n; slot++) {
if (arrayLenBySlot[slot] > 1) {
floatLimitBySlot[slot] = -1;
continue;
}
final UniformMemberInfo info = memberInfo(slot);
final int floats = info == null || info.arrayStride() > 0 ? 0 : info.vectorSize() * info.columns();
floatLimitBySlot[slot] = floats > 0 ? floats : Integer.MAX_VALUE;
}
}
private void fillSlots(UniformMemberInfo[] bySlot, int base, UniformMemberInfo info) {
final int n = bySlot.length;
if (base < 0 || base >= n) return;
bySlot[base] = info;
final int len = arrayLenBySlot[base];
for (int i = 1; i < len; i++) bySlot[base + i] = info.element(i);
}
}
}
@@ -0,0 +1,25 @@
package com.gtnewhorizons.angelica.sdlgpu.shader;
public final class UniformNames {
private UniformNames() {}
public static int arrayIndex(String name) {
final int len = name.length();
if (len == 0 || name.charAt(len - 1) != ']') return -1;
int i = len - 1;
while (i > 0 && isDigit(name.charAt(i - 1))) i--;
if (i == 0 || name.charAt(i - 1) != '[') return -1;
if (name.charAt(i) == '0' && i + 1 != len - 1) return -1;
long index = 0;
for (int c = i; c < len - 1; c++) {
index = index * 10 + (name.charAt(c) - '0');
if (index > Integer.MAX_VALUE) return Integer.MAX_VALUE;
}
return (int) index;
}
private static boolean isDigit(char c) {
return c >= '0' && c <= '9';
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.sdlgpu;
import com.gtnewhorizons.angelica.glsm.texture.ProxyTexture;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@@ -47,11 +48,11 @@ class ProxyTextureLimitsTest {
assertEquals(2048, SDLGPURenderBackend.deviceLimit(GL12.GL_MAX_3D_TEXTURE_SIZE));
assertEquals(16384, SDLGPURenderBackend.deviceLimit(GL13.GL_MAX_CUBE_MAP_TEXTURE_SIZE));
assertEquals(16384, SDLGPURenderBackend.proxyDimensionLimit(GL11.GL_PROXY_TEXTURE_2D));
assertEquals(16384, SDLGPURenderBackend.proxyDimensionLimit(GL11.GL_PROXY_TEXTURE_1D));
assertEquals(2048, SDLGPURenderBackend.proxyDimensionLimit(GL12.GL_PROXY_TEXTURE_3D));
assertEquals(16384, SDLGPURenderBackend.proxyDimensionLimit(GL13.GL_PROXY_TEXTURE_CUBE_MAP));
assertEquals(16384, SDLGPURenderBackend.proxyDimensionLimit(GL40.GL_PROXY_TEXTURE_CUBE_MAP_ARRAY));
assertEquals(16384, SdlTestRig.contextState().proxyTexture.dimensionLimit(GL11.GL_PROXY_TEXTURE_2D));
assertEquals(16384, SdlTestRig.contextState().proxyTexture.dimensionLimit(GL11.GL_PROXY_TEXTURE_1D));
assertEquals(2048, SdlTestRig.contextState().proxyTexture.dimensionLimit(GL12.GL_PROXY_TEXTURE_3D));
assertEquals(16384, SdlTestRig.contextState().proxyTexture.dimensionLimit(GL13.GL_PROXY_TEXTURE_CUBE_MAP));
assertEquals(16384, SdlTestRig.contextState().proxyTexture.dimensionLimit(GL40.GL_PROXY_TEXTURE_CUBE_MAP_ARRAY));
}
@Test
@@ -99,13 +100,13 @@ class ProxyTextureLimitsTest {
@Test
void levelZeroEquivalentSizeRejectsOverflowAndNonsense() {
assertEquals(4, SDLGPURenderBackend.levelZeroEquivalentSize(4, 0));
assertEquals(32, SDLGPURenderBackend.levelZeroEquivalentSize(4, 3));
assertEquals(0, SDLGPURenderBackend.levelZeroEquivalentSize(4, -1));
assertEquals(0, SDLGPURenderBackend.levelZeroEquivalentSize(0, 2));
assertEquals(0, SDLGPURenderBackend.levelZeroEquivalentSize(4, 31));
assertEquals(0, SDLGPURenderBackend.levelZeroEquivalentSize(4, 30));
assertEquals(1 << 30, SDLGPURenderBackend.levelZeroEquivalentSize(1, 30));
assertEquals(4, ProxyTexture.levelZeroEquivalentSize(4, 0));
assertEquals(32, ProxyTexture.levelZeroEquivalentSize(4, 3));
assertEquals(0, ProxyTexture.levelZeroEquivalentSize(4, -1));
assertEquals(0, ProxyTexture.levelZeroEquivalentSize(0, 2));
assertEquals(0, ProxyTexture.levelZeroEquivalentSize(4, 31));
assertEquals(0, ProxyTexture.levelZeroEquivalentSize(4, 30));
assertEquals(1 << 30, ProxyTexture.levelZeroEquivalentSize(1, 30));
}
@Test
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.sdlgpu;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
@@ -81,7 +82,7 @@ class ProxyTextureTargetTest {
@ParameterizedTest(name = "{0}")
@MethodSource("levelSizeCases")
void levelSizeMatchesExpectation(String label, int base, int level, int expected) {
assertEquals(expected, SDLGPURenderBackend.proxyLevelSize(base, level), label);
assertEquals(expected, TextureInfo.mipLevelSize(base, level), label);
}
@Test
@@ -2,11 +2,13 @@ package com.gtnewhorizons.angelica.sdlgpu.device;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -46,4 +48,12 @@ class DeviceLifecycleGpuTest {
device.destroyDevice();
assertEquals(0L, device.getDevice());
}
@Test
void createsVulkanDeviceWithoutClipDistance() {
device = new Device();
assertTrue(device.createDevice(false), "no SDL GPU device on this machine");
Assumptions.assumeTrue(device.isVulkan(), "test needs the vulkan driver, got " + device.getDriverName());
assertFalse(device.hasClipDistance());
}
}
@@ -10,6 +10,7 @@ import com.gtnewhorizons.angelica.glsm.ffp.CubeParityFixture;
import com.gtnewhorizons.angelica.glsm.ffp.FfpFixture;
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
import com.gtnewhorizons.angelica.glsm.ffp.ParticleParityFixture;
import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
@@ -127,6 +128,30 @@ class GlsmSdlFfpTest {
assertTexelEverywhere("two units, glCallLists");
}
@ParameterizedTest
@ValueSource(booleans = {false, true})
void disabledClipPlaneDoesNotClip(boolean emulated) {
try (var ignored = ClipTestSupport.emulation(emulated)) {
bindBaseTexture();
try {
ClipTestSupport.setPlane(1, 0.0, 0.0, 0.0, -1.0);
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE1);
ClipTestSupport.setPlane(0, 1.0, 0.0, 0.0, 0.0);
GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0);
quadUnit0Only();
final int[] pixels = GlsmSdlHeadlessRig.readTarget();
final int left = pixelAt(pixels, SIZE, SIZE / 4, SIZE / 2);
final int right = pixelAt(pixels, SIZE, 3 * SIZE / 4, SIZE / 2);
assertTrue(left != TEXEL, () -> "left quarter must be clipped by enabled plane 0: " + describe(left));
assertEquals(TEXEL, right, () -> "right quarter must survive; disabled plane 1 must not clip: " + describe(right));
} finally {
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0);
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE1);
}
}
}
@Test
void litTexturedQuadIsShaded() {
bindBaseTexture();
@@ -3,6 +3,15 @@ package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GLSMBuffers;
import com.gtnewhorizons.angelica.glsm.GLSMFramebuffer;
import com.gtnewhorizons.angelica.glsm.GLSMTextures;
import com.gtnewhorizons.angelica.glsm.GlTestSupport;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.sdlgpu.frame.FenceTracker;
import com.gtnewhorizons.angelica.sdlgpu.resource.ReadbackShadows;
import com.gtnewhorizons.angelica.sdlgpu.resource.SdlReflect;
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -10,11 +19,19 @@ import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL43;
import java.nio.ByteBuffer;
import java.util.concurrent.locks.LockSupport;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Tag("glsm-sdl")
@ExtendWith(GlsmSdlFrameExtension.class)
@@ -26,6 +43,14 @@ class GlsmSdlReadbackTest {
private static final int B = 0x56;
private static final int A = 0xFF;
private static final int SRC_W = 64;
private static final int SRC_H = 32;
private static final int CAPTURE_W = 16;
private static final int CAPTURE_H = 8;
private static final int COPY_BYTES = 64;
private record StreamReadCopy(int src, int dst, ByteBuffer data) {}
@Test
void getTexImageSeesDeferredTextureUpload() {
final int texture = GLSMTextures.glGenTextures();
@@ -84,6 +109,196 @@ class GlsmSdlReadbackTest {
assertEquals(0xFF, pixel.get(3) & 0xFF);
}
@Test
void screenCaptureSeesUserFramebuffer() {
final int texture = GlsmSdlHeadlessRig.newNearestTexture();
GLSMTextures.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, SRC_W, SRC_H, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null);
final int framebuffer = GlsmSdlHeadlessRig.fboWithColor(texture, 0);
GlTestSupport.fillQuadrants(SRC_W, SRC_H);
final ScreenCapture capture = GLSMFramebuffer.createScreenCapture(CAPTURE_W, CAPTURE_H);
try {
assertTrue(capture.begin(framebuffer, SRC_W, SRC_H, 7L), "capture was not recorded");
GlsmSdlHeadlessRig.endFrame();
GlsmSdlHeadlessRig.beginFrame();
awaitPoll(capture);
assertEquals(7L, capture.tag());
GlTestSupport.assertQuadrantsBottomUp(capture.address(), CAPTURE_W, CAPTURE_H);
capture.release();
final long texture0 = Reflect.<long[]>get(capture, "textures")[0];
final long transferBuffer0 = Reflect.<long[]>get(capture, "transferBuffers")[0];
assertNotEquals(0L, texture0, "slot 0 was never created");
assertNotEquals(0L, transferBuffer0, "slot 0 was never created");
capture.delete();
GlsmSdlHeadlessRig.endFrame();
GlsmSdlHeadlessRig.beginFrame();
assertNotLive(texture0, "liveTextureHandles");
assertNotLive(transferBuffer0, "liveTransferBufferHandles");
} finally {
capture.delete();
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLSMFramebuffer.glDeleteFramebuffers(framebuffer);
GLSMTextures.glDeleteTextures(texture);
}
}
private static void assertNotLive(long handle, String field) {
final LongOpenHashSet live = Reflect.get(GlsmSdlHeadlessRig.resourceManager(), field);
assertFalse(live.contains(handle), field + " still holds capture handle " + handle + " after delete");
}
@Test
void deleteSyncBeforeSubmitIsNotResurrected() {
final long sync = GLSMBuffers.glFenceSync(GL32.GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
GLSMBuffers.glDeleteSync(sync);
GlsmSdlHeadlessRig.endFrame();
GlsmSdlHeadlessRig.beginFrame();
final FenceTracker fences = Reflect.get(BackendManager.RENDER_BACKEND, "fenceTracker");
final Long2LongOpenHashMap fenceMap = Reflect.get(fences, "fenceMap");
assertFalse(fenceMap.containsKey(sync), "a sync deleted before its submit must not be bound to the frame fence afterwards");
}
private static void awaitPoll(ScreenCapture capture) {
final long deadline = System.nanoTime() + 5_000_000_000L;
while (!capture.poll()) {
assertTrue(System.nanoTime() < deadline, "capture did not complete within 5 s");
LockSupport.parkNanos(1_000_000L);
}
}
@Test
void streamReadCopyServesWithoutAppFence() {
GlsmSdlHeadlessRig.endFrame();
final StreamReadCopy copy = streamReadCopy(false);
assertTrue(hasShadow(copy.dst()), "copy into a GL_STREAM_READ buffer must take the shadow path");
final ReadbackShadows shadows = Reflect.get(BackendManager.RENDER_BACKEND, "readbackShadows");
assertNotEquals(0L, SdlReflect.readbackSlotSync(shadows, copy.dst(), 0), "the shadow download must carry a fence");
final ByteBuffer read = BufferUtils.createByteBuffer(COPY_BYTES);
GLSMBuffers.glGetBufferSubData(GL31.GL_COPY_WRITE_BUFFER, 0, read);
assertBytes(copy.data(), read, "glGetBufferSubData right after the copy");
assertEquals(0L, SdlReflect.readbackSlotSync(shadows, copy.dst(), 0), "the read must be served from the shadow, which consumes its fence");
GLSMBuffers.glDeleteBuffers(copy.src());
GLSMBuffers.glDeleteBuffers(copy.dst());
}
@Test
void streamReadShadowSkippedWhileBoundAsSsbo() {
GlsmSdlHeadlessRig.endFrame();
final StreamReadCopy copy = streamReadCopy(true);
assertFalse(hasShadow(copy.dst()), "a destination bound as an SSBO can be written by the GPU, so it must not be shadowed");
final ByteBuffer read = BufferUtils.createByteBuffer(COPY_BYTES);
GLSMBuffers.glGetBufferSubData(GL31.GL_COPY_WRITE_BUFFER, 0, read);
assertBytes(copy.data(), read, "glGetBufferSubData of an SSBO-bound copy destination");
GLSMBuffers.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, 3, 0);
GLSMBuffers.glDeleteBuffers(copy.src());
GLSMBuffers.glDeleteBuffers(copy.dst());
}
@Test
void streamReadCopyVisibleToMapAndGpu() {
GlsmSdlHeadlessRig.endFrame();
final StreamReadCopy copy = streamReadCopy(false);
final ByteBuffer mapped = GLSMBuffers.glMapBufferRange(GL31.GL_COPY_WRITE_BUFFER, 0, COPY_BYTES, GL30.GL_MAP_READ_BIT);
assertNotNull(mapped, "glMapBufferRange returned null");
assertBytes(copy.data(), mapped, "glMapBufferRange READ of the copy destination");
GLSMBuffers.glUnmapBuffer(GL31.GL_COPY_WRITE_BUFFER);
final int third = GLSMBuffers.glGenBuffers();
GLSMBuffers.glBindBuffer(GL31.GL_COPY_READ_BUFFER, copy.dst());
GLSMBuffers.glBindBuffer(GL31.GL_COPY_WRITE_BUFFER, third);
GLSMBuffers.glBufferData(GL31.GL_COPY_WRITE_BUFFER, COPY_BYTES, GL15.GL_DYNAMIC_DRAW);
GLSMBuffers.glCopyBufferSubData(GL31.GL_COPY_READ_BUFFER, GL31.GL_COPY_WRITE_BUFFER, 0, 0, COPY_BYTES);
final ByteBuffer read = BufferUtils.createByteBuffer(COPY_BYTES);
GLSMBuffers.glGetBufferSubData(GL31.GL_COPY_WRITE_BUFFER, 0, read);
assertBytes(copy.data(), read, "GPU copy out of the copy destination");
GLSMBuffers.glDeleteBuffers(third);
GLSMBuffers.glDeleteBuffers(copy.src());
GLSMBuffers.glDeleteBuffers(copy.dst());
}
@Test
void streamReadShadowDroppedOnSubData() {
GlsmSdlHeadlessRig.endFrame();
final StreamReadCopy copy = streamReadCopy(false);
final ByteBuffer other = BufferUtils.createByteBuffer(COPY_BYTES);
for (int i = 0; i < COPY_BYTES; i++) other.put(i, (byte) (0xA0 ^ i));
GLSMBuffers.glBufferSubData(GL31.GL_COPY_WRITE_BUFFER, 0, other);
final ByteBuffer read = BufferUtils.createByteBuffer(COPY_BYTES);
GLSMBuffers.glGetBufferSubData(GL31.GL_COPY_WRITE_BUFFER, 0, read);
assertBytes(other, read, "glGetBufferSubData after glBufferSubData over the copy");
GLSMBuffers.glDeleteBuffers(copy.src());
GLSMBuffers.glDeleteBuffers(copy.dst());
}
@Test
void streamReadShadowServesOnlyDownloadedBytes() {
GlsmSdlHeadlessRig.endFrame();
final StreamReadCopy copy = streamReadCopy(false);
final int half = COPY_BYTES / 2;
final int quarter = COPY_BYTES / 4;
final ByteBuffer tail = BufferUtils.createByteBuffer(half);
for (int i = 0; i < half; i++) tail.put(i, (byte) (0x50 + i));
GLSMBuffers.glBufferSubData(GL31.GL_COPY_WRITE_BUFFER, half, tail);
final ByteBuffer head = BufferUtils.createByteBuffer(quarter);
for (int i = 0; i < quarter; i++) head.put(i, (byte) (0xC0 + i));
final int src2 = GLSMBuffers.glGenBuffers();
GLSMBuffers.glBindBuffer(GL43.GL_SHADER_STORAGE_BUFFER, src2);
GLSMBuffers.glBufferData(GL43.GL_SHADER_STORAGE_BUFFER, head, GL15.GL_DYNAMIC_DRAW);
GLSMBuffers.glBindBuffer(GL31.GL_COPY_READ_BUFFER, src2);
GLSMBuffers.glCopyBufferSubData(GL31.GL_COPY_READ_BUFFER, GL31.GL_COPY_WRITE_BUFFER, 0, 0, quarter);
final ByteBuffer read = BufferUtils.createByteBuffer(COPY_BYTES);
GLSMBuffers.glGetBufferSubData(GL31.GL_COPY_WRITE_BUFFER, 0, read);
for (int i = 0; i < COPY_BYTES; i++) {
final byte expected = i < quarter ? head.get(i) : i < half ? copy.data().get(i) : tail.get(i - half);
assertEquals(expected, read.get(i), "byte " + i + " after a shorter copy reused a larger shadow");
}
GLSMBuffers.glDeleteBuffers(src2);
GLSMBuffers.glDeleteBuffers(copy.src());
GLSMBuffers.glDeleteBuffers(copy.dst());
}
private static StreamReadCopy streamReadCopy(boolean dstBoundAsSsbo) {
final ByteBuffer data = BufferUtils.createByteBuffer(COPY_BYTES);
for (int i = 0; i < COPY_BYTES; i++) data.put(i, (byte) (i * 7 + 3));
final int src = GLSMBuffers.glGenBuffers();
GLSMBuffers.glBindBuffer(GL43.GL_SHADER_STORAGE_BUFFER, src);
GLSMBuffers.glBufferData(GL43.GL_SHADER_STORAGE_BUFFER, data, GL15.GL_DYNAMIC_DRAW);
final int dst = GLSMBuffers.glGenBuffers();
GLSMBuffers.glBindBuffer(GL31.GL_COPY_READ_BUFFER, src);
GLSMBuffers.glBindBuffer(GL31.GL_COPY_WRITE_BUFFER, dst);
GLSMBuffers.glBufferData(GL31.GL_COPY_WRITE_BUFFER, COPY_BYTES, GL15.GL_STREAM_READ);
if (dstBoundAsSsbo) GLSMBuffers.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, 3, dst);
GLSMBuffers.glCopyBufferSubData(GL31.GL_COPY_READ_BUFFER, GL31.GL_COPY_WRITE_BUFFER, 0, 0, COPY_BYTES);
return new StreamReadCopy(src, dst, data);
}
private static boolean hasShadow(int glId) {
final ReadbackShadows shadows = Reflect.get(BackendManager.RENDER_BACKEND, "readbackShadows");
return shadows.hasSlotsFor(glId);
}
private static void assertBytes(ByteBuffer expected, ByteBuffer actual, String what) {
for (int i = 0; i < COPY_BYTES; i++) {
assertEquals(expected.get(i), actual.get(i), what + ": byte " + i);
}
}
private static ByteBuffer solidTexture(int width, int height) {
final ByteBuffer pixels = BufferUtils.createByteBuffer(width * height * 4);
for (int i = 0; i < width * height; i++) {
@@ -0,0 +1,246 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GLSMDraws;
import com.gtnewhorizons.angelica.glsm.GLSMPrograms;
import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
import com.gtnewhorizons.angelica.glsm.GlTestSupport;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@Tag("glsm-sdl")
@ExtendWith(GlsmSdlFrameExtension.class)
class GlsmSdlUniformArrayTest {
private static final int COUNT = 8;
private static final int[][] A = {
{10, 20, 30}, {40, 50, 60}, {70, 80, 90}, {100, 110, 120},
{130, 140, 150}, {160, 170, 180}, {190, 200, 210}, {220, 230, 240}
};
private static final int[] X = {250, 5, 15};
private static final int[][] B = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {11, 12, 13}};
private int program;
private int vao;
private int arr;
private int single;
private int pick;
@BeforeEach
void setUp() {
program = GlTestSupport.program(GlsmSdlHeadlessRig.FULLSCREEN_VERTEX, """
#version 330 core
uniform vec4 arr[8];
uniform vec4 single;
uniform ivec4 iarr[2];
uniform vec4 one[1];
uniform int pick;
out vec4 color;
void main() { color = pick == -2 ? vec4(iarr[1]) / 255.0 : pick == -3 ? one[0] : pick < 0 ? single : arr[pick]; }
""");
vao = GLSMVertexInput.glGenVertexArrays();
GLSMVertexInput.glBindVertexArray(vao);
GLSMPrograms.glUseProgram(program);
arr = loc("arr");
single = loc("single");
pick = loc("pick");
}
@AfterEach
void tearDown() {
GLSMPrograms.glUseProgram(0);
GLSMVertexInput.glBindVertexArray(0);
GLSMVertexInput.glDeleteVertexArrays(vao);
GLSMPrograms.glDeleteProgram(program);
}
private int loc(String name) {
return GLSMPrograms.glGetUniformLocation(program, name);
}
private static int argb(int[] rgb) {
return 0xFF000000 | (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
}
private static float[] vec(int[] rgb) {
return new float[] {rgb[0] / 255f, rgb[1] / 255f, rgb[2] / 255f, 1f};
}
private static FloatBuffer pack(int[]... colors) {
final FloatBuffer buf = MemoryUtil.memAllocFloat(colors.length * 4);
for (int[] c : colors) buf.put(vec(c));
buf.flip();
return buf;
}
private void writeAt(int location, int[]... colors) {
final FloatBuffer buf = pack(colors);
try {
GLSMPrograms.glUniform4(location, buf);
} finally {
MemoryUtil.memFree(buf);
}
}
private void writeElement(int location, int[] rgb) {
final float[] v = vec(rgb);
GLSMPrograms.glUniform4f(location, v[0], v[1], v[2], v[3]);
}
private void writeAll() {
writeAt(arr, A);
}
private int drawPick(int index) {
GlsmSdlHeadlessRig.beginFrame();
GlsmSdlHeadlessRig.bindTarget();
GLSMPrograms.glUniform1i(pick, index);
GLSMDraws.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
return GlsmSdlHeadlessRig.readTarget(GlsmSdlHeadlessRig.SIZE / 2, GlsmSdlHeadlessRig.SIZE / 2, 1, 1)[0];
}
private void assertPick(int index, int[] expected, String label) {
final int got = drawPick(index);
assertEquals(Integer.toHexString(argb(expected)), Integer.toHexString(got), label + ": " + GlsmSdlHeadlessRig.describe(got));
}
private float[] readUniform(int location) {
final FloatBuffer buf = MemoryUtil.memAllocFloat(4);
try {
GLSMPrograms.glGetUniform(program, location, buf);
final float[] out = new float[4];
buf.get(out);
return out;
} finally {
MemoryUtil.memFree(buf);
}
}
@Test
void arrayElementLocations() {
assertNotEquals(-1, arr, "arr");
assertNotEquals(-1, single, "single");
assertEquals(arr, loc("arr[0]"), "arr[0]");
assertEquals(arr + 3, loc("arr[3]"), "arr[3]");
assertEquals(arr + 7, loc("arr[7]"), "arr[7]");
assertEquals(-1, loc("arr[8]"), "arr[8]");
assertEquals(-1, loc("arr[03]"), "arr[03]");
assertEquals(-1, loc("arr[-1]"), "arr[-1]");
assertEquals(arr, loc("arr[]"), "arr[] is index 0, as Mesa parses it");
assertEquals(single, loc("single[0]"), "single[0]");
assertEquals(-1, loc("single[1]"), "single[1]");
}
@Test
void elementWriteLandsInTheArray() {
writeAll();
final int element3 = loc("arr[3]");
writeElement(element3, X);
assertPick(3, X, "pick=3 shows the element write");
assertPick(2, A[2], "pick=2 keeps the bulk write");
assertArrayEquals(vec(X), readUniform(element3), 0f, "glGetUniform arr[3]");
assertArrayEquals(vec(A[2]), readUniform(loc("arr[2]")), 0f, "glGetUniform arr[2]");
}
@Test
void elementWriteClampsToTheArrayEnd() {
writeAll();
writeAt(loc("arr[6]"), B);
assertPick(6, B[0], "pick=6");
assertPick(7, B[1], "pick=7");
assertPick(5, A[5], "pick=5");
}
@Test
void countBeyondTheDeclarationFollowsMesa() {
writeElement(single, A[0]);
writeAt(single, B);
assertPick(-1, A[0], "count 2 on a non-array is ignored");
assertArrayEquals(vec(A[0]), readUniform(single), 0f, "glGetUniform single keeps its value");
writeAt(loc("one"), B);
assertPick(-3, B[0], "count 2 on a size-1 array clamps");
}
@Test
void fullWriteAfterElementWriteIsNotElided() {
writeAll();
writeElement(loc("arr[3]"), X);
writeAll();
assertPick(3, A[3], "pick=3 after the repeated full write");
}
@Test
void intElementReadsBackAsInts() {
final int element1 = loc("iarr[1]");
assertEquals(loc("iarr") + 1, element1, "iarr[1]");
GLSMPrograms.glUniform4i(loc("iarr"), 1, 2, 3, 4);
GLSMPrograms.glUniform4i(element1, X[0], X[1], X[2], 255);
assertPick(-2, X, "pick=-2 shows iarr[1]");
assertArrayEquals(new int[] {X[0], X[1], X[2], 255}, readIntUniform(element1), "glGetUniformiv iarr[1]");
assertArrayEquals(new int[] {1, 2, 3, 4}, readIntUniform(loc("iarr")), "glGetUniformiv iarr[0]");
}
private int[] readIntUniform(int location) {
final IntBuffer buf = MemoryUtil.memAllocInt(4);
try {
GLSMPrograms.glGetUniform(program, location, buf);
final int[] out = new int[4];
buf.get(out);
return out;
} finally {
MemoryUtil.memFree(buf);
}
}
@Test
void activeUniformReportsArrayNameWithIndexZero() {
final Map<String, Integer> sizes = activeUniformSizes();
assertEquals(COUNT, sizes.getOrDefault("arr[0]", -1), "size of arr[0] in " + sizes);
assertEquals(1, sizes.getOrDefault("single", -1), "size of single in " + sizes);
assertEquals(1, sizes.getOrDefault("one[0]", -1), "size of one[0] in " + sizes);
}
private Map<String, Integer> activeUniformSizes() {
final int active = GLSMPrograms.glGetProgrami(program, GL20.GL_ACTIVE_UNIFORMS);
final Map<String, Integer> sizes = new HashMap<>();
final IntBuffer length = MemoryUtil.memAllocInt(1);
final IntBuffer size = MemoryUtil.memAllocInt(1);
final IntBuffer type = MemoryUtil.memAllocInt(1);
final ByteBuffer name = MemoryUtil.memAlloc(128);
try {
for (int i = 0; i < active; i++) {
GLSMPrograms.glGetActiveUniform(program, i, length, size, type, name);
final byte[] bytes = new byte[length.get(0)];
for (int c = 0; c < bytes.length; c++) bytes[c] = name.get(c);
sizes.put(new String(bytes, java.nio.charset.StandardCharsets.US_ASCII), size.get(0));
}
} finally {
MemoryUtil.memFree(name);
MemoryUtil.memFree(type);
MemoryUtil.memFree(size);
MemoryUtil.memFree(length);
}
return sizes;
}
}
@@ -1,12 +1,10 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.sdlgpu.SdlAsserts;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -19,7 +17,7 @@ class ReadbackShadowsTest {
private static final int SLOT_STRIDE = 64;
private static ReadbackShadows shadows() {
return new ReadbackShadows(null);
return new ReadbackShadows(null, null, null);
}
private static ByteBuffer out(int bytes) {
@@ -36,15 +34,24 @@ class ReadbackShadowsTest {
}
@Test
void invalidateTouchesOnlyItsOwnSlot() {
void invalidateAllTouchesOnlyItsBuffer() {
final ReadbackShadows shadows = shadows();
SdlReflect.putReadbackSlot(shadows, 7, 0, SENTINEL_HANDLE, 64, true);
SdlReflect.putReadbackSlot(shadows, 7, 64, SENTINEL_HANDLE, 64, true);
SdlReflect.putReadbackSlot(shadows, 8, 0, SENTINEL_HANDLE, 64, true);
shadows.invalidate(7, 0);
shadows.invalidateAll(7);
assertFalse(shadows.serve(7, 0, out(64), 64), "the invalidated slot is still being served");
assertTrue(SdlReflect.readbackSlotValid(shadows, 7, 64), "invalidating one ring slot must not disturb the others");
assertFalse(shadows.serve(7, 0, out(64), 64), "an invalidated slot is still being served");
assertFalse(SdlReflect.readbackSlotValid(shadows, 7, 64), "every slot of the written buffer must be invalidated");
assertTrue(SdlReflect.readbackSlotValid(shadows, 8, 0), "a write to one buffer must not invalidate another's slots");
}
@Test
void invalidateAllOfAnUnshadowedBufferDoesNotAllocate() {
final ReadbackShadows shadows = shadows();
SdlReflect.putReadbackSlot(shadows, 8, 0, SENTINEL_HANDLE, 64, true);
SdlAsserts.assertAllocationFree(() -> shadows.invalidateAll(7), "buffer writes while another buffer is shadowed");
}
@Test
@@ -60,10 +67,6 @@ class ReadbackShadowsTest {
assertTrue(shadows.hasSlotsFor(8), "deleting one buffer must not drop another's slots");
}
private static int slotCount(ReadbackShadows shadows) {
return Reflect.<Map<Long, Object>>get(shadows, "slots").size();
}
private static ReadbackShadows ringShadows(int... glIds) {
final ReadbackShadows shadows = shadows();
for (int glId : glIds) {
@@ -81,7 +84,7 @@ class ReadbackShadowsTest {
shadows.release(2);
assertFalse(shadows.hasSlotsFor(2));
assertEquals(2 * SLOTS_PER_BUFFER, slotCount(shadows));
assertEquals(2 * SLOTS_PER_BUFFER, SdlReflect.readbackSlotCount(shadows));
for (int glId = 1; glId <= 3; glId += 2) {
for (int i = 0; i < SLOTS_PER_BUFFER; i++) {
assertTrue(SdlReflect.readbackSlotValid(shadows, glId, (long) i * SLOT_STRIDE));
@@ -1,9 +1,10 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import java.util.Map;
import java.util.Queue;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
public final class SdlReflect {
@@ -30,20 +31,27 @@ public final class SdlReflect {
final Object slot = Reflect.construct(slotClass);
Reflect.setDeclared(slotClass, slot, "transferBuffer", handle);
Reflect.setDeclared(slotClass, slot, "capacity", capacity);
Reflect.setDeclared(slotClass, slot, "validLen", capacity);
Reflect.setDeclared(slotClass, slot, "valid", valid);
readbackSlots(shadows).put(readbackKey(glId, offset), slot);
readbackSlots(shadows).computeIfAbsent(glId, k -> new Long2ObjectOpenHashMap<>()).put(offset, slot);
}
static boolean readbackSlotValid(ReadbackShadows shadows, int glId, long offset) {
return Reflect.get(readbackSlots(shadows).get(readbackKey(glId, offset)), "valid");
return Reflect.get(readbackSlots(shadows).get(glId).get(offset), "valid");
}
private static long readbackKey(int glId, long offset) {
return (((long) glId) << 32) | (offset & 0xFFFFFFFFL);
public static long readbackSlotSync(ReadbackShadows shadows, int glId, long offset) {
return Reflect.get(readbackSlots(shadows).get(glId).get(offset), "sync");
}
private static Map<Long, Object> readbackSlots(ReadbackShadows shadows) {
return Reflect.get(shadows, "slots");
static int readbackSlotCount(ReadbackShadows shadows) {
int count = 0;
for (Long2ObjectOpenHashMap<Object> slots : readbackSlots(shadows).values()) count += slots.size();
return count;
}
private static Int2ObjectOpenHashMap<Long2ObjectOpenHashMap<Object>> readbackSlots(ReadbackShadows shadows) {
return Reflect.get(shadows, "byBuffer");
}
static void recordBuffer(ResourceManager rm, int glId, long handle, long size, int usage) {
@@ -28,6 +28,7 @@ import com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader;
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
import com.gtnewhorizons.angelica.rendering.tesr.BatchDrawDefaults;
import com.gtnewhorizons.angelica.rendering.tesr.EntityShadowReplay;
import com.gtnewhorizons.angelica.rendering.tesr.FlattenedItemLighting;
import com.gtnewhorizons.angelica.rendering.tesr.ModelPartBatcher;
import com.gtnewhorizons.angelica.rendering.tesr.TesrBatchRenderer;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
@@ -1194,6 +1195,7 @@ public class BatchingFontRenderer {
final Tessellator tessellator = Tessellator.instance;
int lastTexture = INVALID_TEXTURE;
boolean drawing = false;
FlattenedItemLighting.setPreLitText(true);
try {
for (int i = from; i < to; i++) {
final FontDrawCmd cmd = cmdsData[i];
@@ -1219,8 +1221,12 @@ public class BatchingFontRenderer {
owner.emitCommand(tessellator, cmd);
}
} finally {
if (drawing) {
tessellator.draw();
try {
if (drawing) {
tessellator.draw();
}
} finally {
FlattenedItemLighting.setPreLitText(false);
}
}
}
@@ -99,6 +99,22 @@ public class TracyOptionPages {
.build())
.add(new ActionOption(I18n.format("options.angelica.tracy.captureAction"), I18n.format("options.angelica.tracy.captureAction.tooltip"),
captureActionLabel, TracyCaptureNotifier.INSTANCE::toggle, () -> Tracy.ENABLED))
.build(),
OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
.setName(I18n.format("options.angelica.tracy.frameImages"))
.setTooltip(overrideTooltip("options.angelica.tracy.frameImages.tooltip", "angelica.tracy.frameImages", SystemProperties.TRACY_FRAME_IMAGES_OVERRIDE != null))
.setControl(TickBoxControl::new)
.setBinding((opts, value) -> AngelicaConfig.tracyFrameImages = value, opts -> AngelicaConfig.tracyFrameImages)
.setEnabled(Tracy.ENABLED && SystemProperties.TRACY_FRAME_IMAGES_OVERRIDE == null)
.build())
.add(OptionImpl.createBuilder(int.class, angelicaOpts)
.setName(I18n.format("options.angelica.tracy.frameImageRate"))
.setTooltip(overrideTooltip("options.angelica.tracy.frameImageRate.tooltip", "angelica.tracy.frameImageRate", SystemProperties.TRACY_FRAME_IMAGE_RATE_OVERRIDE != null))
.setControl(o -> new SliderControl(o, 0, 60, 5, ControlValueFormatter.quantityOrDisabled("options.angelica.tracy.frameImageRate.value", "options.angelica.tracy.frameImageRate.everyFrame")))
.setBinding((opts, value) -> AngelicaConfig.tracyFrameImageRate = value, opts -> AngelicaConfig.tracyFrameImageRate)
.setEnabled(Tracy.ENABLED && SystemProperties.TRACY_FRAME_IMAGE_RATE_OVERRIDE == null)
.build())
.build()));
}
@@ -0,0 +1,34 @@
package com.gtnewhorizons.angelica.common;
import net.minecraft.block.BlockContainer;
import net.minecraft.block.material.Material;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.world.World;
public class BlockClipPlaneTest extends BlockContainer {
public BlockClipPlaneTest() {
super(Material.iron);
setHardness(0.7f);
}
@Override
public TileEntity createNewTileEntity(World world, int meta) {
return new TileEntityClipPlaneTest();
}
@Override
public boolean isOpaqueCube() {
return false;
}
@Override
public boolean renderAsNormalBlock() {
return false;
}
@Override
public int getRenderType() {
return -1;
}
}
@@ -0,0 +1,11 @@
package com.gtnewhorizons.angelica.common;
import net.minecraft.tileentity.TileEntity;
public class TileEntityClipPlaneTest extends TileEntity {
@Override
public boolean canUpdate() {
return false;
}
}
@@ -15,6 +15,7 @@ import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.StateSet;
import com.gtnewhorizons.angelica.glsm.ffp.FragmentKey;
import com.gtnewhorizons.angelica.glsm.hooks.BatchStateGuard;
import com.gtnewhorizons.angelica.glsm.states.FogState;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import net.coderbot.iris.gl.framebuffer.GlFramebuffer;
@@ -181,6 +182,7 @@ final class PortalDrawer {
}
void drawDirect(float[] faces, int count, int tunnelTexture, int fieldTexture, Frame frame, boolean fogged) {
BatchStateGuard.beforeChange();
final int fogMode = fogged ? FragmentKey.activeFogMode(GLStateManager.ctx()) : FragmentKey.FOG_NONE;
final GlProgram<Uniforms> program;
if (fogMode == FragmentKey.FOG_NONE) {
@@ -556,6 +556,15 @@ public class AngelicaConfig {
@Config.RangeInt(min = 0, max = 3600)
public static int tracyCaptureSeconds;
@Config.Comment("If enabled, attaches downscaled screenshots to Tracy frames. Overridden by -Dangelica.tracy.frameImages")
@Config.DefaultBoolean(false)
public static boolean tracyFrameImages;
@Config.Comment("Tracy frame images captured per second. 0 = every frame. Overridden by -Dangelica.tracy.frameImageRate")
@Config.DefaultInt(10)
@Config.RangeInt(min = 0, max = 60)
public static int tracyFrameImageRate;
public static void applyGpuCullingMode() {
GpuCulling.setMode(gpuCullingMode == null ? GpuCullingMode.CPU_ONLY : gpuCullingMode);
}
@@ -34,9 +34,6 @@ public final class PositionEntries {
}
}
private WorldProvider cachedProvider;
private String dimensionName = "";
@Override
public void display(DebugScreenDisplayer displayer, DebugContext context) {
final EntityClientPlayerMP player = context.player();
@@ -52,10 +49,7 @@ public final class PositionEntries {
facing.value(DIRECTION_NAMES[heading]).text(" (").text(TOWARDS[heading]).text(") (");
facing.value(MathHelper.wrapAngleTo180_float(player.rotationYaw), 1).text(SEPARATOR).value(MathHelper.wrapAngleTo180_float(player.rotationPitch), 1).text(')');
final WorldProvider provider = context.world().provider;
if (provider != cachedProvider) {
cachedProvider = provider;
dimensionName = provider.getDimensionName();
}
final String dimensionName = provider.getDimensionName();
displayer.fact(DebugGroup.POSITION, "Dimension").value(dimensionName).text(" (").value(provider.dimensionId).text(')');
}
}
@@ -274,6 +274,13 @@ public class IrisGLSMBridge {
refreshBlendCondition();
});
GLSMHooks.CLIP_PLANES_IN_USE.addListener(event -> {
if (!Iris.enabled) return;
if (Iris.getPipelineManager().getPipelineNullable() instanceof DeferredWorldRenderingPipeline drp) {
drp.onClipPlanesInUseChanged();
}
});
GLSMHooks.FOREIGN_DRAW_END.addListener(event -> {
if (!Iris.enabled) return;
if (programRestoreDeferred) {
@@ -336,12 +343,13 @@ public class IrisGLSMBridge {
WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable();
if (pipeline instanceof DeferredWorldRenderingPipeline drp) {
DeferredWorldRenderingPipeline.Pass activePass = drp.getActivePassProgram();
if (activePass != null && activePass.getProgram() != null && activePass.getProgram().getProgramId() == event.newProgram) {
final Program bound = activePass != null ? activePass.boundProgram() : null;
if (bound != null && bound.getProgramId() == event.newProgram) {
final int frame = SystemTimeUniforms.COUNTER.getAsInt();
if (programLastUpdatedFrame.get(event.newProgram) != frame
|| !ProgramUniforms.isActiveProgramBound()
|| ProgramUniforms.activeHasDeferredUploads()) {
activePass.getProgram().getUniforms().update();
bound.getUniforms().update();
programLastUpdatedFrame.put(event.newProgram, frame);
}
}
@@ -0,0 +1,104 @@
package com.gtnewhorizons.angelica.profiling;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.CaptureGate;
import com.gtnewhorizons.angelica.glsm.GLSMFramebuffer;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import net.minecraft.client.Minecraft;
import net.minecraft.client.shader.Framebuffer;
public final class TracyFrameImages {
private static final int MAX_WIDTH = 320;
private static final int MAX_HEIGHT = 180;
private static final Tracy.ZoneId Z_CAPTURE = Tracy.zoneId("frameImageCapture", Tracy.COLOR_SWAP);
private static final Tracy.ZoneId Z_EMIT = Tracy.zoneId("frameImageEmit", Tracy.COLOR_SWAP);
private static final boolean HAS_ENABLED_OVERRIDE = SystemProperties.TRACY_FRAME_IMAGES_OVERRIDE != null;
private static final boolean ENABLED_OVERRIDE = HAS_ENABLED_OVERRIDE && SystemProperties.TRACY_FRAME_IMAGES_OVERRIDE;
private static final boolean HAS_RATE_OVERRIDE = SystemProperties.TRACY_FRAME_IMAGE_RATE_OVERRIDE != null;
private static final int RATE_OVERRIDE = HAS_RATE_OVERRIDE ? Math.max(0, SystemProperties.TRACY_FRAME_IMAGE_RATE_OVERRIDE) : 0;
private static ScreenCapture capture;
private static long frameIndex;
private static long lastCaptureNs;
private static int srcW;
private static int srcH;
private TracyFrameImages() {}
public static void onPostSwap(Minecraft mc) {
if (!enabled() || !CaptureGate.tracyConnected) {
drop();
return;
}
emitReady();
final long now = System.nanoTime();
final int rate = rate();
if (rate != 0 && now - lastCaptureNs < 1_000_000_000L / rate) return;
final Framebuffer source = mc.getFramebuffer();
final int w = source.framebufferWidth;
final int h = source.framebufferHeight;
if (w <= 0 || h <= 0) return;
if (capture == null || w != srcW || h != srcH) {
srcW = w;
srcH = h;
final long size = fitCaptureSize(w, h);
final int captureW = (int) (size >>> 32);
final int captureH = (int) size;
if (capture == null || capture.width() != captureW || capture.height() != captureH) {
if (capture != null) capture.delete();
capture = GLSMFramebuffer.createScreenCapture(captureW, captureH);
}
return;
}
Tracy.beginZone(Z_CAPTURE);
if (capture.begin(source.framebufferObject, w, h, frameIndex)) lastCaptureNs = now;
Tracy.endZone();
}
static long fitCaptureSize(int srcWidth, int srcHeight) {
final float aspect = (float) srcWidth / srcHeight;
int width = srcWidth;
int height = srcHeight;
if (width > MAX_WIDTH) {
width = MAX_WIDTH;
height = (int) (MAX_WIDTH / aspect);
}
if (height > MAX_HEIGHT) {
width = (int) (MAX_HEIGHT * aspect);
height = MAX_HEIGHT;
}
return ((long) Math.max(4, width & ~3) << 32) | Math.max(4, height & ~3);
}
public static void onFrameMark() {
frameIndex++;
}
private static void emitReady() {
if (capture == null || capture.idle()) return;
Tracy.beginZone(Z_EMIT);
while (capture.poll()) {
final long lag = frameIndex - capture.tag();
if (lag <= 255) Tracy.frameImage(capture.address(), capture.width(), capture.height(), (int) lag, true);
capture.release();
}
Tracy.endZone();
}
private static void drop() {
if (capture == null) return;
capture.delete();
capture = null;
srcW = srcH = 0;
}
private static boolean enabled() {
return HAS_ENABLED_OVERRIDE ? ENABLED_OVERRIDE : AngelicaConfig.tracyFrameImages;
}
private static int rate() {
return HAS_RATE_OVERRIDE ? RATE_OVERRIDE : AngelicaConfig.tracyFrameImageRate;
}
}
@@ -39,6 +39,7 @@ import com.gtnewhorizons.angelica.commands.AngelicaCommand;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.common.BlockError;
import com.gtnewhorizons.angelica.common.BlockIsbrhTest;
import com.gtnewhorizons.angelica.common.TileEntityClipPlaneTest;
import com.gtnewhorizons.angelica.compat.ModStatus;
import com.gtnewhorizons.angelica.compat.bettercrashes.BetterCrashesCompat;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
@@ -70,6 +71,7 @@ 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.ClipPlaneTestRenderer;
import com.gtnewhorizons.angelica.rendering.FpsReducer;
import com.gtnewhorizons.angelica.rendering.FramePacer;
import com.gtnewhorizons.angelica.rendering.IsbrhTestRenderer;
@@ -86,6 +88,7 @@ import com.gtnewhorizons.angelica.utils.AngelicaJar;
import com.gtnewhorizons.angelica.utils.AnimationMode;
import com.gtnewhorizons.angelica.utils.ManagedEnum;
import com.gtnewhorizons.angelica.zoom.Zoom;
import cpw.mods.fml.client.registry.ClientRegistry;
import cpw.mods.fml.client.registry.RenderingRegistry;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.event.FMLInitializationEvent;
@@ -146,6 +149,7 @@ public final class ClientProxy extends CommonProxy {
if (AngelicaConfig.enableTestBlocks) {
BlockIsbrhTest.renderId = RenderingRegistry.getNextAvailableRenderId();
RenderingRegistry.registerBlockHandler(BlockIsbrhTest.renderId, new IsbrhTestRenderer());
ClientRegistry.bindTileEntitySpecialRenderer(TileEntityClipPlaneTest.class, new ClipPlaneTestRenderer());
}
AngelicaJar.configureShaderDiskCache(event.getSourceFile());
if (AngelicaConfig.enableIris) {
@@ -1,7 +1,9 @@
package com.gtnewhorizons.angelica.proxy;
import com.gtnewhorizons.angelica.common.BlockClipPlaneTest;
import com.gtnewhorizons.angelica.common.BlockIsbrhTest;
import com.gtnewhorizons.angelica.common.BlockTest;
import com.gtnewhorizons.angelica.common.TileEntityClipPlaneTest;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.event.FMLPostInitializationEvent;
@@ -14,6 +16,8 @@ public class CommonProxy {
if (AngelicaConfig.enableTestBlocks) {
GameRegistry.registerBlock(new BlockTest(), "test_block");
GameRegistry.registerBlock(new BlockIsbrhTest(), "test_isbrh");
GameRegistry.registerBlock(new BlockClipPlaneTest(), "test_clip_plane");
GameRegistry.registerTileEntity(TileEntityClipPlaneTest.class, "angelica:test_clip_plane");
}
}
@@ -0,0 +1,41 @@
package com.gtnewhorizons.angelica.rendering;
import com.gtnewhorizons.angelica.glsm.GLSMEnable;
import com.gtnewhorizons.angelica.glsm.GLSMMatrices;
import com.gtnewhorizons.angelica.glsm.GLSMRasterizer;
import net.minecraft.client.renderer.RenderBlocks;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer;
import net.minecraft.init.Blocks;
import net.minecraft.tileentity.TileEntity;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.nio.DoubleBuffer;
public class ClipPlaneTestRenderer extends TileEntitySpecialRenderer {
private static final DoubleBuffer KEEP_LOWER_HALF = plane(0.0, -1.0, 0.0, 0.5);
private static final DoubleBuffer REJECT_ALL = plane(0.0, 0.0, 0.0, -1.0);
private static DoubleBuffer plane(double a, double b, double c, double d) {
final DoubleBuffer buf = BufferUtils.createDoubleBuffer(4);
buf.put(a).put(b).put(c).put(d).flip();
return buf;
}
@Override
public void renderTileEntityAt(TileEntity te, double x, double y, double z, float partialTicks) {
GLSMMatrices.glPushMatrix();
GLSMMatrices.glTranslated(x, y, z);
GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE1, REJECT_ALL);
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE1);
GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE0, KEEP_LOWER_HALF);
GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0);
GLSMMatrices.glTranslated(0.5, 0.5, 0.5);
bindTexture(TextureMap.locationBlocksTexture);
RenderBlocks.getInstance().renderBlockAsItem(Blocks.gold_block, 0, 1.0f);
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0);
GLSMMatrices.glPopMatrix();
}
}
@@ -17,6 +17,7 @@ import com.gtnewhorizons.angelica.rendering.tesr.EntityMaterials;
import com.gtnewhorizons.angelica.rendering.tesr.GlintCapture;
import com.gtnewhorizons.angelica.rendering.tesr.ModelPartBatcher;
import com.gtnewhorizons.angelica.rendering.tesr.EntityShadowReplay;
import com.gtnewhorizons.angelica.rendering.tesr.FlattenedItemLighting;
import com.gtnewhorizons.angelica.rendering.tesr.TemplateBuffer;
import com.gtnewhorizons.angelica.utils.AnimationsRenderUtils;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
@@ -26,6 +27,8 @@ import net.minecraft.client.renderer.RenderBlocks;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.item.ItemStack;
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.VERTEX_SIZE;
public final class DroppedItemInstancer {
public static final long SKIP = -2L;
@@ -93,7 +96,8 @@ public final class DroppedItemInstancer {
private static boolean glintEligible(ItemStack stack) {
return stack != null && stack.getItem() != null && ModelPartBatcher.INSTANCE.isActive() && !GLStateManager.isRecordingDisplayList()
&& !TessellatorManager.isCurrentlyCapturing() && !TessellatorManager.shouldInterceptDraw(Tessellator.instance);
&& !TessellatorManager.isCurrentlyCapturing() && !TessellatorManager.shouldInterceptDraw(Tessellator.instance)
&& !FlattenedItemLighting.applies();
}
static TesrMaterial material(ItemStack stack, boolean unfilteredAtlas) {
@@ -219,6 +223,29 @@ public final class DroppedItemInstancer {
return material != null ? batchBlock(material, rb, block, meta, brightness) : GLSMDraws.drawCalls;
}
public static boolean drawFlattenedBlock(RenderBlocks rb, Block block, int meta, float brightness) {
final BlockMesh mesh = blockMesh(rb, block, meta, brightness);
final TemplateBuffer template = mesh.template();
if (template == null) return false;
mesh.sprites().markUsed();
final Tessellator tess = Tessellator.instance;
final int ints = template.vertexCount * VERTEX_SIZE;
tess.startDrawing(template.drawMode);
if (tess.rawBuffer == null || tess.rawBufferSize < ints) {
tess.rawBufferSize = Math.max(0x10000, ints);
tess.rawBuffer = new int[tess.rawBufferSize];
}
System.arraycopy(template.data, 0, tess.rawBuffer, 0, ints);
tess.vertexCount = template.vertexCount;
tess.rawBufferIndex = ints;
tess.hasTexture = true;
tess.hasNormals = true;
tess.hasColor = false;
tess.hasBrightness = false;
tess.draw();
return true;
}
private static long itemBlock(ItemStack stack, RenderBlocks rb, Block block, int meta, float brightness, boolean unfilteredAtlas) {
final TesrMaterial material = material(stack, unfilteredAtlas);
if (material == null) {
@@ -236,12 +263,7 @@ public final class DroppedItemInstancer {
static long batchBlock(TesrMaterial material, RenderBlocks rb, Block block, int meta, float brightness) {
final long drawsBefore = GLSMDraws.drawCalls;
if (BatchEligibility.batchingAllowed()) {
BlockMesh mesh = lookupBlock(block, meta);
if (mesh == null) {
mesh = captureBlock(rb, block, meta, brightness);
} else if (mesh.template() != null) {
GLSMCurrentAttribs.glColor4f(mesh.red(), mesh.green(), mesh.blue(), mesh.alpha());
}
final BlockMesh mesh = blockMesh(rb, block, meta, brightness);
if (mesh.template() == null) {
fallback++;
bail(BailReason.TEMPLATE);
@@ -338,6 +360,13 @@ public final class DroppedItemInstancer {
return mesh;
}
private static BlockMesh blockMesh(RenderBlocks rb, Block block, int meta, float brightness) {
final BlockMesh hit = lookupBlock(block, meta);
if (hit == null) return captureBlock(rb, block, meta, brightness);
if (hit.template() != null) GLSMCurrentAttribs.glColor4f(hit.red(), hit.green(), hit.blue(), hit.alpha());
return hit;
}
private static BlockMesh lookupBlock(Block block, int meta) {
blockKey.block = block;
blockKey.meta = meta;
@@ -34,7 +34,8 @@ public final class BatchDrawDefaults {
}
public void apply() {
if (passFramebuffer >= 0 && TesrBatchRenderer.deferredPipeline() == null) {
final boolean pack = TesrBatchRenderer.deferredPipeline() != null;
if (passFramebuffer >= 0 && !pack) {
final int framebuffer = GLSMFramebuffer.getDrawFramebuffer();
if (framebuffer != passFramebuffer) {
callerFramebuffer = framebuffer;
@@ -42,7 +43,7 @@ public final class BatchDrawDefaults {
}
}
final int program = GLSMPrograms.getActiveProgram();
if (program != 0 && !Program.isManagedBind(program) && !DepthColorStorage.isOwnedProgram(program)) {
if (program != 0 && !Program.isManagedBind(program) && !(pack && DepthColorStorage.isOwnedProgram(program))) {
callerProgram = program;
GLSMPrograms.glUseProgram(0);
}
@@ -0,0 +1,39 @@
package com.gtnewhorizons.angelica.rendering.tesr;
import com.gtnewhorizons.angelica.glsm.GLSMLighting;
import com.gtnewhorizons.angelica.glsm.GLSMMatrices;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import net.coderbot.iris.shadows.ShadowRenderingState;
import org.joml.Matrix4fc;
public final class FlattenedItemLighting {
static final float FLATNESS_CUTOFF = 0.05f;
private static boolean preLitText;
private FlattenedItemLighting() {}
public static void setPreLitText(boolean preLit) {
preLitText = preLit;
}
static boolean isFlattened(Matrix4fc m) {
final float ax = m.m00(), ay = m.m01(), az = m.m02();
final float bx = m.m10(), by = m.m11(), bz = m.m12();
final float cx = m.m20(), cy = m.m21(), cz = m.m22();
final float bcx = by * cz - bz * cy, bcy = bz * cx - bx * cz, bcz = bx * cy - by * cx;
final float cax = cy * az - cz * ay, cay = cz * ax - cx * az, caz = cx * ay - cy * ax;
final float abx = ay * bz - az * by, aby = az * bx - ax * bz, abz = ax * by - ay * bx;
final float det = ax * bcx + ay * bcy + az * bcz;
final float frob2 = ax * ax + ay * ay + az * az + bx * bx + by * by + bz * bz + cx * cx + cy * cy + cz * cz;
final float adj2 = bcx * bcx + bcy * bcy + bcz * bcz + cax * cax + cay * cay + caz * caz + abx * abx + aby * aby + abz * abz;
if (!(adj2 > 0f) || det == 0f) return false;
return Math.abs(det) * (float) Math.sqrt(frob2) < FLATNESS_CUTOFF * adj2;
}
public static boolean applies() {
return GLStateManager.isMainThread() && !preLitText && TesrBatchRenderer.INSTANCE.inScope() && GLSMLighting.getLightingState().isEnabled() && !GLStateManager.isRecordingDisplayList()
&& TesrShaderPrograms.packActive() && !ShadowRenderingState.areShadowsCurrentlyBeingRendered() && isFlattened(GLSMMatrices.getModelViewMatrix());
}
}
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff. Show more