Compare commits

...
17 Commits
Author SHA1 Message Date
Jason Mitchell 996fb54cb0 Add a weather API for mods and instance their weather (#2364)
* Mods can supply their own weather columns, drawn alongside vanilla rain and snow
* Mod weather renders instanced with and without shader packs
* NotFine leaf settings apply to Biomes O' Plenty leaves
* Flyby can start in a named biome and fly in survival
2026-11-13 15:03:24 -08:00
Eldrinn Elantey d1e7838cf9 Render fluids stored by LiquidLogged in the chunk mesher (#2356)
* Render fluids stored by LiquidLogged in the chunk mesher

* Update dependencies.gradle.kts
2026-11-13 14:05:32 -08:00
Alexdoru a12d2bb4b0 fix world client leak in MixinRenderBlockConnectedTexturesEthereal (#2360) 2026-11-11 12:33:01 -08:00
Eldrinn Elantey 60d1a3de3c Restore FluidLogged fluid rendering (#2354)
* Restore FluidLogged fluid rendering

* Check region type before reading logged fluid
2026-11-08 18:39:33 -08:00
DarkShadow44 367f9d9161 Fix compatibility with LOTR mod (#2353) 2026-11-08 14:56:07 -08:00
Jason Mitchell d69fd20a8b Tag profiler captures with the Angelica version (#2351)
* RenderDoc, Tracy, async-profiler, spark
2026-11-07 22:49:01 -08:00
Jason Mitchell 9493d630a0 Emulate clip distances on devices without them (#2348) 2026-11-07 13:42:32 -08:00
Jason Mitchell aee989d23b Fix flat item icons on drawers and barrels with shader packs (#2347) 2026-11-07 12:59:19 -08:00
Jason Mitchell 3876218405 Fix GLES crashing at startup and rendering black (#2346) 2026-11-06 21:30:30 -08:00
Armisael5andEclipse-Sol a6d014a9eb Fix falling block texture (#2336)
Co-authored-by: Eclipse-Sol <[email protected]>
2026-11-06 15:25:15 -05:00
Eldrinn Elantey 52444510d0 Move the tilde glyph down in the modern ascii font sheet (#2343) 2026-11-06 09:05:22 -08:00
DarkXFast 8840c2109f Fix replayed "glDrawElements" display lists losing their normals and texcoords (#2328) 2026-11-02 08:40:12 -08:00
Alexdoru e64a4e7214 Fix world client leak in PositionEntries$PlayerPosition (#2327) 2026-11-02 08:35:41 -08:00
Jason Mitchell e46eb46343 Add Tracy frame images (#2326) 2026-11-01 15:09:13 -08:00
Jason Mitchell 5da38c9180 Fix black patches covering the world (#2325) 2026-11-01 12:45:06 -08:00
Rad 85bde5827f Update Polish translation (#2321) 2026-11-01 12:21:47 -08:00
kurrycat ea2d34589e fix thaum mirror render corruption (#2324) 2026-11-01 12:09:09 -08:00
153 changed files with 5522 additions and 976 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
+3
View File
@@ -186,6 +186,8 @@ DependencyHandlerScope.of(dependencies).apply {
compileOnly("mega:fluidlogged-mc1.7.10:0.1.2")
compileOnly("com.github.GTNewHorizons:LiquidLogged:0.2.0:dev") { isTransitive = false }
compileOnly("com.cardinalstar.cubicchunks:CubicChunks1710:v0.1.5-alpha:dev") { isTransitive = false }
}
@@ -210,6 +212,7 @@ afterEvaluate {
glsmTestFixtures(testFixtures(project(":glsm")) as ModuleDependency) { isTransitive = false }
testCompileOnly(testFixtures(project(":glsm")))
testImplementation("mega:fluidlogged-mc1.7.10:0.1.2")
testImplementation("com.github.GTNewHorizons:LiquidLogged:0.2.0:dev") { isTransitive = false }
testRuntimeOnly(libs.fastutil)
testRuntimeOnly(libs.joml)
+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");
@@ -63,6 +66,8 @@ public final class SystemProperties {
public static final String FLYBY_COMMANDS = System.getProperty("angelica.flyby.commands", "");
public static final String FLYBY_ORIGIN = System.getProperty("angelica.flyby.origin", "");
public static final Integer FLYBY_DIMENSION = Integer.getInteger("angelica.flyby.dimension");
public static final String FLYBY_BIOME = System.getProperty("angelica.flyby.biome", "");
public static final boolean FLYBY_SURVIVAL = Boolean.getBoolean("angelica.flyby.survival");
public static final FlybyPacing FLYBY_PACING = parseEnum("angelica.flyby.pacing", FlybyPacing.UNCAPPED, FlybyPacing.UNCAPPED, FlybyPacing.class);
public static final FlybyWeather FLYBY_WEATHER = parseEnum("angelica.flyby.weather", FlybyWeather.CLEAR, FlybyWeather.CLEAR, FlybyWeather.class);
public static final FlybyCamera FLYBY_CAMERA = parseEnum("angelica.flyby.camera", FlybyCamera.FIRST_PERSON, FlybyCamera.FIRST_PERSON, FlybyCamera.class);
@@ -25,11 +25,21 @@ 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 static boolean toolAttached = TOOL_ATTACHED;
private CaptureGate() {}
public static void refresh() {
markersThisFrame = gateValue(FORCED, TOOL_ATTACHED, Tracy.isConnected());
tracyConnected = Tracy.isConnected();
markersThisFrame = gateValue(FORCED, toolAttached, tracyConnected);
}
static void probeGlTool() {
if (RenderSystem.hasExtension("GL_EXT_debug_tool")) {
toolAttached = true;
LOGGER.info("Capture tool detected via GL_EXT_debug_tool; instrumentation enabled");
}
}
static boolean gateValue(boolean forced, boolean toolAttached, boolean tracyConnected) {
@@ -37,7 +47,7 @@ public final class CaptureGate {
}
public static boolean enabledAtStartup() {
return FORCED || TOOL_ATTACHED;
return FORCED || toolAttached;
}
private static boolean detectCaptureTool() {
@@ -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);
}
@@ -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,
@@ -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;
}
}
@@ -458,6 +458,7 @@ public class GLStateManager {
static void init(Runnable initCallback) {
final GLContextState glCtx = ctx();
RenderSystem.initRenderer();
CaptureGate.probeGlTool();
GLSMFramebuffer.queryDefaultDrawBuffer();
@@ -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,11 +439,11 @@ public class RenderSystem {
return supportsImageLoadStore;
}
public static boolean hasClipCullDistance() {
return hasClipCullDistance;
public static boolean isClipDistanceEmulated() {
return clipDistanceEmulated;
}
private static boolean hasExtension(String name) {
static boolean hasExtension(String name) {
final int count = GLSMQueries.glGetInteger(GL30.GL_NUM_EXTENSIONS);
for (int i = 0; i < count; i++) {
if (name.equals(GLSMQueries.glGetStringi(GL11.GL_EXTENSIONS, i))) return true;
@@ -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");
}
}
@@ -108,11 +108,13 @@ public final class FFPUniformBlock {
public static final int FOG_PARAMS = vec4("u_FogParams");
public static final int FOG_COLOR = vec4("u_FogColor");
// Rain and snow, for Instancing.WEATHER. 0 = (translate.xyz, 1/radius),
// 1 = (cameraFrac.xy, partialTicks, age), 2 = (rainScroll, snowScroll, rainStrength, unused).
// Weather columns, for Instancing.WEATHER. 0 = (translate.xyz, 1/radius),
// 1 = (cameraFrac.xy, partialTicks, age), 2 = (vanillaTick, providerTick, rainStrength, providerAge),
// 3 = (provider 1/radius, unused, unused, unused).
public static final int WEATHER_PARAMS_0 = vec4("u_WeatherParams0");
public static final int WEATHER_PARAMS_1 = vec4("u_WeatherParams1");
public static final int WEATHER_PARAMS_2 = vec4("u_WeatherParams2");
public static final int WEATHER_PARAMS_3 = vec4("u_WeatherParams3");
public static final int SIZE;
public static final String GLSL_DECL;
@@ -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");
}
@@ -39,17 +39,16 @@ public final class InstancedGlslHelpers {
vec2 {p}wColumn = {span}.xy;
float {p}wBottom = {span}.z;
float {p}wTop = {span}.w;
vec2 {p}wLightmap = {params}.xy;
float {p}wHash = {params}.z;
bool {p}wSnow = {params}.w > 0.5;
float {p}wLightV = floor({params}.x / 65536.0);
vec2 {p}wLightmap = vec2({params}.x - {p}wLightV * 65536.0, {p}wLightV);
bool {p}wProvider = {params}.w > 0.5;
vec3 {p}wTranslate = {p}WeatherParams0.xyz;
float {p}wInvRadius = {p}WeatherParams0.w;
float {p}wInvRadius = {p}wProvider ? {p}WeatherParams3.x : {p}WeatherParams0.w;
vec2 {p}wCameraFrac = {p}WeatherParams1.xy;
float {p}wPartialTicks = {p}WeatherParams1.z;
float {p}wAge = {p}WeatherParams1.w;
float {p}wRainScroll = {p}WeatherParams2.x;
float {p}wSnowScroll = {p}WeatherParams2.y;
float {p}wStrength = {p}WeatherParams2.z;
float {p}wAge = {p}wProvider ? {p}WeatherParams2.w : {p}WeatherParams1.w;
float {p}wTick = {p}wProvider ? {p}WeatherParams2.y : {p}WeatherParams2.x;
float {p}wStrength = {p}wProvider ? 1.0 : {p}WeatherParams2.z;
vec2 {p}wHalfWidth = vec2(-{p}wColumn.y, {p}wColumn.x) * (0.5 / sqrt(dot({p}wColumn, {p}wColumn)));
vec2 {p}wCornerXZ = {p}wColumn + vec2(0.5) + ({corner}.x * 2.0 - 1.0) * {p}wHalfWidth;
@@ -58,22 +57,13 @@ public final class InstancedGlslHelpers {
float {p}wDistance = length({p}wColumn + vec2(0.5) - {p}wCameraFrac) * {p}wInvRadius;
float {p}wFade = 1.0 - {p}wDistance * {p}wDistance;
float {p}wScroll;
vec2 {p}wDrift;
float {p}wAlpha;
if ({p}wSnow) {
{p}wScroll = ({p}wSnowScroll + {p}wPartialTicks) / 512.0;
{p}wDrift = vec2({jitter}.x + {p}wAge * 0.01 * {jitter}.y, {jitter}.z + {p}wAge * {jitter}.w * 0.001);
{p}wAlpha = {p}wFade * 0.3 + 0.5;
} else {
{p}wScroll = (mod({p}wRainScroll + {p}wHash, 32.0) + {p}wPartialTicks) / 32.0 * (3.0 + {jitter}.x);
{p}wDrift = vec2(0.0);
{p}wAlpha = {p}wFade * 0.5 + 0.5;
}
float {p}wScroll = (mod({p}wTick + {params}.y, {layer}.x) + {p}wPartialTicks) / {layer}.x * {params}.z;
vec2 {p}wDrift = vec2({jitter}.x + {p}wAge * {layer}.y * {jitter}.y, {jitter}.z + {p}wAge * {layer}.z * {jitter}.w);
float {p}wAlpha = {p}wFade * {layer}.w + 0.5;
{tex0} = vec4({corner}.x + {p}wDrift.x, {p}wCornerY * 0.25 + {p}wScroll + {p}wDrift.y, 0.0, 1.0);
{tex1} = vec4({p}wLightmap, 0.0, 1.0);
{color} = vec4(1.0, 1.0, 1.0, {p}wAlpha * {p}wStrength);""";
{color} = floor(clamp(vec4({colorAttr}.rgb, {p}wAlpha * {colorAttr}.a * {p}wStrength), 0.0, 1.0) * 255.0) / 255.0;""";
public static String weatherPrologue(String uniformPrefix, String attribPrefix, String corner,
String vertex, String color, String tex0, String tex1, String sep) {
@@ -82,6 +72,8 @@ public final class InstancedGlslHelpers {
.replace("{span}", attribPrefix + "InstColumnSpan")
.replace("{jitter}", attribPrefix + "InstJitter")
.replace("{params}", attribPrefix + "InstWeatherParams")
.replace("{layer}", attribPrefix + "InstWeatherLayer")
.replace("{colorAttr}", attribPrefix + "InstWeatherColor")
.replace("{corner}", corner)
.replace("{vertex}", vertex)
.replace("{color}", color)
@@ -125,6 +117,8 @@ public final class InstancedGlslHelpers {
decls.add(decl(WeatherInstancedAttribs.LOC_COLUMN_SPAN, "vec4", prefix + "InstColumnSpan"));
decls.add(decl(WeatherInstancedAttribs.LOC_JITTER, "vec4", prefix + "InstJitter"));
decls.add(decl(WeatherInstancedAttribs.LOC_PARAMS, "vec4", prefix + "InstWeatherParams"));
decls.add(decl(WeatherInstancedAttribs.LOC_COLOR, "vec4", prefix + "InstWeatherColor"));
decls.add(decl(WeatherInstancedAttribs.LOC_LAYER, "vec4", prefix + "InstWeatherLayer"));
}
}
return decls.toArray(String[]::new);
@@ -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));
@@ -394,8 +391,9 @@ public class Uniforms {
WeatherParams.translateZ, WeatherParams.invRadius);
putVec4(FFPUniformBlock.WEATHER_PARAMS_1, WeatherParams.cameraFracX, WeatherParams.cameraFracZ,
WeatherParams.partialTicks, WeatherParams.age);
putVec4(FFPUniformBlock.WEATHER_PARAMS_2, WeatherParams.rainScroll, WeatherParams.snowScroll,
WeatherParams.rainStrength, 0.0f);
putVec4(FFPUniformBlock.WEATHER_PARAMS_2, WeatherParams.vanillaTick, WeatherParams.providerTick,
WeatherParams.rainStrength, WeatherParams.providerAge);
putVec4(FFPUniformBlock.WEATHER_PARAMS_3, WeatherParams.providerInvRadius, 0.0f, 0.0f, 0.0f);
}
private void putVec4(int offset, Vector4f v) {
@@ -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);
}
}
@@ -3,31 +3,76 @@ package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
/**
* One rain or snow column per instance.
* One weather column per instance.
*/
public final class WeatherInstancedAttribs {
public static final int LOC_COLUMN_SPAN = ParticleInstancedAttribs.LOC_CENTER_HALF;
public static final int LOC_JITTER = ParticleInstancedAttribs.LOC_UV;
public static final int LOC_PARAMS = InstancedAttribs.LOC_COLOR;
public static final int LOC_COLOR = InstancedAttribs.LOC_ROW2;
public static final int LOC_LAYER = InstancedAttribs.LOC_OVERLAY;
public static final int OFFSET_COLUMN_SPAN = 0;
public static final int OFFSET_JITTER = 16;
public static final int OFFSET_PARAMS = 32;
public static final int STRIDE = 48;
public static final int OFFSET_COLOR = 48;
public static final int OFFSET_LAYER = 64;
public static final int STRIDE = 80;
public static final int RAIN_PERIOD = 32;
public static final float RAIN_SCROLL_BASE = 3.0F;
public static final float RAIN_FADE = 0.5F;
public static final int SNOW_PERIOD = 512;
public static final float SNOW_U_DRIFT = 0.01F;
public static final float SNOW_V_DRIFT = 0.001F;
public static final float SNOW_FADE = 0.3F;
public static final float SOURCE_VANILLA = 0.0F;
public static final float SOURCE_PROVIDER = 1.0F;
public static void putColumn(ByteBuffer data, int index, float x, float z, float bottom, float top, float uBase, float uDrift,
float vBase, float vDrift, float brightness, int scrollOffset, float scrollScale, float source, float r, float g, float b,
float a, int period, float uDriftScale, float vDriftScale, float fadeScale) {
final int base = index * STRIDE;
data.putFloat(base + OFFSET_COLUMN_SPAN, x);
data.putFloat(base + OFFSET_COLUMN_SPAN + 4, z);
data.putFloat(base + OFFSET_COLUMN_SPAN + 8, bottom);
data.putFloat(base + OFFSET_COLUMN_SPAN + 12, top);
data.putFloat(base + OFFSET_JITTER, uBase);
data.putFloat(base + OFFSET_JITTER + 4, uDrift);
data.putFloat(base + OFFSET_JITTER + 8, vBase);
data.putFloat(base + OFFSET_JITTER + 12, vDrift);
data.putFloat(base + OFFSET_PARAMS, brightness);
data.putFloat(base + OFFSET_PARAMS + 4, scrollOffset & (period - 1));
data.putFloat(base + OFFSET_PARAMS + 8, scrollScale);
data.putFloat(base + OFFSET_PARAMS + 12, source);
data.putFloat(base + OFFSET_COLOR, r);
data.putFloat(base + OFFSET_COLOR + 4, g);
data.putFloat(base + OFFSET_COLOR + 8, b);
data.putFloat(base + OFFSET_COLOR + 12, a);
data.putFloat(base + OFFSET_LAYER, period);
data.putFloat(base + OFFSET_LAYER + 4, uDriftScale);
data.putFloat(base + OFFSET_LAYER + 8, vDriftScale);
data.putFloat(base + OFFSET_LAYER + 12, fadeScale);
}
public static void pointInstanceAttribs(long base) {
GLSMVertexInput.glVertexAttribPointer(LOC_COLUMN_SPAN, 4, GL11.GL_FLOAT, false, STRIDE, base + OFFSET_COLUMN_SPAN);
GLSMVertexInput.glVertexAttribPointer(LOC_JITTER, 4, GL11.GL_FLOAT, false, STRIDE, base + OFFSET_JITTER);
GLSMVertexInput.glVertexAttribPointer(LOC_PARAMS, 4, GL11.GL_FLOAT, false, STRIDE, base + OFFSET_PARAMS);
GLSMVertexInput.glVertexAttribPointer(LOC_COLOR, 4, GL11.GL_FLOAT, false, STRIDE, base + OFFSET_COLOR);
GLSMVertexInput.glVertexAttribPointer(LOC_LAYER, 4, GL11.GL_FLOAT, false, STRIDE, base + OFFSET_LAYER);
}
static void enableInstanceArrays() {
InstancedAttribs.enableInstanced(LOC_COLUMN_SPAN);
InstancedAttribs.enableInstanced(LOC_JITTER);
InstancedAttribs.enableInstanced(LOC_PARAMS);
InstancedAttribs.enableInstanced(LOC_COLOR);
InstancedAttribs.enableInstanced(LOC_LAYER);
}
private WeatherInstancedAttribs() {}
@@ -1,7 +1,7 @@
package com.gtnewhorizons.angelica.glsm.ffp;
/**
* The per-frame half of the rain and snow instancing, set once per weather draw.
* The per-frame half of the weather column instancing, set once per frame.
*/
public final class WeatherParams {
@@ -15,9 +15,14 @@ public final class WeatherParams {
public static float partialTicks;
public static float age;
public static float rainScroll;
public static float snowScroll;
public static float vanillaTick;
public static float providerTick;
public static float rainStrength;
public static float providerAge;
public static float providerInvRadius;
public static final int TICK_WRAP = 1024;
public static int generation;
@@ -25,7 +30,7 @@ public final class WeatherParams {
public static void set(float translateX, float translateY, float translateZ, float invRadius,
float cameraFracX, float cameraFracZ, float partialTicks, float age,
float rainScroll, float snowScroll, float rainStrength) {
int vanillaTick, float rainStrength, int providerTick, float providerAge, float providerInvRadius) {
WeatherParams.translateX = translateX;
WeatherParams.translateY = translateY;
WeatherParams.translateZ = translateZ;
@@ -34,9 +39,11 @@ public final class WeatherParams {
WeatherParams.cameraFracZ = cameraFracZ;
WeatherParams.partialTicks = partialTicks;
WeatherParams.age = age;
WeatherParams.rainScroll = rainScroll;
WeatherParams.snowScroll = snowScroll;
WeatherParams.vanillaTick = vanillaTick & (TICK_WRAP - 1);
WeatherParams.providerTick = providerTick & (TICK_WRAP - 1);
WeatherParams.rainStrength = rainStrength;
WeatherParams.providerAge = providerAge;
WeatherParams.providerInvRadius = providerInvRadius;
generation++;
}
}
@@ -11,7 +11,7 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* The four corners every rain and snow column is expanded from. x picks the end of the billboard, y picks
* The four corners every weather column is expanded from. x picks the end of the billboard, y picks
* the bottom or top edge.
*/
public final class WeatherQuadMesh {
@@ -49,14 +49,6 @@ public final class WeatherQuadMesh {
return vao;
}
public static void bindInstanceBuffer(int bufferId) {
GLSMVertexInput.glBindVertexArray(vao());
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, bufferId);
WeatherInstancedAttribs.pointInstanceAttribs(0L);
GLSMVertexInput.glBindVertexArray(0);
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
}
public static void delete() {
if (vbo != 0) {
GLSMBuffers.glDeleteBuffers(vbo);
@@ -6,6 +6,7 @@ import com.gtnewhorizons.angelica.glsm.GLDebug;
import com.gtnewhorizons.angelica.glsm.GLSMTextureLifetime;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
@@ -16,11 +17,18 @@ public final class FrameHooks {
private static boolean gameLoopStarted;
private static long frameCounter;
private static boolean frameGroupActive;
private static String framePrefix = "frame:";
private static String buildLabel = "";
public static void bootstrapFirstFrame() {
DisplayListManager.abortIfLeaked();
if (!gameLoopStarted) {
gameLoopStarted = true;
final GLSMInitConfig config = GLStateManager.getInitConfig();
if (config != null && !config.getBuildLabel().isEmpty()) {
buildLabel = config.getBuildLabel();
framePrefix = buildLabel + " frame:";
}
RENDER_BACKEND.onFrameBegin();
pushFrameGroup();
}
@@ -48,8 +56,10 @@ public final class FrameHooks {
}
private static void pushFrameGroup() {
final boolean wasConnected = CaptureGate.tracyConnected;
CaptureGate.refresh();
GLDebug.pushGroup("frame:", frameCounter);
if (CaptureGate.tracyConnected && !wasConnected && !buildLabel.isEmpty()) Tracy.message(buildLabel);
GLDebug.pushGroup(framePrefix, frameCounter);
frameGroupActive = true;
}
@@ -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();
@@ -17,6 +17,7 @@ public final class GLSMInitConfig {
@Getter private final Runnable postInitCallback;
private final boolean dsaEnabled;
@Getter private final boolean noErrorChecks;
@Getter private final String buildLabel;
private GLSMInitConfig(Builder builder) {
this.lwjglDebug = builder.lwjglDebug;
@@ -27,6 +28,7 @@ public final class GLSMInitConfig {
this.displayHeight = builder.displayHeight;
this.postInitCallback = builder.postInitCallback;
this.dsaEnabled = builder.dsaEnabled;
this.buildLabel = builder.buildLabel;
}
public boolean isDSAEnabled() {
@@ -46,6 +48,7 @@ public final class GLSMInitConfig {
private Runnable postInitCallback = null;
private boolean dsaEnabled = true;
private boolean noErrorChecks;
private String buildLabel = "";
private Builder() {}
@@ -85,6 +88,11 @@ public final class GLSMInitConfig {
return this;
}
public Builder buildLabel(String buildLabel) {
this.buildLabel = buildLabel == null ? "" : buildLabel;
return this;
}
public GLSMInitConfig build() {
return new GLSMInitConfig(this);
}
@@ -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);
@@ -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;
@@ -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));
}
}
@@ -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));
@@ -100,6 +100,7 @@ class InstancedFFPDrawGLTest {
CubeParityFixture.disableDirectionalLight();
CubeParityFixture.deleteResources();
ParticleParityFixture.deleteResources();
WeatherParityFixture.deleteResources();
if (templateVbo != 0) { GLSMBuffers.glDeleteBuffers(templateVbo); templateVbo = 0; }
if (instanceVbo != 0) { GLSMBuffers.glDeleteBuffers(instanceVbo); instanceVbo = 0; }
if (vao != 0) { GLSMVertexInput.glDeleteVertexArrays(vao); vao = 0; }
@@ -196,6 +197,27 @@ class InstancedFFPDrawGLTest {
ParticleParityFixture.assertPixelParity(reference, instanced, PARITY_SIZE, 0xFF000000, pixel -> "0x" + Integer.toHexString(pixel));
}
@Test
void weatherColumnsMatchTessellatedReference() {
GLSMRasterizer.glViewport(0, 0, PARITY_SIZE, PARITY_SIZE);
WeatherParityFixture.setupScene();
FfpFixture.activateFfp();
final WeatherParityFixture.Spec[] specs = WeatherParityFixture.specs();
FfpFixture.clear();
WeatherParityFixture.drawReference(specs);
final int[] reference = FfpFixture.readRegion(PARITY_SIZE);
FfpFixture.clear();
WeatherParityFixture.drawInstanced(specs);
final int[] instanced = FfpFixture.readRegion(PARITY_SIZE);
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "parity draws must not raise a GL error");
WeatherParityFixture.assertPixelParity(reference, instanced, PARITY_SIZE, 0xFF000000, pixel -> "0x" + Integer.toHexString(pixel));
}
@Test
void entityAttribSelectsColorPerInstance() {
GLSMRasterizer.glViewport(0, 0, INTEGER_WIDTH, INTEGER_HEIGHT);
@@ -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,292 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizons.angelica.glsm.GLSMBlend;
import com.gtnewhorizons.angelica.glsm.GLSMBuffers;
import com.gtnewhorizons.angelica.glsm.GLSMDraws;
import com.gtnewhorizons.angelica.glsm.GLSMEnable;
import com.gtnewhorizons.angelica.glsm.GLSMMatrices;
import com.gtnewhorizons.angelica.glsm.GLSMPrograms;
import com.gtnewhorizons.angelica.glsm.GLSMTextures;
import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.function.IntFunction;
public final class WeatherParityFixture {
public record Spec(float x, float z, float bottom, float top, float uBase, float uDrift, float vBase, float vDrift,
int brightness, int scrollOffset, float scrollScale, float source,
float r, float g, float b, float a, int period, float uDriftScale, float vDriftScale, float fadeScale) {}
private static final int TICK = 2650;
private static final float PARTIAL_TICKS = 0.375f;
private static final float TRANSLATE_X = 0.37f;
private static final float TRANSLATE_Y = -62.25f;
private static final float TRANSLATE_Z = -0.61f;
private static final float INV_RADIUS = 1.0f / 6.0f;
private static final float CAMERA_FRAC_X = 0.3f;
private static final float CAMERA_FRAC_Z = 0.7f;
private static final float STRENGTH = 0.8f;
private static final int PROVIDER_TICK = 1777;
private static final float PROVIDER_INV_RADIUS = 1.0f / 9.0f;
private static final float[] CORNER_X = { 0.0f, 1.0f, 1.0f, 0.0f };
private static final float[] CORNER_Y = { 0.0f, 0.0f, 1.0f, 1.0f };
private static final int REF_STRIDE = 44;
private static int referenceVao;
private static int referenceVbo;
private static int instanceVbo;
private static int weatherTexture;
private static int lightmapTexture;
private WeatherParityFixture() {}
private static Spec rain(float x, float z, float bottom, float top, float j0, int hash, int brightness) {
return new Spec(x, z, bottom, top, 0f, 0f, 0f, 0f, brightness, hash, WeatherInstancedAttribs.RAIN_SCROLL_BASE + j0, WeatherInstancedAttribs.SOURCE_VANILLA,
1f, 1f, 1f, 1f, WeatherInstancedAttribs.RAIN_PERIOD, 0f, 0f, WeatherInstancedAttribs.RAIN_FADE);
}
private static Spec snow(float x, float z, float bottom, float top, float j0, float j1, float j2, float j3, int brightness) {
return new Spec(x, z, bottom, top, j0, j1, j2, j3, brightness, 0, 1f, WeatherInstancedAttribs.SOURCE_VANILLA,
1f, 1f, 1f, 1f, WeatherInstancedAttribs.SNOW_PERIOD, WeatherInstancedAttribs.SNOW_U_DRIFT, WeatherInstancedAttribs.SNOW_V_DRIFT,
WeatherInstancedAttribs.SNOW_FADE);
}
private static Spec provider(float x, float z, float bottom, float top, float uBase, float uDrift, float vBase, float vDrift,
int brightness, int scrollOffset, float scrollScale, float r, float g, float b, float a,
int period, float uDriftScale, float vDriftScale, float fadeScale) {
return new Spec(x, z, bottom, top, uBase, uDrift, vBase, vDrift, brightness, scrollOffset, scrollScale, WeatherInstancedAttribs.SOURCE_PROVIDER, r, g, b, a,
period, uDriftScale, vDriftScale, fadeScale);
}
public static Spec[] specs() {
return new Spec[] {
rain(-4f, -3f, 58f, 66f, 0.31f, 5, 0x00F00030),
rain(2f, -4f, 60f, 67f, 0.77f, 19, 0x00A000D0),
rain(-2f, 3f, 57f, 65f, 0.05f, 62, 0x0060F0A0),
rain(4f, 2f, 59f, 64f, 0.58f, 11, 0x00D01090),
rain(1f, 4f, 56f, 66f, 0.93f, 24, 0x0080C0F0),
snow(-5f, 1f, 58f, 66f, 0.12f, 0.47f, 0.83f, 0.29f, 0x00E02070),
snow(3f, -2f, 60f, 68f, 0.66f, 0.91f, 0.18f, 0.74f, 0x00B0B0B0),
snow(-1f, -5f, 57f, 63f, 0.39f, 0.25f, 0.52f, 0.96f, 0x0040E0C0),
snow(5f, 4f, 59f, 67f, 0.84f, 0.63f, 0.07f, 0.41f, 0x00F0F0F0),
snow(-3f, 5f, 55f, 65f, 0.22f, 0.78f, 0.35f, 0.58f, 0x00109050),
provider(-5f, -4f, 58f, 66f, 0.14f, 0.52f, 0.71f, 0.33f, 0x00C06040, 0, 1f, 1f, 1f, 1f, 1f, 512, 0.03f, 0.001f, 0.3f),
provider(4f, -3f, 59f, 67f, 0.61f, 0.27f, 0.09f, 0.88f, 0x00F020B0, 0, 1f, 1f, 1f, 1f, 1f, 512, 0.03f, 0.001f, 0.3f),
provider(-4f, 2f, 57f, 65f, 0.42f, 0.36f, 0.64f, 0.19f, 0x0090D070, 0, 0f, 0.12f, 0.27f, 0.18f, 2.0f, 512, 0.03f, 0.003f, 0f),
provider(2f, 5f, 60f, 66f, 0.83f, 0.74f, 0.25f, 0.57f, 0x0050A0E0, 0, 0f, 0.29f, 0.14f, 0.21f, 2.0f, 512, 0.03f, 0.003f, 0f),
provider(-2f, -5f, 56f, 64f, 0.07f, 0.45f, 0.93f, 0.62f, 0x00B0F040, 0, -1f, 0.83f, 0.92f, 0.88f, 1f, 1024, 0.001f, 0.001f, 0.3f),
provider(5f, 1f, 58f, 68f, 0.55f, 0.16f, 0.38f, 0.81f, 0x007070F0, 0, -1f, 0.97f, 0.86f, 0.81f, 1f, 1024, 0.001f, 0.001f, 0.3f),
provider(-5f, 4f, 59f, 66f, 0f, 0.68f, 0f, 0.23f, 0x00E0E050, 0, -1f, 1f, 1f, 1f, 0.35f, 512, 0f, 0.01f, 0.3f),
provider(3f, 3f, 57f, 67f, 0f, 0.49f, 0f, 0.91f, 0x0030C0A0, 0, -1f, 1f, 1f, 1f, 0.82f, 512, 0f, 0.01f, 0.3f),
provider(0f, -4f, 58f, 65f, 0.26f, 0.12f, 0.67f, 0.44f, 0x00A0A0A0, 21, 3.46f, 1f, 1f, 1f, 1f, 32, 0f, 0f, 0.3f),
provider(1f, 3f, 60f, 66f, 0.89f, 0.31f, 0.15f, 0.76f, 0x00F0A040, 7, 3.81f, 1f, 1f, 1f, 1f, 32, 0f, 0f, 0.3f),
provider(-3f, 0f, 56f, 67f, 0.33f, 0.85f, 0.48f, 0.13f, 0x00D0D0D0, 0, 1f, 1f, 1f, 1f, 0.24f, 512, 0.01f, 0.01f, 0.3f),
provider(0f, 5f, 58f, 64f, 0.72f, 0.04f, 0.59f, 0.97f, 0x0010F0F0, 0, 1f, 1f, 1f, 1f, 0.67f, 512, 0.01f, 0.01f, 0.3f),
};
}
private static float mod(float x, float y) {
return x - y * (float) Math.floor(x / y);
}
public static void setupScene() {
GLSMEnable.glDisable(GL11.GL_FOG);
GLSMEnable.glDisable(GL11.GL_ALPHA_TEST);
GLSMEnable.glDisable(GL11.GL_DEPTH_TEST);
GLSMEnable.glDisable(GL11.GL_CULL_FACE);
GLSMEnable.glDisable(GL11.GL_LIGHTING);
GLSMEnable.glEnable(GL11.GL_BLEND);
GLSMBlend.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GLSMMatrices.glMatrixMode(GL11.GL_PROJECTION);
GLSMMatrices.glLoadIdentity();
GLSMMatrices.glOrtho(-8.0, 8.0, -8.0, 8.0, -64.0, 64.0);
GLSMMatrices.glMatrixMode(GL11.GL_MODELVIEW);
GLSMMatrices.glLoadIdentity();
GLSMMatrices.glRotatef(30.0f, 1.0f, 0.0f, 0.0f);
GLSMMatrices.glRotatef(35.0f, 0.0f, 1.0f, 0.0f);
lightmapTexture = createTexture(GL13.GL_TEXTURE1, 16, GL11.GL_LINEAR, GL11.GL_CLAMP);
GLSMMatrices.glMatrixMode(GL11.GL_TEXTURE);
GLSMMatrices.glLoadIdentity();
GLSMMatrices.glScalef(0.00390625f, 0.00390625f, 0.00390625f);
GLSMMatrices.glTranslatef(8.0f, 8.0f, 8.0f);
GLSMMatrices.glMatrixMode(GL11.GL_MODELVIEW);
weatherTexture = createTexture(GL13.GL_TEXTURE0, 4, GL11.GL_NEAREST, GL11.GL_REPEAT);
GLSMMatrices.glMatrixMode(GL11.GL_TEXTURE);
GLSMMatrices.glLoadIdentity();
GLSMMatrices.glMatrixMode(GL11.GL_MODELVIEW);
}
private static int createTexture(int unit, int size, int filter, int wrap) {
final int id = GLSMTextures.glGenTextures();
GLSMTextures.glActiveTexture(unit);
GLSMTextures.glBindTexture(GL11.GL_TEXTURE_2D, id);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, filter);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, filter);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, wrap);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, wrap);
final ByteBuffer texels = BufferUtils.createByteBuffer(size * size * 4);
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
texels.put((byte) (40 + x * (200 / size)));
texels.put((byte) (40 + y * (200 / size)));
texels.put((byte) (255 - (x + y) * (100 / size)));
texels.put((byte) 255);
}
}
texels.flip();
GLSMTextures.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, size, size, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, texels);
GLSMEnable.glEnable(GL11.GL_TEXTURE_2D);
return id;
}
private static void putVertex(ByteBuffer data, float px, float py, float pz, float r, float g, float b, float a,
float u, float v, float lmX, float lmY) {
final int base = data.position();
data.putFloat(base, px);
data.putFloat(base + 4, py);
data.putFloat(base + 8, pz);
data.putFloat(base + 12, r);
data.putFloat(base + 16, g);
data.putFloat(base + 20, b);
data.putFloat(base + 24, a);
data.putFloat(base + 28, u);
data.putFloat(base + 32, v);
data.putFloat(base + 36, lmX);
data.putFloat(base + 40, lmY);
data.position(base + REF_STRIDE);
}
private static float tessellatorByte(float channel) {
final int value = (int) (channel * 255.0F);
return Math.max(0, Math.min(255, value)) / 255.0f;
}
private static void addColumn(ByteBuffer data, Spec s) {
final boolean provider = s.source() == WeatherInstancedAttribs.SOURCE_PROVIDER;
final int tick = provider ? PROVIDER_TICK : TICK;
final float age = tick + PARTIAL_TICKS;
final float invRadius = provider ? PROVIDER_INV_RADIUS : INV_RADIUS;
final float strength = provider ? 1.0f : STRENGTH;
final float halfScale = 0.5f / (float) Math.sqrt(s.x() * s.x() + s.z() * s.z());
final float halfX = -s.z() * halfScale;
final float halfZ = s.x() * halfScale;
final float dx = s.x() + 0.5f - CAMERA_FRAC_X;
final float dz = s.z() + 0.5f - CAMERA_FRAC_Z;
final float distance = (float) Math.sqrt(dx * dx + dz * dz) * invRadius;
final float fade = 1.0f - distance * distance;
final float scroll = (mod((float) tick + s.scrollOffset(), s.period()) + PARTIAL_TICKS) / s.period() * s.scrollScale();
final float driftU = s.uBase() + age * s.uDriftScale() * s.uDrift();
final float driftV = s.vBase() + age * s.vDriftScale() * s.vDrift();
final float alpha = fade * s.fadeScale() + 0.5f;
final float r = tessellatorByte(s.r());
final float g = tessellatorByte(s.g());
final float b = tessellatorByte(s.b());
final float a = tessellatorByte(alpha * s.a() * strength);
final float lmX = s.brightness() & 0xFFFF;
final float lmY = s.brightness() >> 16 & 0xFFFF;
for (int i = 0; i < 4; i++) {
final float side = CORNER_X[i] * 2.0f - 1.0f;
final float cornerX = s.x() + 0.5f + side * halfX;
final float cornerZ = s.z() + 0.5f + side * halfZ;
final float cornerY = s.bottom() * (1.0f - CORNER_Y[i]) + s.top() * CORNER_Y[i];
putVertex(data, cornerX + TRANSLATE_X, cornerY + TRANSLATE_Y, cornerZ + TRANSLATE_Z, r, g, b, a,
CORNER_X[i] + driftU, cornerY * 0.25f + scroll + driftV, lmX, lmY);
}
}
private static ByteBuffer buildReferenceData(Spec[] specs) {
final ByteBuffer data = BufferUtils.createByteBuffer(specs.length * 4 * REF_STRIDE);
for (Spec s : specs) {
addColumn(data, s);
}
data.flip();
return data;
}
public static void drawReference(Spec[] specs) {
final ByteBuffer data = buildReferenceData(specs);
if (referenceVao == 0) referenceVao = GLSMVertexInput.glGenVertexArrays();
GLSMVertexInput.glBindVertexArray(referenceVao);
if (referenceVbo == 0) referenceVbo = GLSMBuffers.glGenBuffers();
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, referenceVbo);
GLSMBuffers.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STREAM_DRAW);
FfpFixture.attrib(0, 3, GL11.GL_FLOAT, false, REF_STRIDE, 0);
FfpFixture.attrib(1, 4, GL11.GL_FLOAT, false, REF_STRIDE, 12);
FfpFixture.attrib(2, 2, GL11.GL_FLOAT, false, REF_STRIDE, 28);
FfpFixture.attrib(3, 2, GL11.GL_FLOAT, false, REF_STRIDE, 36);
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
VAOManager.setCurrentVertexFlags(VertexFlags.COLOR_BIT | VertexFlags.TEXTURE_BIT | VertexFlags.BRIGHTNESS_BIT);
GLSMDraws.glDrawArrays(GL11.GL_QUADS, 0, specs.length * 4);
GLSMVertexInput.glBindVertexArray(0);
}
public static void drawInstanced(Spec[] specs) {
final ByteBuffer records = BufferUtils.createByteBuffer(specs.length * WeatherInstancedAttribs.STRIDE);
for (int i = 0; i < specs.length; i++) {
final Spec s = specs[i];
WeatherInstancedAttribs.putColumn(records, i, s.x(), s.z(), s.bottom(), s.top(), s.uBase(), s.uDrift(), s.vBase(), s.vDrift(),
s.brightness(), s.scrollOffset(), s.scrollScale(), s.source(), s.r(), s.g(), s.b(), s.a(),
s.period(), s.uDriftScale(), s.vDriftScale(), s.fadeScale());
}
WeatherParams.set(TRANSLATE_X, TRANSLATE_Y, TRANSLATE_Z, INV_RADIUS, CAMERA_FRAC_X, CAMERA_FRAC_Z,
PARTIAL_TICKS, TICK + PARTIAL_TICKS, TICK, STRENGTH, PROVIDER_TICK, PROVIDER_TICK + PARTIAL_TICKS, PROVIDER_INV_RADIUS);
GLSMVertexInput.glBindVertexArray(WeatherQuadMesh.vao());
if (instanceVbo == 0) instanceVbo = GLSMBuffers.glGenBuffers();
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, instanceVbo);
GLSMBuffers.glBufferData(GL15.GL_ARRAY_BUFFER, records, GL15.GL_STREAM_DRAW);
WeatherInstancedAttribs.pointInstanceAttribs(0L);
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
VAOManager.setCurrentVertexFlags(WeatherQuadMesh.VERTEX_FLAGS);
GLSMPrograms.setFfpInstancing(Instancing.WEATHER);
GLSMDraws.glDrawArraysInstanced(GL11.GL_TRIANGLE_FAN, 0, WeatherQuadMesh.VERTEX_COUNT, specs.length);
GLSMPrograms.setFfpInstancing(Instancing.NONE);
GLSMVertexInput.glBindVertexArray(0);
}
public static void assertPixelParity(int[] reference, int[] instanced, int size, int background, IntFunction<String> describe) {
FfpFixture.assertPixelParity("instanced weather columns", reference, instanced, size, background, 16, 0, describe);
}
public static void deleteResources() {
if (instanceVbo != 0) {
GLSMBuffers.glDeleteBuffers(instanceVbo);
instanceVbo = 0;
}
if (referenceVbo != 0) {
GLSMBuffers.glDeleteBuffers(referenceVbo);
referenceVbo = 0;
}
if (referenceVao != 0) {
GLSMVertexInput.glDeleteVertexArrays(referenceVao);
referenceVao = 0;
}
if (weatherTexture != 0) {
GLSMTextures.glDeleteTextures(weatherTexture);
weatherTexture = 0;
}
if (lightmapTexture != 0) {
GLSMTextures.glDeleteTextures(lightmapTexture);
lightmapTexture = 0;
}
WeatherQuadMesh.delete();
}
}
@@ -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);
}
}
}
+1
View File
@@ -36,6 +36,7 @@ tasks.withType<JavaExec>().matching { it.name.startsWith("runClient") }.configur
for (name in forwardedRunProperties) {
jvmArgs("-D$name=${System.getProperty(name)}")
}
jvmArgs("-Dangelica.buildVersion=${project.version}")
}
val isMacOs = org.gradle.internal.os.OperatingSystem.current().isMacOsX
+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;
}
}
+2
View File
@@ -11,6 +11,8 @@
# extra - passed through to gradle; -Dangelica.* reaches the client (e.g. -Dangelica.flyby.timeOfDay=1500).
# -Dangelica.flyby.camera=FIRST_PERSON|THIRD_PERSON_BACK|THIRD_PERSON_FRONT (default FIRST_PERSON).
# -Dangelica.flyby.dimension=<id> moves the player there first (1 = End), after world saving is off.
# -Dangelica.flyby.biome=<name> pins the origin to the 44x44-block area of that biome nearest to 0,0 (BiomeGenBase.biomeName), searched in the target dimension.
# -Dangelica.flyby.survival=true runs the flight in survival with flying and no damage, restoring the original game mode afterwards.
# -PclientJvmVendor=adoptium (macOS) picks the JVM vendor for runClient17/21/25.
#
# FLYBY_SPEED - blocks per tick. (0.5 ~= creative flight).
+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) {
@@ -0,0 +1,21 @@
package com.gtnewhorizons.angelica.api.weather;
import net.minecraft.client.multiplayer.WorldClient;
import net.minecraft.entity.EntityLivingBase;
public interface WeatherColumnProvider {
// False suppresses vanilla rain and snow in this dimension.
boolean rendersVanillaWeather();
// Called each instanced weather frame before columnRevision and collectColumns; returns the tick counter that drives scroll and drift.
int beginFrame(WorldClient world, EntityLivingBase view, float partialTicks);
// Must change whenever any collected column changes; columns are re-collected only then or when the dimension or the player's block X/Z changes.
int columnRevision();
// Finite positive block distance d0 in alpha = ((1 - (d / d0)^2) * fadeScale + 0.5) * a.
float fadeRadius();
void collectColumns(WeatherColumnSink sink);
}
@@ -0,0 +1,12 @@
package com.gtnewhorizons.angelica.api.weather;
import net.minecraft.util.ResourceLocation;
public interface WeatherColumnSink {
// scrollPeriod is a power of two no greater than 1024.
void beginLayer(ResourceLocation texture, int scrollPeriod, float uDriftScale, float vDriftScale, float fadeScale);
// x and z are world block coordinates; brightness is a packed lightmap coordinate below 2^24.
void column(int x, int z, float bottom, float top, float uBase, float uDrift, float vBase, float vDrift, int brightness, int scrollOffset, float scrollScale, float r, float g, float b, float a);
}
@@ -0,0 +1,4 @@
@API(owner = "angelica", provides = "angelica|weather", apiVersion = "1")
package com.gtnewhorizons.angelica.api.weather;
import cpw.mods.fml.common.API;
@@ -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;
}
}
@@ -30,6 +30,7 @@ public class ModStatus {
public static boolean isBaublesLoaded;
public static boolean isCosmeticArmorReworkedLoaded;
public static boolean isFluidLoggedLoaded;
public static boolean isLiquidLoggedLoaded;
public static boolean isCubicChunksLoaded;
public static boolean isBOPLoaded;
public static boolean isEtFuturumLoaded;
@@ -51,6 +52,7 @@ public class ModStatus {
isBaublesLoaded = Loader.isModLoaded("Baubles");
isCosmeticArmorReworkedLoaded = Loader.isModLoaded("cosmeticarmorreworked");
isFluidLoggedLoaded = Loader.isModLoaded("fluidlogged");
isLiquidLoggedLoaded = Loader.isModLoaded("liquidlogged");
isCubicChunksLoaded = Loader.isModLoaded("cubicchunks");
isEtFuturumLoaded = Loader.isModLoaded("etfuturum");
@@ -0,0 +1,77 @@
package com.gtnewhorizons.angelica.compat.spark;
import cpw.mods.fml.common.Loader;
import cpw.mods.fml.common.ModContainer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.io.InputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public final class SparkKnownSources {
private static final Logger LOGGER = LogManager.getLogger("Angelica");
private static final String PLUGIN = "me/lucko/spark/forge/plugin/Forge1710SparkPlugin";
private static final String SOURCE_METADATA = "me.lucko.spark.common.sampler.source.SourceMetadata";
private static MethodHandle constructor;
private static boolean failed;
private SparkKnownSources() {}
public static boolean mixinApplies() {
try (InputStream stream = SparkKnownSources.class.getResourceAsStream("/" + PLUGIN + ".class")) {
if (stream == null) {
return false;
}
final boolean[] declared = new boolean[1];
new ClassReader(stream).accept(new ClassVisitor(Opcodes.ASM5) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if ("getKnownSources".equals(name) && "()Ljava/util/Collection;".equals(desc)) {
declared[0] = true;
}
return null;
}
}, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
if (declared[0]) {
LOGGER.info("spark already reports mod sources; skipping");
return false;
}
return true;
} catch (Throwable t) {
LOGGER.warn("Could not inspect spark plugin class; not adding mod sources", t);
return false;
}
}
public static Collection<Object> gather(ClassLoader loader) {
if (failed) {
return Collections.emptyList();
}
try {
if (constructor == null) constructor = MethodHandles.publicLookup().findConstructor(Class.forName(SOURCE_METADATA, false, loader),
MethodType.methodType(void.class, String.class, String.class, String.class, String.class));
final List<ModContainer> mods = Loader.instance().getActiveModList();
final List<Object> sources = new ArrayList<>(mods.size());
for (int i = 0; i < mods.size(); i++) {
final ModContainer mod = mods.get(i);
sources.add(constructor.invoke(mod.getModId(), mod.getVersion(), (String) null, (String) null));
}
return sources;
} catch (Throwable t) {
failed = true;
LOGGER.warn("Could not build spark mod sources; profiles will omit mod versions", t);
return Collections.emptyList();
}
}
}
@@ -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);
}
Loaded 100 of 153 files, more files were not shown because too many files have changed in this diff. Show more