Parallelize atlas loading and fix SDL-GPU mip-level targets (#2167)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-09-29 16:57:53 -07:00
1 parent 2f4ac2a9b7
commit c10d30b2da
56 files changed
+4167 -409

No files matched your search

+1
View File
@@ -202,6 +202,7 @@ afterEvaluate {
testImplementation(libs.junit.platform.launcher)
testImplementation(libs.archunit.junit5)
testImplementation(libs.retrofuturabootstrap) { isTransitive = false }
glsmTestFixtures(testFixtures(project(":glsm")) as ModuleDependency) { isTransitive = false }
testCompileOnly(testFixtures(project(":glsm")))
@@ -67,6 +67,7 @@ import com.gtnewhorizons.angelica.glsm.states.TextureBinding;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import com.gtnewhorizons.angelica.glsm.texture.TextureStaging;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import lombok.Getter;
@@ -5990,6 +5991,42 @@ public class GLStateManager {
maybeGenerateMipmap(target, level);
}
public static TextureStaging beginTextureStaging(int level, int x, int y, int width, int height) {
if (DisplayListManager.isRecording()) {
throw DisplayListManager.unsupportedInList("beginTextureStaging");
}
if (!ctx().pixelUnpackState.isDefault()) {
return null;
}
return RENDER_BACKEND.beginTextureStaging(level, x, y, width, height);
}
public static boolean commitTextureStaging(TextureStaging staging) {
if (DisplayListManager.isRecording()) {
throw DisplayListManager.unsupportedInList("commitTextureStaging");
}
suspendPixelUnpackBuffer();
final boolean committed;
try {
committed = RENDER_BACKEND.commitTextureStaging(staging);
} finally {
restorePixelUnpackBuffer();
}
if (committed) {
TextureInfoCache.INSTANCE.onTexSubImage2D(GL11.GL_TEXTURE_2D, staging.level());
maybeGenerateMipmap(GL11.GL_TEXTURE_2D, staging.level());
}
return committed;
}
public static void abandonTextureStaging(TextureStaging staging) {
RENDER_BACKEND.abandonTextureStaging(staging);
}
public static void trimTextureStaging() {
RENDER_BACKEND.trimTextureStaging();
}
public static void glTexSubImage3D(int target, int level, int xoffset, int yoffset, int zoffset, int width, int height, int depth, int format, int type, ByteBuffer pixels) {
if (DisplayListManager.isRecording()) {
throw DisplayListManager.unsupportedInList("glTexSubImage3D");
@@ -1,10 +1,14 @@
package com.gtnewhorizons.angelica.glsm.backend;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.GLESFormatRemap;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.texture.TextureStaging;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL20;
import java.nio.ByteBuffer;
@@ -66,6 +70,7 @@ public abstract class RenderBackend {
public abstract void flush();
public abstract void finish();
private final ByteBuffer[] clientStagingCache = new ByteBuffer[16];
private VSyncMode effectiveVSyncMode = VSyncMode.ON;
private VSyncMode preferredTearFreeMode;
@@ -351,6 +356,71 @@ public abstract class RenderBackend {
public void texImage3D(int target, int level, int internalFormat, int width, int height, int depth, int border, int format, int type, IntBuffer pixels) {}
public void texSubImage1D(int target, int level, int xoffset, int width, int format, int type, ByteBuffer pixels) {}
public void texSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, long pboOffset) {}
public TextureStaging beginTextureStaging(int level, int x, int y, int width, int height) {
final int size = Math.multiplyExact(Math.multiplyExact(width, height), 4);
ByteBuffer buffer = null;
for (int i = 0; i < clientStagingCache.length; i++) {
final ByteBuffer cached = clientStagingCache[i];
if (cached != null && cached.capacity() == size) {
clientStagingCache[i] = null;
buffer = cached;
break;
}
}
if (buffer == null) buffer = MemoryUtilities.memAlloc(size);
buffer.clear();
return new ClientTextureStaging(buffer, level, x, y, width, height);
}
public boolean commitTextureStaging(TextureStaging staging) {
final int type = RenderSystem.isGLES() ? GLESFormatRemap.remapPixelType(GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV) : GL12.GL_UNSIGNED_INT_8_8_8_8_REV;
try {
final ClientTextureStaging s = (ClientTextureStaging) staging;
texSubImage2D(GL11.GL_TEXTURE_2D, s.level(), s.x(), s.y(), s.width(), s.height(), GL12.GL_BGRA, type, s.buffer());
} finally {
recycleClientStaging(staging.buffer());
}
return true;
}
public void abandonTextureStaging(TextureStaging staging) {
recycleClientStaging(staging.buffer());
}
public void trimTextureStaging() {
for (int i = 0; i < clientStagingCache.length; i++) {
MemoryUtilities.memFree(clientStagingCache[i]);
clientStagingCache[i] = null;
}
}
private record ClientTextureStaging(ByteBuffer buffer, int level, int x, int y, int width, int height) implements TextureStaging {
@Override
public boolean bgra() {
return true;
}
}
private void recycleClientStaging(ByteBuffer buffer) {
int victim = -1;
for (int i = 0; i < clientStagingCache.length; i++) {
final ByteBuffer cached = clientStagingCache[i];
if (cached == null) {
clientStagingCache[i] = buffer;
return;
}
if (cached.capacity() < buffer.capacity() && (victim < 0 || cached.capacity() < clientStagingCache[victim].capacity())) {
victim = i;
}
}
if (victim < 0) {
MemoryUtilities.memFree(buffer);
return;
}
MemoryUtilities.memFree(clientStagingCache[victim]);
clientStagingCache[victim] = buffer;
}
public void texSubImage3D(int target, int level, int xoffset, int yoffset, int zoffset, int width, int height, int depth, int format, int type, ByteBuffer pixels) {}
public void copyTexImage1D(int target, int level, int internalFormat, int x, int y, int width, int border) {}
public void copyTexImage2D(int target, int level, int internalFormat, int x, int y, int width, int height, int border) {}
@@ -18,6 +18,7 @@ public final class GLSMConfig {
public static boolean extendedAttribsExpected;
public static volatile boolean expandVertexFormats;
public static int workerThreadCount;
private GLSMConfig() {}
}
@@ -11,7 +11,6 @@ public final class GLSMHooks {
public static DeferredDepthColorHandler depthColorHandler;
public static GlintColorHandler glintColorHandler;
public static ImmediateExtendedAttribHandler immediateExtendedHandler;
public static ShaderWorkSubmitter shaderWorkSubmitter;
public static ShaderTransformPostProcessor postTransformProcessor;
public static PerFrameUniformBlock perFrameUniformBlock;
public static PerFrameUniformBlock perPassUniformBlock;
@@ -1,8 +0,0 @@
package com.gtnewhorizons.angelica.glsm.hooks;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
public interface ShaderWorkSubmitter {
<T> CompletableFuture<T> submit(Supplier<T> work);
}
@@ -38,6 +38,10 @@ public class ModRedirector {
return allExclusions;
}
public String[] getConstantPoolSearchStrings() {
return core.getConstantPoolSearchStrings();
}
public boolean shouldTransform(byte[] basicClass) {
return core.shouldTransform(basicClass);
}
@@ -1,5 +1,7 @@
package com.gtnewhorizons.angelica.glsm.loading;
import com.gtnewhorizons.retrofuturabootstrap.api.BytePatternMatcher;
import com.gtnewhorizons.retrofuturabootstrap.api.ClassHeaderMetadata;
import com.gtnewhorizons.retrofuturabootstrap.api.ClassNodeHandle;
import com.gtnewhorizons.retrofuturabootstrap.api.ExtensibleClassLoader;
import com.gtnewhorizons.retrofuturabootstrap.api.RfbClassTransformer;
@@ -16,10 +18,12 @@ public abstract class RfbModRedirector implements RfbClassTransformer {
private final String id;
private final ModRedirector inner;
private final BytePatternMatcher cstPoolMatcher;
protected RfbModRedirector(@Pattern("[a-z0-9-]+") String id, ModRedirector inner) {
this.id = id;
this.inner = inner;
this.cstPoolMatcher = new BytePatternMatcher(inner.getConstantPoolSearchStrings(), BytePatternMatcher.Mode.StartsWith);
}
@Override
@@ -53,7 +57,11 @@ public abstract class RfbModRedirector implements RfbClassTransformer {
if (!classNode.isPresent()) {
return false;
}
return inner.shouldTransform(classNode.getOriginalBytes());
final ClassHeaderMetadata metadata = classNode.getOriginalMetadata();
if (metadata == null) {
return inner.shouldTransform(classNode.getOriginalBytes());
}
return metadata.matchesBytes(classNode.getOriginalBytes(), cstPoolMatcher);
}
@Override
@@ -176,6 +176,7 @@ public class GLSMRedirector {
private static final Map<Integer, String> glCapRedirects = new HashMap<>();
private static final Map<String, String> typeRedirects = new HashMap<>();
private static final ClassConstantPoolParser cstPoolParser;
private static final String[] CONSTANT_POOL_SEARCH_STRINGS;
private static final String[] CORE_EXCLUSIONS = {
"org.lwjgl",
@@ -826,13 +827,18 @@ public class GLSMRedirector {
stringsToSearch.add(Display);
stringsToSearch.addAll(typeRedirects.keySet());
stringsToSearch.addAll(methodRedirects.keySet());
cstPoolParser = new ClassConstantPoolParser(stringsToSearch.toArray(new String[0]));
CONSTANT_POOL_SEARCH_STRINGS = stringsToSearch.toArray(new String[0]);
cstPoolParser = new ClassConstantPoolParser(CONSTANT_POOL_SEARCH_STRINGS);
}
public String[] getCoreExclusions() {
return CORE_EXCLUSIONS.clone();
}
public String[] getConstantPoolSearchStrings() {
return CONSTANT_POOL_SEARCH_STRINGS.clone();
}
public static String getGLPrefix() { return GL_PREFIX; }
public static String getTargetClassName() { return GLStateManager; }
@@ -0,0 +1,9 @@
package com.gtnewhorizons.angelica.glsm.texture;
import java.nio.ByteBuffer;
public interface TextureStaging {
ByteBuffer buffer();
boolean bgra();
int level();
}
@@ -0,0 +1,186 @@
package com.gtnewhorizons.angelica.glsm.threading;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
public final class AngelicaWorkers {
private static final Logger LOGGER = LogManager.getLogger("Angelica");
private static final long IDLE_TIMEOUT_SECONDS = 120;
private static final Object lock = new Object();
private static final List<Runnable> idleShutdownHooks = new ArrayList<>();
private static ExecutorService executor;
private static ScheduledExecutorService scheduler;
private static volatile long lastActivityTime;
private static final AtomicInteger inFlight = new AtomicInteger(0);
private static boolean idleCheckScheduled;
private static int resolvedThreads;
private AngelicaWorkers() {}
private static final class Worker extends Thread {
Worker(Runnable r, String name) {
super(r, name);
setDaemon(true);
}
}
public static boolean isWorkerThread() {
return Thread.currentThread() instanceof Worker;
}
public static void onIdleShutdown(Runnable hook) {
Objects.requireNonNull(hook);
synchronized (lock) {
idleShutdownHooks.add(hook);
}
}
public static void prestart() {
executor();
}
public static int threads() {
synchronized (lock) {
resolveThreads();
return resolvedThreads;
}
}
private static void resolveThreads() {
if (resolvedThreads == 0) {
final int configured = GLSMConfig.workerThreadCount;
resolvedThreads = configured > 0 ? configured : Math.max(2, Math.min(12, Runtime.getRuntime().availableProcessors() / 2));
}
}
private static void noteActivity() {
lastActivityTime = System.nanoTime();
}
private static ExecutorService executor() {
synchronized (lock) {
noteActivity();
if (executor != null && !executor.isShutdown()) {
return executor;
}
resolveThreads();
final AtomicInteger threadCounter = new AtomicInteger();
final ThreadFactory factory = r -> new Worker(r, "Angelica-Worker-" + threadCounter.getAndIncrement());
final ThreadPoolExecutor tpe = new ThreadPoolExecutor(resolvedThreads, resolvedThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), factory);
tpe.prestartAllCoreThreads();
executor = tpe;
LOGGER.debug("Created Angelica worker pool with {} prestarted threads", resolvedThreads);
if (scheduler == null || scheduler.isShutdown()) {
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
final Thread t = new Thread(r, "Angelica-Worker-Scheduler");
t.setDaemon(true);
return t;
});
}
if (!idleCheckScheduled) {
idleCheckScheduled = true;
scheduleIdleCheck(IDLE_TIMEOUT_SECONDS);
}
return executor;
}
}
private static void scheduleIdleCheck(long delaySeconds) {
try {
scheduler.schedule(AngelicaWorkers::checkIdleShutdown, delaySeconds, TimeUnit.SECONDS);
} catch (Exception e) {
idleCheckScheduled = false;
LOGGER.warn("Failed to schedule idle check", e);
}
}
public static <T> CompletableFuture<T> submit(Supplier<T> work) {
Objects.requireNonNull(work);
noteActivity();
inFlight.incrementAndGet();
try {
return CompletableFuture.supplyAsync(() -> {
try {
return work.get();
} finally {
noteActivity();
inFlight.decrementAndGet();
}
}, executor());
} catch (Exception e) {
inFlight.decrementAndGet();
throw e;
}
}
public static CompletableFuture<Void> run(Runnable work) {
Objects.requireNonNull(work);
noteActivity();
inFlight.incrementAndGet();
try {
return CompletableFuture.runAsync(() -> {
try {
work.run();
} finally {
noteActivity();
inFlight.decrementAndGet();
}
}, executor());
} catch (Exception e) {
inFlight.decrementAndGet();
throw e;
}
}
private static void checkIdleShutdown() {
synchronized (lock) {
final ExecutorService current = executor;
if (current == null || current.isShutdown()) {
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
scheduler = null;
}
idleCheckScheduled = false;
return;
}
final long idleSeconds = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - lastActivityTime);
if (idleSeconds >= IDLE_TIMEOUT_SECONDS && inFlight.get() == 0) {
LOGGER.debug("Shutting down idle Angelica worker pool after {} seconds", idleSeconds);
current.shutdown();
executor = null;
scheduler.shutdown();
scheduler = null;
idleCheckScheduled = false;
for (int i = 0; i < idleShutdownHooks.size(); i++) {
idleShutdownHooks.get(i).run();
}
} else {
scheduleIdleCheck(Math.max(1, IDLE_TIMEOUT_SECONDS - idleSeconds + 1));
}
}
}
}
@@ -15,6 +15,7 @@ import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import com.gtnewhorizons.angelica.glsm.texture.TextureStaging;
import com.gtnewhorizons.angelica.sdlgpu.compute.ComputeBinder;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import com.gtnewhorizons.angelica.sdlgpu.device.SDLDrawable;
@@ -113,6 +114,8 @@ import com.gtnewhorizons.angelica.sdlgpu.compat.Lwjgl3GLCapabilitiesShim;
import me.eigenraven.lwjgl3ify.api.GLCapabilitiesOverride;
import static org.lwjgl.sdl.SDLGPU.*;
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_HORIZONTAL;
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_HORIZONTAL_AND_VERTICAL;
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_NONE;
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_VERTICAL;
@@ -1110,17 +1113,26 @@ public class SDLGPURenderBackend extends RenderBackend {
if (db < 0 || db >= ContextState.MAX_COLOR_ATTACHMENTS) continue;
final long tex = fbo.colorTextures[db];
if (tex == 0) continue;
if (fbo.colorLevels[db] != 0) {
fboClearTracker.clearColorLevel(tex, fbo.colorLevels[db], st.clearR, st.clearG, st.clearB, st.clearA);
continue;
}
FBOClearTracker.recordPendingColorClear(st, tex, st.clearR, st.clearG, st.clearB, st.clearA);
if (passActive && tex == activeColor) affectsActivePass = true;
}
}
if (wantDepth && fbo.depthTexture != 0) {
FBOClearTracker.recordPendingDepthClear(st, fbo.depthTexture, st.depthClearValue);
if (passActive && fbo.depthTexture == activeDepth) affectsActivePass = true;
}
if (wantStencil && fbo.depthTexture != 0 && PixelOps.isDepthStencilFormat(fbo.depthFormat)) {
FBOClearTracker.recordPendingStencilClear(st, fbo.depthTexture, st.stencilClearValue);
if (passActive && fbo.depthTexture == activeDepth) affectsActivePass = true;
final boolean clearStencilAspect = wantStencil && fbo.depthTexture != 0 && PixelOps.isDepthStencilFormat(fbo.depthFormat);
if (fbo.depthTexture != 0 && fbo.depthLevel != 0) {
fboClearTracker.clearDepthStencilLevel(fbo.depthTexture, fbo.depthLevel, wantDepth, st.depthClearValue, clearStencilAspect, st.stencilClearValue);
} else {
if (wantDepth && fbo.depthTexture != 0) {
FBOClearTracker.recordPendingDepthClear(st, fbo.depthTexture, st.depthClearValue);
if (passActive && fbo.depthTexture == activeDepth) affectsActivePass = true;
}
if (clearStencilAspect) {
FBOClearTracker.recordPendingStencilClear(st, fbo.depthTexture, st.stencilClearValue);
if (passActive && fbo.depthTexture == activeDepth) affectsActivePass = true;
}
}
if (affectsActivePass) {
@@ -1622,6 +1634,37 @@ public class SDLGPURenderBackend extends RenderBackend {
resourceManager.getTextureHandle(glId),
staging.slice(), xoffset, yoffset, width, height, level, format, type);
}
@Override public TextureStaging beginTextureStaging(int level, int x, int y, int width, int height) {
final int glId = boundTextureFor(s(), GL11.GL_TEXTURE_2D);
if (glId == 0) return null;
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(glId);
if (meta == null || level >= meta.levels()) return null;
final long texHandle = resourceManager.getTextureHandle(glId);
if (texHandle == 0 || frameManager.getCommandBuffer() == 0) return null;
return resourceManager.beginTextureStaging(glId, texHandle, PixelOps.isBgraSdlFormat(meta.sdlFormat()), level, x, y, width, height);
}
@Override public boolean commitTextureStaging(TextureStaging staging) {
final ResourceManager.TextureStagingRegion region = (ResourceManager.TextureStagingRegion) staging;
final ContextState st = s();
final boolean defer = resourceManager.shouldDeferTextureUpload(st, region.glId());
if (!defer || !resourceManager.commitTextureStagingDeferred(st, region)) {
final long cp = frameManager.ensureCopyPass();
if (cp == 0) {
resourceManager.abandonTextureStaging(region);
return false;
}
resourceManager.commitTextureStagingInline(cp, region);
}
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(region.glId());
if (meta != null) textureOps.trackMipUploadForTexture(region.glId(), region.level(), meta.levels());
return true;
}
@Override public void abandonTextureStaging(TextureStaging staging) {
resourceManager.abandonTextureStaging((ResourceManager.TextureStagingRegion) staging);
}
@Override public void copyTexSubImage2D(int target, int level, int xoffset, int yoffset, int x, int y, int width, int height) {
final ContextState cs = s();
textureOps.copyTexSubImageImpl(cs, boundTextureFor(cs, target), level, xoffset, yoffset, x, y, width, height);
@@ -1836,13 +1879,14 @@ public class SDLGPURenderBackend extends RenderBackend {
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(texture);
fbo.colorTextures[colorIdx] = handle;
fbo.colorGlIds[colorIdx] = texture;
fbo.colorLevels[colorIdx] = level;
resourceManager.markFboAttachment(texture);
fbo.targetsDirty = true;
if (meta != null) {
fbo.colorFormats[colorIdx] = meta.sdlFormat();
if (colorIdx == 0) {
fbo.width = meta.width();
fbo.height = meta.height();
fbo.width = PixelOps.mipLevelSize(meta.width(), level);
fbo.height = PixelOps.mipLevelSize(meta.height(), level);
}
}
fbo.colorAttachmentCount = Math.max(fbo.colorAttachmentCount, colorIdx + 1);
@@ -1870,6 +1914,7 @@ public class SDLGPURenderBackend extends RenderBackend {
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(texture);
fbo.depthTexture = handle;
fbo.depthGlId = texture;
fbo.depthLevel = level;
resourceManager.markFboAttachment(texture);
fbo.targetsDirty = true;
if (meta != null) {
@@ -2044,6 +2089,7 @@ public class SDLGPURenderBackend extends RenderBackend {
int depthFormat = 0;
final int srcWidth;
final int srcHeight;
int srcLevel = 0;
if (fromFbo0) {
srcWidth = frameManager.getFbo0Width();
srcHeight = frameManager.getFbo0Height();
@@ -2063,9 +2109,11 @@ public class SDLGPURenderBackend extends RenderBackend {
colorTex = resourceManager.getTextureHandle(colorGlId);
colorFormat = meta != null ? meta.sdlFormat() : SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
if (meta != null) colorBase = PixelPack.glBaseFormat(meta.glFormat());
srcWidth = meta != null ? meta.width() : fbo.width;
srcHeight = meta != null ? meta.height() : fbo.height;
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;
} 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;
depthFormat = fbo.depthFormat;
srcWidth = fbo.width;
@@ -2079,7 +2127,7 @@ public class SDLGPURenderBackend extends RenderBackend {
final int w = CopyRectClip.width(clip);
final int h = CopyRectClip.height(clip);
final int dy = CopyRectClip.dstY(clip, srcY, 0);
readback(cs, true, kind, colorTex, colorFormat, colorBase, depthTex, depthFormat, 0, srcWidth, srcHeight,
readback(cs, true, kind, colorTex, colorFormat, colorBase, depthTex, depthFormat, srcLevel, srcWidth, srcHeight,
CopyRectClip.srcX(clip), CopyRectClip.srcY(clip), w, h, fromFbo0,
width, CopyRectClip.dstX(clip, x, 0), fromFbo0 ? height - h - dy : dy, format, type, dst, pbo, pboOffset);
}
@@ -2212,6 +2260,9 @@ public class SDLGPURenderBackend extends RenderBackend {
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE) {
return (colorPresent || depthPresent) ? GL11.GL_TEXTURE : GL11.GL_NONE;
}
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL) {
return colorPresent ? fbo.colorLevels[colorIdx] : depthPresent ? fbo.depthLevel : 0;
}
if (colorPresent) return colorAttachmentSize(fbo.colorFormats[colorIdx], pname);
if (depthPresent) return depthAttachmentSize(fbo.depthFormat, pname);
return 0;
@@ -3707,63 +3758,126 @@ public class SDLGPURenderBackend extends RenderBackend {
final int fbo0Height = frameManager.getFbo0Height();
if (anyFbo0 && fbo0Height <= 0) return;
final long colorSrcTex;
final long colorDstTex;
final int colorSrcGlId;
final int colorDstGlId;
if ((mask & GL11.GL_COLOR_BUFFER_BIT) != 0) {
colorSrcTex = srcIsFbo0 ? frameManager.getFbo0Texture() : srcFbo.colorTextures[srcFbo.readBufferIndex];
colorDstTex = dstIsFbo0 ? frameManager.getFbo0Texture() : dstFbo.colorTextures[0];
colorSrcGlId = srcIsFbo0 ? 0 : srcFbo.colorGlIds[srcFbo.readBufferIndex];
colorDstGlId = dstIsFbo0 ? 0 : dstFbo.colorGlIds[0];
} else {
colorSrcTex = 0L;
colorDstTex = 0L;
colorSrcGlId = 0;
colorDstGlId = 0;
}
final boolean blitColor = colorSrcTex != 0 && colorDstTex != 0 && colorSrcTex != colorDstTex;
final boolean colorRequested = (mask & GL11.GL_COLOR_BUFFER_BIT) != 0;
final long colorSrcTex = !colorRequested ? 0L : srcIsFbo0 ? frameManager.getFbo0Texture() : srcFbo.colorTextures[srcFbo.readBufferIndex];
final int colorSrcGlId = (!colorRequested || srcIsFbo0) ? 0 : srcFbo.colorGlIds[srcFbo.readBufferIndex];
final int colorSrcLevel = (!colorRequested || srcIsFbo0) ? 0 : srcFbo.colorLevels[srcFbo.readBufferIndex];
final boolean blitColor = colorSrcTex != 0 && (dstIsFbo0 ? frameManager.getFbo0Texture() != 0 : dstFbo.drawBuffers.length > 0);
final boolean depthRequested = (mask & (GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_STENCIL_BUFFER_BIT)) != 0;
final long depthSrcTex = (depthRequested && !anyFbo0) ? srcFbo.depthTexture : 0L;
final long depthDstTex = (depthRequested && !anyFbo0) ? dstFbo.depthTexture : 0L;
final boolean blitDepth = depthSrcTex != 0 && depthDstTex != 0 && depthSrcTex != depthDstTex;
final int depthSrcLevel = depthSrcTex != 0 ? srcFbo.depthLevel : 0;
final int depthDstLevel = depthDstTex != 0 ? dstFbo.depthLevel : 0;
final boolean blitDepth = depthSrcTex != 0 && depthDstTex != 0 && (depthSrcTex != depthDstTex || depthSrcLevel != depthDstLevel);
if (!blitColor && !blitDepth) return;
final int srcX = Math.min(srcX0, srcX1);
final int dstX = Math.min(dstX0, dstX1);
final int srcW = Math.abs(srcX1 - srcX0);
final int srcH = Math.abs(srcY1 - srcY0);
final int dstW = Math.abs(dstX1 - dstX0);
final int dstH = Math.abs(dstY1 - dstY0);
if (srcW == 0 || srcH == 0 || dstW == 0 || dstH == 0) return;
final boolean mirrorX = (srcX1 < srcX0) != (dstX1 < dstX0);
final boolean mirrorY = (srcY1 < srcY0) != (dstY1 < dstY0);
if (blitDepth && (mirrorX || mirrorY)) {
throw new UnsupportedOperationException("glBlitFramebuffer: mirrored depth/stencil blit is not supported on SDL-GPU (src " + srcX0 + "," + srcY0 + "," + srcX1 + "," + srcY1 + " dst " + dstX0 + "," + dstY0 + "," + dstX1 + "," + dstY1 + ")");
}
final ContextState cs = s();
if (dstIsFbo0 || (srcIsFbo0 && (cs.pendingSwapchainClear || cs.pendingSwapchainDepthClear || cs.pendingSwapchainStencilClear))) {
frameManager.ensureFbo0RenderPass(frameManager.frame(), cs);
}
final int srcW = srcX1 - srcX0;
final int srcH = srcY1 - srcY0;
final int dstW = dstX1 - dstX0;
final int dstH = dstY1 - dstY0;
final int srcY = srcIsFbo0 ? (fbo0Height - srcY0 - srcH) : srcY0;
final int dstY = dstIsFbo0 ? (fbo0Height - dstY0 - dstH) : dstY0;
final int flipMode = (srcIsFbo0 != dstIsFbo0) ? SDL_FLIP_VERTICAL : SDL_FLIP_NONE;
final int srcY = srcIsFbo0 ? (fbo0Height - Math.max(srcY0, srcY1)) : Math.min(srcY0, srcY1);
final int dstY = dstIsFbo0 ? (fbo0Height - Math.max(dstY0, dstY1)) : Math.min(dstY0, dstY1);
final boolean flipV = mirrorY != (srcIsFbo0 != dstIsFbo0);
final int flipMode = mirrorX ? (flipV ? SDL_FLIP_HORIZONTAL_AND_VERTICAL : SDL_FLIP_HORIZONTAL) : (flipV ? SDL_FLIP_VERTICAL : SDL_FLIP_NONE);
if (blitColor) {
resolveBlitClears(cs, colorSrcTex, colorDstTex, colorDstGlId, dstX0, dstY, dstW, dstH, true);
final ResourceManager.TextureMeta srcMeta = resourceManager.getTextureMeta(colorSrcGlId);
final ResourceManager.TextureMeta dstMeta = resourceManager.getTextureMeta(colorDstGlId);
if (flipMode == SDL_FLIP_NONE && TextureOps.canCopyInsteadOfBlit(srcMeta, dstMeta, srcW, srcH, dstW, dstH)) {
textureOps.copyTexture(colorSrcTex, srcX0, srcY, colorDstTex, 0, dstX0, dstY, srcW, srcH);
if (dstIsFbo0) {
blitColorTarget(cs, srcMeta, colorSrcTex, colorSrcLevel, srcX, srcY, srcW, srcH, frameManager.getFbo0Texture(), 0, 0, dstX, dstY, dstW, dstH, filter, flipMode, srcIsFbo0, true);
} else {
textureOps.blitTexture(colorSrcTex, srcX0, srcY, srcW, srcH, colorDstTex, 0, dstX0, dstY, dstW, dstH, filter, flipMode);
final int[] drawBuffers = dstFbo.drawBuffers;
for (int i = 0; i < drawBuffers.length; i++) {
final int db = drawBuffers[i];
if (db < 0 || db >= ContextState.MAX_COLOR_ATTACHMENTS) continue;
final long dstTex = dstFbo.colorTextures[db];
if (dstTex == 0) continue;
blitColorTarget(cs, srcMeta, colorSrcTex, colorSrcLevel, srcX, srcY, srcW, srcH, dstTex, dstFbo.colorGlIds[db], dstFbo.colorLevels[db], dstX, dstY, dstW, dstH, filter, flipMode, srcIsFbo0, false);
}
}
}
if (blitDepth) {
final boolean allAspects = !PixelOps.isDepthStencilFormat(dstFbo.depthFormat) || (mask & (GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_STENCIL_BUFFER_BIT)) == (GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_STENCIL_BUFFER_BIT);
resolveBlitClears(cs, depthSrcTex, depthDstTex, dstFbo.depthGlId, dstX0, dstY, dstW, dstH, allAspects);
textureOps.copyTexture(depthSrcTex, srcX0, srcY, depthDstTex, 0, dstX0, dstY, srcW, srcH);
copyClipped(cs, depthSrcTex, resourceManager.getTextureMeta(srcFbo.depthGlId), depthSrcLevel, srcX, srcY, depthDstTex, resourceManager.getTextureMeta(dstFbo.depthGlId), depthDstLevel, dstX, dstY, srcW, srcH, allAspects);
}
}
private void resolveBlitClears(ContextState cs, long srcTex, long dstTex, int dstGlId, int dstX, int dstY, int dstW, int dstH, boolean allAspects) {
private void copyClipped(ContextState cs, long srcTex, ResourceManager.TextureMeta srcMeta, int srcLevel, int sx, int sy, long dstTex, ResourceManager.TextureMeta dstMeta, int dstLevel, int dx, int dy, int w, int h, boolean allAspects) {
final long clip = CopyRectClip.clipCopyRect(sx, sy, dx, dy, w, h, TextureOps.levelWidth(srcMeta, srcLevel), TextureOps.levelHeight(srcMeta, srcLevel), TextureOps.levelWidth(dstMeta, dstLevel), TextureOps.levelHeight(dstMeta, dstLevel));
if (clip == CopyRectClip.EMPTY) return;
final int cw = CopyRectClip.width(clip);
final int ch = CopyRectClip.height(clip);
final int cdx = CopyRectClip.dstX(clip, sx, dx);
final int cdy = CopyRectClip.dstY(clip, sy, dy);
resolveBlitClears(cs, srcTex, dstTex, dstMeta, dstLevel, cdx, cdy, cw, ch, allAspects);
textureOps.copyTexture(srcTex, srcLevel, CopyRectClip.srcX(clip), CopyRectClip.srcY(clip), dstTex, dstLevel, cdx, cdy, cw, ch);
}
private void blitColorTarget(ContextState cs, ResourceManager.TextureMeta srcMeta, long srcTex, int srcLevel, int sx, int sy, int sw, int sh, long dstTex, int dstGlId, int dstLevel, int dx, int dy, int dw, int dh, int filter, int flipMode, boolean srcIsFbo0, boolean dstIsFbo0) {
if (srcTex == dstTex && srcLevel == dstLevel) return;
final ResourceManager.TextureMeta dstMeta = resourceManager.getTextureMeta(dstGlId);
if (flipMode == SDL_FLIP_NONE && TextureOps.canCopyInsteadOfBlit(srcMeta, dstMeta, sw, sh, dw, dh)) {
copyClipped(cs, srcTex, srcMeta, srcLevel, sx, sy, dstTex, dstMeta, dstLevel, dx, dy, sw, sh, true);
return;
}
final boolean flipH = (flipMode & SDL_FLIP_HORIZONTAL) != 0;
final boolean flipV = (flipMode & SDL_FLIP_VERTICAL) != 0;
final int[] r = cs.blitRect;
r[0] = sx; r[1] = sy; r[2] = sw; r[3] = sh;
r[4] = dx; r[5] = dy; r[6] = dw; r[7] = dh;
if (!clipScaled(r, 0, srcIsFbo0 ? frameManager.getFbo0Width() : TextureOps.levelWidth(srcMeta, srcLevel), srcIsFbo0 ? frameManager.getFbo0Height() : TextureOps.levelHeight(srcMeta, srcLevel), flipH, flipV)) return;
if (!clipScaled(r, 4, dstIsFbo0 ? frameManager.getFbo0Width() : TextureOps.levelWidth(dstMeta, dstLevel), dstIsFbo0 ? frameManager.getFbo0Height() : TextureOps.levelHeight(dstMeta, dstLevel), flipH, flipV)) return;
resolveBlitClears(cs, srcTex, dstTex, dstMeta, dstLevel, r[4], r[5], r[6], r[7], true);
textureOps.blitTexture(srcTex, srcLevel, r[0], r[1], r[2], r[3], dstTex, dstLevel, r[4], r[5], r[6], r[7], filter, flipMode);
}
static boolean clipScaled(int[] r, int a, int limW, int limH, boolean flipH, boolean flipV) {
final int b = 4 - a;
final int aw = r[a + 2];
final int ah = r[a + 3];
final int bw = r[b + 2];
final int bh = r[b + 3];
if (aw <= 0 || ah <= 0 || bw <= 0 || bh <= 0 || limW <= 0 || limH <= 0) return true;
final int left = Math.max(0, -r[a]);
final int right = Math.max(0, r[a] + aw - limW);
final int top = Math.max(0, -r[a + 1]);
final int bottom = Math.max(0, r[a + 1] + ah - limH);
if (left + right >= aw || top + bottom >= ah) return false;
if ((left | right | top | bottom) == 0) return true;
final int bLeft = (int) ((long) left * bw / aw);
final int bRight = (int) ((long) right * bw / aw);
final int bTop = (int) ((long) top * bh / ah);
final int bBottom = (int) ((long) bottom * bh / ah);
r[a] += left;
r[a + 2] -= left + right;
r[a + 1] += top;
r[a + 3] -= top + bottom;
r[b] += flipH ? bRight : bLeft;
r[b + 2] -= bLeft + bRight;
r[b + 1] += flipV ? bBottom : bTop;
r[b + 3] -= bTop + bBottom;
return r[b + 2] > 0 && r[b + 3] > 0;
}
private void resolveBlitClears(ContextState cs, long srcTex, long dstTex, ResourceManager.TextureMeta dstMeta, int dstLevel, int dstX, int dstY, int dstW, int dstH, boolean allAspects) {
fboClearTracker.materializePendingClearForTexture(cs, srcTex);
fboClearTracker.resolveDestinationForWrite(cs, dstTex, resourceManager.getTextureMeta(dstGlId), 0, dstX, dstY, 0, dstW, dstH, allAspects);
fboClearTracker.resolveDestinationForWrite(cs, dstTex, dstMeta, dstLevel, dstX, dstY, 0, dstW, dstH, allAspects);
}
@Override public int createBuffers() { return resourceManager.genBuffer(); }
@Override public void namedBufferData(int buffer, long size, int usage) {
@@ -4129,6 +4243,7 @@ public class SDLGPURenderBackend extends RenderBackend {
fboClearTracker.resolveDestinationForWrite(copySt, dstTex, resourceManager.getTextureMeta(dstName), dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, true);
final long cp = frameManager.ensureCopyPass();
if (cp == 0) return;
resourceManager.flushBatchedUploads(cp);
try (var stack = MemoryStack.stackPush()) {
final var src = SDL_GPUTextureLocation.calloc(stack)
.texture(srcTex).mip_level(srcLevel).x(srcX).y(srcY).z(srcZ);
@@ -138,6 +138,7 @@ public final class ContextState {
public int logicOpMode = GL11.GL_COPY;
public long appliedLogicOpKey;
public LogicOpVariant activeLogicOpVariant;
public final int[] blitRect = new int[8];
public final long[] logicOpScratch = new long[MAX_COLOR_ATTACHMENTS];
public final int[] logicOpScratchFormat = new int[MAX_COLOR_ATTACHMENTS];
public final int[] logicOpScratchWidth = new int[MAX_COLOR_ATTACHMENTS];
@@ -649,7 +649,11 @@ public final class FrameManager {
public void submitMidFrame() {
final FrameState f = frame();
if (f.commandBuffer == 0) return;
if (f.commandBuffer == 0) {
if (f.wantFenceOnNextSubmit) getOrCreatePendingUploadCommandBuffer(f);
flushPendingUploadCommandBuffer(f);
return;
}
endCopyPassIfActive(f);
endRenderPassIfActive(f, PASS_END_FRAME_END);
Tracy.beginZone(Z_SDL_SUBMIT);
@@ -749,7 +753,14 @@ public final class FrameManager {
Tracy.beginZone(Z_SDL_SUBMIT);
final boolean submitted;
try {
submitted = SDL_SubmitGPUCommandBuffer(f.pendingUploadCommandBuffer);
if (f.wantFenceOnNextSubmit) {
f.wantFenceOnNextSubmit = false;
if (f.lastAcquiredFence != 0) SDL_ReleaseGPUFence(device.getDevice(), f.lastAcquiredFence);
f.lastAcquiredFence = SDL_SubmitGPUCommandBufferAndAcquireFence(f.pendingUploadCommandBuffer);
submitted = f.lastAcquiredFence != 0;
} else {
submitted = SDL_SubmitGPUCommandBuffer(f.pendingUploadCommandBuffer);
}
} finally {
Tracy.endZone();
}
@@ -4,6 +4,7 @@ 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;
@@ -70,8 +71,11 @@ public final class LogicOpEmulator {
st.logicOpScratchHeight[loc] = h;
st.samplerBindGen++;
}
if (cp == 0L) cp = frameManager.ensureCopyPass(FrameManager.PASS_END_LOGIC_OP);
TextureOps.copyTexture(cp, tex, 0, 0, scratch, 0, 0, 0, w, h);
if (cp == 0L) {
cp = frameManager.ensureCopyPass(FrameManager.PASS_END_LOGIC_OP);
resourceManager.flushBatchedUploads(cp);
}
TextureOps.copyTexture(cp, tex, targetLevel(fbo, loc), 0, 0, scratch, 0, 0, 0, w, h);
}
}
@@ -109,15 +113,21 @@ public final class LogicOpEmulator {
return fbo == null ? frameManager.getSwapchainFormat() : fbo.colorFormats[drawBuffer(fbo, loc)];
}
private static int targetLevel(FboState fbo, int loc) {
return fbo == null ? 0 : fbo.colorLevels[drawBuffer(fbo, loc)];
}
private int targetWidth(FboState fbo, int loc) {
if (fbo == null) return frameManager.getFbo0Width();
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(fbo.colorGlIds[drawBuffer(fbo, loc)]);
return meta != null ? meta.width() : fbo.width;
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;
}
private int targetHeight(FboState fbo, int loc) {
if (fbo == null) return frameManager.getFbo0Height();
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(fbo.colorGlIds[drawBuffer(fbo, loc)]);
return meta != null ? meta.height() : fbo.height;
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;
}
}
@@ -143,6 +143,8 @@ public final class PipelineApplier {
return;
}
fboClearTracker.materializePendingClearsForMipTargets(st, fbo);
final int totalTargets = fbo.drawBuffers.length;
final long dummyTex = totalTargets > 0 ? resourceManager.getOrCreateDummyColorTarget() : 0L;
@@ -151,17 +153,21 @@ public final class PipelineApplier {
for (int i = 0; i < totalTargets; i++) {
final int db = fbo.drawBuffers[i];
final long tex;
final int level;
if (db >= 0 && db < ContextState.MAX_COLOR_ATTACHMENTS && fbo.colorTextures[db] != 0) {
tex = fbo.colorTextures[db];
level = fbo.colorLevels[db];
} else {
tex = dummyTex;
level = 0;
}
final boolean pendingClear = tex != dummyTex && st.pendingColorTextures.contains(tex);
final boolean defined = tex != dummyTex && resourceManager.isTextureContentDefined(tex);
final boolean firstUse = !pendingClear && tex != dummyTex && !defined;
final boolean firstUse = !pendingClear && tex != dummyTex && !defined && level == 0;
if (pendingClear || firstUse) proposedClearOps |= 1 << i;
layoutHash = Hashing.fmix64(layoutHash, tex);
layoutHash = Hashing.fmix64(layoutHash, db);
layoutHash = Hashing.fmix64(layoutHash, level);
if (pendingClear || firstUse) {
final float[] c = pendingClear ? st.pendingColorValues.get(tex) : null;
layoutHash = foldClearColor(layoutHash, c != null ? c[0] : st.clearR, c != null ? c[1] : st.clearG, c != null ? c[2] : st.clearB, c != null ? c[3] : st.clearA);
@@ -173,10 +179,12 @@ public final class PipelineApplier {
if (fbo.depthTexture != 0) {
final boolean defined = resourceManager.isTextureContentDefined(fbo.depthTexture);
final boolean pendingClear = st.pendingDepthTextures.contains(fbo.depthTexture);
proposedDepthClear = pendingClear || !defined;
final boolean firstUse = !defined && fbo.depthLevel == 0;
proposedDepthClear = pendingClear || firstUse;
final boolean pendingStencil = depthHasStencil && st.pendingStencilTextures.contains(fbo.depthTexture);
proposedStencilClear = depthHasStencil && (pendingStencil || !defined);
proposedStencilClear = depthHasStencil && (pendingStencil || firstUse);
layoutHash = Hashing.fmix64(layoutHash, fbo.depthTexture);
layoutHash = Hashing.fmix64(layoutHash, fbo.depthLevel);
if (proposedDepthClear) {
layoutHash = foldClearDepth(layoutHash, pendingClear ? st.pendingDepthValues.get(fbo.depthTexture) : st.depthClearValue);
}
@@ -204,15 +212,19 @@ public final class PipelineApplier {
for (int i = 0; i < totalTargets; i++) {
final int db = fbo.drawBuffers[i];
final long tex;
final int level;
if (db >= 0 && db < ContextState.MAX_COLOR_ATTACHMENTS && fbo.colorTextures[db] != 0) {
tex = fbo.colorTextures[db];
level = fbo.colorLevels[db];
} else {
tex = dummyTex;
level = 0;
}
final boolean pendingClear = tex != dummyTex && st.pendingColorTextures.contains(tex);
final boolean clearThis = (proposedClearOps & (1 << i)) != 0;
final long ctAddr = colorTargets.address() + (long) i * SDL_GPUColorTargetInfo.SIZEOF;
MemoryAccess.putAddress(ctAddr + SDL_GPUColorTargetInfo.TEXTURE, tex);
MemoryAccess.putInt(ctAddr + SDL_GPUColorTargetInfo.MIP_LEVEL, level);
MemoryAccess.putInt(ctAddr + SDL_GPUColorTargetInfo.LOAD_OP, clearThis ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(ctAddr + SDL_GPUColorTargetInfo.STORE_OP, db >= 0 ? SDL_GPU_STOREOP_STORE : SDL_GPU_STOREOP_DONT_CARE);
if (clearThis) {
@@ -237,6 +249,7 @@ public final class PipelineApplier {
depthTarget = fbo.cachedDepthTarget;
final long dtAddr = depthTarget.address();
MemoryAccess.putAddress(dtAddr + SDL_GPUDepthStencilTargetInfo.TEXTURE, fbo.depthTexture);
MemoryAccess.putByte(dtAddr + SDL_GPUDepthStencilTargetInfo.MIP_LEVEL, (byte) fbo.depthLevel);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, proposedDepthClear ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
if (proposedDepthClear) {
@@ -167,15 +167,7 @@ public final class FBOClearTracker {
final int idx = batchIdx[k];
final long handle = colorHandles.getLong(idx);
final float[] color = st.pendingColorValues.get(handle);
final long addr = targets.get(k).address();
MemoryAccess.putAddress(addr + SDL_GPUColorTargetInfo.TEXTURE, handle);
MemoryAccess.putInt(addr + SDL_GPUColorTargetInfo.LOAD_OP, SDL_GPU_LOADOP_CLEAR);
MemoryAccess.putInt(addr + SDL_GPUColorTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
final long ccAddr = addr + SDL_GPUColorTargetInfo.CLEAR_COLOR;
MemoryAccess.putFloat(ccAddr + SDL_FColor.R, color[0]);
MemoryAccess.putFloat(ccAddr + SDL_FColor.G, color[1]);
MemoryAccess.putFloat(ccAddr + SDL_FColor.B, color[2]);
MemoryAccess.putFloat(ccAddr + SDL_FColor.A, color[3]);
putColorClear(targets.get(k).address(), handle, 0, color[0], color[1], color[2], color[3]);
resourceManager.markTextureContentDefined(handle);
consumed[idx] = true;
}
@@ -194,25 +186,8 @@ public final class FBOClearTracker {
private void emitDepthStencilClearPass(ContextState st, long handle, boolean clearDepth, boolean clearStencil, boolean materialized) {
if (!clearDepth && !clearStencil) return;
try (var stack = MemoryStack.stackPush()) {
final SDL_GPUDepthStencilTargetInfo dt = SDL_GPUDepthStencilTargetInfo.calloc(stack);
final long addr = dt.address();
MemoryAccess.putAddress(addr + SDL_GPUDepthStencilTargetInfo.TEXTURE, handle);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, clearDepth ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
if (clearDepth) {
MemoryAccess.putFloat(addr + SDL_GPUDepthStencilTargetInfo.CLEAR_DEPTH, st.pendingDepthValues.get(handle));
}
if (clearStencil) {
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STENCIL_LOAD_OP, SDL_GPU_LOADOP_CLEAR);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STENCIL_STORE_OP, SDL_GPU_STOREOP_STORE);
MemoryAccess.putByte(addr + SDL_GPUDepthStencilTargetInfo.CLEAR_STENCIL, (byte) st.pendingStencilValues.get(handle));
}
frameManager.noteClearPass();
if (materialized) frameManager.noteMaterializedClearPass();
frameManager.beginRenderPass(null, dt);
frameManager.endRenderPassIfActive(FrameManager.PASS_END_CLEAR);
}
if (materialized) frameManager.noteMaterializedClearPass();
depthStencilClearPass(handle, 0, clearDepth, clearDepth ? st.pendingDepthValues.get(handle) : 0f, clearStencil, clearStencil ? st.pendingStencilValues.get(handle) : 0);
if (clearDepth) st.pendingDepthValues.remove(handle);
if (clearStencil) st.pendingStencilValues.remove(handle);
if (clearDepth && clearStencil) resourceManager.markTextureContentDefined(handle);
@@ -270,15 +245,7 @@ public final class FBOClearTracker {
frameManager.endRenderPassIfActive(FrameManager.PASS_END_CLEAR);
try (var stack = MemoryStack.stackPush()) {
final SDL_GPUColorTargetInfo.Buffer targets = SDL_GPUColorTargetInfo.calloc(1, stack);
final long addr = targets.get(0).address();
MemoryAccess.putAddress(addr + SDL_GPUColorTargetInfo.TEXTURE, handle);
MemoryAccess.putInt(addr + SDL_GPUColorTargetInfo.LOAD_OP, SDL_GPU_LOADOP_CLEAR);
MemoryAccess.putInt(addr + SDL_GPUColorTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
final long ccAddr = addr + SDL_GPUColorTargetInfo.CLEAR_COLOR;
MemoryAccess.putFloat(ccAddr + SDL_FColor.R, color != null ? color[0] : 0f);
MemoryAccess.putFloat(ccAddr + SDL_FColor.G, color != null ? color[1] : 0f);
MemoryAccess.putFloat(ccAddr + SDL_FColor.B, color != null ? color[2] : 0f);
MemoryAccess.putFloat(ccAddr + SDL_FColor.A, color != null ? color[3] : 0f);
putColorClear(targets.get(0).address(), handle, 0, color != null ? color[0] : 0f, color != null ? color[1] : 0f, color != null ? color[2] : 0f, color != null ? color[3] : 0f);
frameManager.noteClearPass();
frameManager.noteMaterializedClearPass();
frameManager.beginRenderPass(targets, null);
@@ -289,6 +256,68 @@ public final class FBOClearTracker {
}
}
public void materializePendingClearsForMipTargets(ContextState st, FboState fbo) {
for (int db : fbo.drawBuffers) {
if (db < 0 || db >= ContextState.MAX_COLOR_ATTACHMENTS || fbo.colorLevels[db] == 0) continue;
final long tex = fbo.colorTextures[db];
if (tex != 0 && st.pendingColorTextures.contains(tex)) materializePendingClearForTexture(st, tex);
}
if (fbo.depthLevel != 0 && fbo.depthTexture != 0 && (st.pendingDepthTextures.contains(fbo.depthTexture) || st.pendingStencilTextures.contains(fbo.depthTexture))) {
materializePendingClearForTexture(st, fbo.depthTexture);
}
}
public void clearColorLevel(long handle, int level, float r, float g, float b, float a) {
frameManager.endRenderPassIfActive(FrameManager.PASS_END_CLEAR);
try (var stack = MemoryStack.stackPush()) {
final SDL_GPUColorTargetInfo.Buffer targets = SDL_GPUColorTargetInfo.calloc(1, stack);
putColorClear(targets.get(0).address(), handle, level, r, g, b, a);
frameManager.noteClearPass();
frameManager.beginRenderPass(targets, null);
frameManager.endRenderPassIfActive(FrameManager.PASS_END_CLEAR);
}
}
public void clearDepthStencilLevel(long handle, int level, boolean clearDepth, float depth, boolean clearStencil, int stencil) {
if (!clearDepth && !clearStencil) return;
frameManager.endRenderPassIfActive(FrameManager.PASS_END_CLEAR);
depthStencilClearPass(handle, level, clearDepth, depth, clearStencil, stencil);
}
private void depthStencilClearPass(long handle, int level, boolean clearDepth, float depth, boolean clearStencil, int stencil) {
try (var stack = MemoryStack.stackPush()) {
final SDL_GPUDepthStencilTargetInfo dt = SDL_GPUDepthStencilTargetInfo.calloc(stack);
final long addr = dt.address();
MemoryAccess.putAddress(addr + SDL_GPUDepthStencilTargetInfo.TEXTURE, handle);
MemoryAccess.putByte(addr + SDL_GPUDepthStencilTargetInfo.MIP_LEVEL, (byte) level);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, clearDepth ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
if (clearDepth) {
MemoryAccess.putFloat(addr + SDL_GPUDepthStencilTargetInfo.CLEAR_DEPTH, depth);
}
if (clearStencil) {
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STENCIL_LOAD_OP, SDL_GPU_LOADOP_CLEAR);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STENCIL_STORE_OP, SDL_GPU_STOREOP_STORE);
MemoryAccess.putByte(addr + SDL_GPUDepthStencilTargetInfo.CLEAR_STENCIL, (byte) stencil);
}
frameManager.noteClearPass();
frameManager.beginRenderPass(null, dt);
frameManager.endRenderPassIfActive(FrameManager.PASS_END_CLEAR);
}
}
private static void putColorClear(long addr, long handle, int level, float r, float g, float b, float a) {
MemoryAccess.putAddress(addr + SDL_GPUColorTargetInfo.TEXTURE, handle);
MemoryAccess.putInt(addr + SDL_GPUColorTargetInfo.MIP_LEVEL, level);
MemoryAccess.putInt(addr + SDL_GPUColorTargetInfo.LOAD_OP, SDL_GPU_LOADOP_CLEAR);
MemoryAccess.putInt(addr + SDL_GPUColorTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
final long ccAddr = addr + SDL_GPUColorTargetInfo.CLEAR_COLOR;
MemoryAccess.putFloat(ccAddr + SDL_FColor.R, r);
MemoryAccess.putFloat(ccAddr + SDL_FColor.G, g);
MemoryAccess.putFloat(ccAddr + SDL_FColor.B, b);
MemoryAccess.putFloat(ccAddr + SDL_FColor.A, a);
}
public void materializeAllPendingClears(ContextState st) {
if (st.pendingColorTextures.isEmpty() && st.pendingDepthTextures.isEmpty() && st.pendingStencilTextures.isEmpty()) return;
final LongArrayList handles = st.samplerFlushColorHandles;
@@ -319,6 +348,7 @@ public final class FBOClearTracker {
if (fa == null || fb == null) return false;
if (fa.depthTexture != fb.depthTexture) return false;
if (fa.depthFormat != fb.depthFormat) return false;
if (fa.depthLevel != fb.depthLevel) return false;
if (fa.colorAttachmentCount != fb.colorAttachmentCount) return false;
if (fa.drawBuffers.length != fb.drawBuffers.length) return false;
for (int i = 0; i < fa.drawBuffers.length; i++) {
@@ -327,6 +357,7 @@ public final class FBOClearTracker {
for (int i = 0; i < ContextState.MAX_COLOR_ATTACHMENTS; i++) {
if (fa.colorTextures[i] != fb.colorTextures[i]) return false;
if (fa.colorFormats[i] != fb.colorFormats[i]) return false;
if (fa.colorLevels[i] != fb.colorLevels[i]) return false;
}
if (fboHasPendingClear(st, fb)) return false;
return true;
@@ -11,6 +11,7 @@ public final class FboState {
public final long[] colorTextures = new long[MAX_COLOR_ATTACHMENTS];
public final int[] colorGlIds = new int[MAX_COLOR_ATTACHMENTS];
public final int[] colorFormats = new int[MAX_COLOR_ATTACHMENTS];
public final int[] colorLevels = new int[MAX_COLOR_ATTACHMENTS];
public int colorAttachmentCount;
public int[] drawBuffers = {0};
@@ -19,6 +20,7 @@ public final class FboState {
public long depthTexture;
public int depthGlId;
public int depthFormat;
public int depthLevel;
public int width, height;
public int[] cachedColorFormats;
@@ -62,8 +64,10 @@ public final class FboState {
final long tex = (db >= 0 && db < MAX_COLOR_ATTACHMENTS) ? colorTextures[db] : 0L;
h = Hashing.fmix64(h, tex);
h = Hashing.fmix64(h, db);
h = Hashing.fmix64(h, tex != 0L ? colorLevels[db] : 0);
}
h = Hashing.fmix64(h, depthTexture);
h = Hashing.fmix64(h, depthLevel);
if (h == 0L || h == FrameManager.FBO0_LAYOUT_HASH) h = 0xD1CE5EEDL;
structuralLayoutHash = h;
}
@@ -73,6 +77,7 @@ public final class FboState {
colorTextures[colorIdx] = 0L;
colorGlIds[colorIdx] = 0;
colorFormats[colorIdx] = 0;
colorLevels[colorIdx] = 0;
int newCount = 0;
for (int i = 0; i < MAX_COLOR_ATTACHMENTS; i++) {
if (colorTextures[i] != 0L) newCount = i + 1;
@@ -86,6 +91,7 @@ public final class FboState {
depthTexture = 0L;
depthGlId = 0;
depthFormat = 0;
depthLevel = 0;
targetsDirty = true;
}
@@ -45,6 +45,7 @@ import static org.lwjgl.system.MemoryUtil.memAlloc;
import static org.lwjgl.system.MemoryUtil.memFree;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.texture.TextureStaging;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
@@ -125,6 +126,8 @@ public final class ResourceManager {
public void markFboAttachment(int glId) { fboAttachedGlIds.add(glId); }
public boolean isFboAttachment(int glId) { return fboAttachedGlIds.contains(glId); }
public boolean shouldDeferTextureUpload(ContextState st, int glId) { return st.deferUploads && !isFboAttachment(glId); }
private int preferredD24 = SDL_GPU_TEXTUREFORMAT_D24_UNORM;
private int preferredD24S8 = SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
private volatile int swapchainDepthStencilFormat = SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
@@ -586,6 +589,7 @@ public final class ResourceManager {
private boolean copyAllMips(long srcTex, long dstTex, int glTarget, int width, int height, int depth, int levels) {
final long cp = frameManager.ensureCopyPass();
if (cp == 0) return false;
flushBatchedUploads(cp);
final boolean volume = glTarget == GL12.GL_TEXTURE_3D;
final int layers = volume ? 1 : Math.max(1, depth);
try (var stack = stackPush()) {
@@ -1740,22 +1744,7 @@ public final class ResourceManager {
}
copyMappedFromData(mapped, data, (int) size, COPY_CALLSITE_UPLOAD_TEX_DIRECT);
SDL_UnmapGPUTransferBuffer(device.getDevice(), xfer);
try (var stack = stackPush()) {
final SDL_GPUTextureTransferInfo src = SDL_GPUTextureTransferInfo.calloc(stack)
.transfer_buffer(xfer)
.offset(0);
final SDL_GPUTextureRegion dst = SDL_GPUTextureRegion.calloc(stack)
.texture(gpuTexture)
.mip_level(level)
.x(x).y(y).z(z)
.w(w).h(h).d(d);
SDL_UploadToGPUTexture(copyPass, src, dst, false);
}
markTextureContentDefined(gpuTexture);
frameManager.recordUploadCommands(size, 1);
returnTransferBuffer(xfer, size);
uploadRegionFromTransfer(copyPass, xfer, size, gpuTexture, level, x, y, z, w, h, d);
}
private void directUploadToTexture(long copyPass, ByteBuffer data, long gpuTexture, int x, int y, int w, int h, int level) {
@@ -1772,24 +1761,28 @@ public final class ResourceManager {
}
copyMappedFromData(mapped, data, (int) size, COPY_CALLSITE_UPLOAD_TEX_DIRECT);
SDL_UnmapGPUTransferBuffer(device.getDevice(), xfer);
uploadRegionFromTransfer(copyPass, xfer, size, gpuTexture, level, x, y, 0, w, h, 1);
}
try (var stack = stackPush()) {
final SDL_GPUTextureTransferInfo src = SDL_GPUTextureTransferInfo.calloc(stack)
.transfer_buffer(xfer)
.offset(0);
final SDL_GPUTextureRegion dst = SDL_GPUTextureRegion.calloc(stack)
.texture(gpuTexture)
.mip_level(level)
.x(x).y(y).z(0)
.w(w).h(h).d(1);
SDL_UploadToGPUTexture(copyPass, src, dst, false);
private void uploadRegionFromTransfer(long copyPass, long xfer, long size, long tex, int level, int x, int y, int z, int w, int h, int d) {
try {
flushBatchedUploads(copyPass);
try (var stack = stackPush()) {
final SDL_GPUTextureTransferInfo src = SDL_GPUTextureTransferInfo.calloc(stack)
.transfer_buffer(xfer)
.offset(0);
final SDL_GPUTextureRegion dst = SDL_GPUTextureRegion.calloc(stack)
.texture(tex)
.mip_level(level)
.x(x).y(y).z(z)
.w(w).h(h).d(d);
SDL_UploadToGPUTexture(copyPass, src, dst, false);
}
markTextureContentDefined(tex);
frameManager.recordUploadCommands(size, 1);
} finally {
returnTransferBufferThreadSafe(xfer, size);
}
markTextureContentDefined(gpuTexture);
frameManager.recordUploadCommands(size, 1);
returnTransferBuffer(xfer, size);
}
public boolean enqueueDeferredTextureUpload(ContextState st, ByteBuffer prepped, long texHandle, int x, int y, int w, int h, int level) {
@@ -1806,15 +1799,54 @@ public final class ResourceManager {
prepped.position(prevPos);
}
SDL_UnmapGPUTransferBuffer(device.getDevice(), xfer);
enqueueMappedRegion(tt, st, xfer, size, texHandle, level, x, y, w, h);
return true;
}
private void enqueueMappedRegion(TransferThread tt, ContextState st, long xfer, long size, long tex, int level, int x, int y, int w, int h) {
final long seq = TransferThread.nextSeq();
st.frameHighestEnqueuedSeq = seq;
lastTextureUploadSeq = seq;
markTextureContentDefined(texHandle);
tt.enqueue(TransferThread.TextureRegionUpload.acquire(xfer, texHandle, x, y, w, h, level, size, seq));
markTextureContentDefined(tex);
tt.enqueue(TransferThread.TextureRegionUpload.acquire(xfer, tex, x, y, w, h, level, size, seq));
tt.wake();
}
public TextureStagingRegion beginTextureStaging(int glId, long texHandle, boolean bgra, int level, int x, int y, int w, int h) {
final long size = (long) w * h * 4;
if (size <= 0 || size > Integer.MAX_VALUE) return null;
final long xfer = acquireTransferBufferThreadSafe(size);
if (xfer == 0) return null;
final ByteBuffer mapped = SDL_MapGPUTransferBuffer(device.getDevice(), xfer, true, getTransferBufferMapSize(size));
if (mapped == null || mapped.capacity() < size) {
SDL_UnmapGPUTransferBuffer(device.getDevice(), xfer);
returnTransferBufferThreadSafe(xfer, size);
return null;
}
mapped.clear().limit((int) size);
return new TextureStagingRegion(mapped.slice(), bgra, level, x, y, w, h, glId, texHandle, xfer, size);
}
public boolean commitTextureStagingDeferred(ContextState st, TextureStagingRegion s) {
final TransferThread tt = transferThread;
if (tt == null) return false;
SDL_UnmapGPUTransferBuffer(device.getDevice(), s.transferBuffer());
enqueueMappedRegion(tt, st, s.transferBuffer(), s.size(), s.texHandle(), s.level(), s.x(), s.y(), s.width(), s.height());
return true;
}
public void commitTextureStagingInline(long copyPass, TextureStagingRegion s) {
SDL_UnmapGPUTransferBuffer(device.getDevice(), s.transferBuffer());
uploadRegionFromTransfer(copyPass, s.transferBuffer(), s.size(), s.texHandle(), s.level(), s.x(), s.y(), 0, s.width(), s.height(), 1);
}
public void abandonTextureStaging(TextureStagingRegion s) {
SDL_UnmapGPUTransferBuffer(device.getDevice(), s.transferBuffer());
returnTransferBufferThreadSafe(s.transferBuffer(), s.size());
}
public record TextureStagingRegion(ByteBuffer buffer, boolean bgra, int level, int x, int y, int width, int height, int glId, long texHandle, long transferBuffer, long size) implements TextureStaging {}
public void downloadFromTexture(long commandBuffer, long gpuTexture, int x, int y, int w, int h, int level, ByteBuffer output) {
downloadFromTexture(commandBuffer, gpuTexture, x, y, 0, w, h, 1, level, output);
}
@@ -47,7 +47,7 @@ public final class TextureOps {
public boolean uploadTextureRegion(ContextState st, int glId, ResourceManager.TextureMeta meta, long texHandle, ByteBuffer src, int x, int y, int w, int h, int level, int srcFormat, int srcType) {
if (meta == null || level >= meta.levels()) return false;
if (src == null || texHandle == 0 || frameManager.getCommandBuffer() == 0) return false;
final boolean defer = st.deferUploads && !resourceManager.isFboAttachment(glId);
final boolean defer = resourceManager.shouldDeferTextureUpload(st, glId);
final long cp = defer ? 0L : frameManager.ensureCopyPass();
if (!defer && cp == 0) return false;
final ByteBuffer unpacked = PixelOps.applyUnpackPixelStore(src, w, h, srcFormat, srcType, st.pixelStore);
@@ -161,8 +161,9 @@ public final class TextureOps {
if (srcTex == 0) return;
final int srcGlId = isDepthDest ? fbo.depthGlId : fbo.colorGlIds[fbo.readBufferIndex];
final int srcLevel = isDepthDest ? fbo.depthLevel : fbo.colorLevels[fbo.readBufferIndex];
final ResourceManager.TextureMeta srcMeta = srcGlId != 0 ? resourceManager.getTextureMeta(srcGlId) : null;
final long clip = CopyRectClip.clipCopyRect(x, y, xoffset, yoffset, width, height, srcMeta != null ? srcMeta.width() : fbo.width, srcMeta != null ? srcMeta.height() : fbo.height, levelWidth(destMeta, level), levelHeight(destMeta, level));
final long clip = CopyRectClip.clipCopyRect(x, y, xoffset, yoffset, width, height, srcMeta != null ? levelWidth(srcMeta, srcLevel) : fbo.width, srcMeta != null ? levelHeight(srcMeta, srcLevel) : fbo.height, levelWidth(destMeta, level), levelHeight(destMeta, level));
if (clip == CopyRectClip.EMPTY) return;
final int sx = CopyRectClip.srcX(clip);
final int sy = CopyRectClip.srcY(clip);
@@ -175,17 +176,17 @@ public final class TextureOps {
fboClearTracker.resolveDestinationForWrite(st, destTex, destMeta, level, dx, dy, 0, w, h, true);
if (isDepthDest || (srcMeta != null && destMeta != null && srcMeta.sdlFormat() == destMeta.sdlFormat())) {
copyTexture(srcTex, sx, sy, destTex, level, dx, dy, w, h);
copyTexture(srcTex, srcLevel, sx, sy, destTex, level, dx, dy, w, h);
} else {
blitTexture(srcTex, sx, sy, w, h, destTex, level, dx, dy, w, h, GL11.GL_NEAREST);
blitTexture(srcTex, srcLevel, sx, sy, w, h, destTex, level, dx, dy, w, h, GL11.GL_NEAREST);
}
}
private static int levelWidth(ResourceManager.TextureMeta meta, int level) {
public static int levelWidth(ResourceManager.TextureMeta meta, int level) {
return meta != null ? PixelOps.mipLevelSize(meta.width(), level) : 0;
}
private static int levelHeight(ResourceManager.TextureMeta meta, int level) {
public static int levelHeight(ResourceManager.TextureMeta meta, int level) {
return meta != null ? PixelOps.mipLevelSize(meta.height(), level) : 0;
}
@@ -213,31 +214,34 @@ public final class TextureOps {
}
fboClearTracker.resolveDestinationForWrite(st, destTex, destMeta, level, dx, dy, 0, w, h, true);
blitTexture(srcTex, sx, srcFullH - sy - h, w, h, destTex, level, dx, dy, w, h, GL11.GL_NEAREST, SDL_FLIP_VERTICAL);
blitTexture(srcTex, 0, sx, srcFullH - sy - h, w, h, destTex, level, dx, dy, w, h, GL11.GL_NEAREST, SDL_FLIP_VERTICAL);
}
public static boolean canCopyInsteadOfBlit(ResourceManager.TextureMeta src, ResourceManager.TextureMeta dst, int srcW, int srcH, int dstW, int dstH) {
return src != null && dst != null && src.sdlFormat() == dst.sdlFormat() && srcW > 0 && srcH > 0 && srcW == dstW && srcH == dstH;
}
public void copyTexture(long srcTex, int srcX, int srcY, long dstTex, int dstLevel, int dstX, int dstY, int w, int h) {
copyTexture(frameManager.ensureCopyPass(), srcTex, srcX, srcY, dstTex, dstLevel, dstX, dstY, w, h);
public void copyTexture(long srcTex, int srcLevel, int srcX, int srcY, long dstTex, int dstLevel, int dstX, int dstY, int w, int h) {
final long cp = frameManager.ensureCopyPass();
if (cp == 0) return;
resourceManager.flushBatchedUploads(cp);
copyTexture(cp, srcTex, srcLevel, srcX, srcY, dstTex, dstLevel, dstX, dstY, w, h);
}
public static void copyTexture(long cp, long srcTex, int srcX, int srcY, long dstTex, int dstLevel, int dstX, int dstY, int w, int h) {
public static void copyTexture(long cp, long srcTex, int srcLevel, int srcX, int srcY, long dstTex, int dstLevel, int dstX, int dstY, int w, int h) {
if (cp == 0) return;
try (var stack = MemoryStack.stackPush()) {
final var src = SDL_GPUTextureLocation.calloc(stack).texture(srcTex).x(srcX).y(srcY);
final var src = SDL_GPUTextureLocation.calloc(stack).texture(srcTex).mip_level(srcLevel).x(srcX).y(srcY);
final var dst = SDL_GPUTextureLocation.calloc(stack).texture(dstTex).mip_level(dstLevel).x(dstX).y(dstY);
SDL_CopyGPUTextureToTexture(cp, src, dst, w, h, 1, false);
}
}
public void blitTexture(long srcTex, int srcX, int srcY, int srcW, int srcH, long dstTex, int dstLevel, int dstX, int dstY, int dstW, int dstH, int glFilter) {
blitTexture(srcTex, srcX, srcY, srcW, srcH, dstTex, dstLevel, dstX, dstY, dstW, dstH, glFilter, SDL_FLIP_NONE);
public void blitTexture(long srcTex, int srcLevel, int srcX, int srcY, int srcW, int srcH, long dstTex, int dstLevel, int dstX, int dstY, int dstW, int dstH, int glFilter) {
blitTexture(srcTex, srcLevel, srcX, srcY, srcW, srcH, dstTex, dstLevel, dstX, dstY, dstW, dstH, glFilter, SDL_FLIP_NONE);
}
public void blitTexture(long srcTex, int srcX, int srcY, int srcW, int srcH, long dstTex, int dstLevel, int dstX, int dstY, int dstW, int dstH, int glFilter, int flipMode) {
public void blitTexture(long srcTex, int srcLevel, int srcX, int srcY, int srcW, int srcH, long dstTex, int dstLevel, int dstX, int dstY, int dstW, int dstH, int glFilter, int flipMode) {
frameManager.endCopyPassIfActive();
frameManager.endRenderPassIfActive(FrameManager.PASS_END_COPY);
final long cb = frameManager.getCommandBuffer();
@@ -245,8 +249,8 @@ public final class TextureOps {
try (var stack = MemoryStack.stackPush()) {
final var info = SDL_GPUBlitInfo.calloc(stack);
info.source(s -> s.texture(srcTex).x(srcX).y(srcY).w(srcW).h(srcH));
info.destination(d -> d.texture(dstTex).mip_level(dstLevel).x(dstX).y(dstY).w(dstW).h(dstH));
info.source().texture(srcTex).mip_level(srcLevel).x(srcX).y(srcY).w(srcW).h(srcH);
info.destination().texture(dstTex).mip_level(dstLevel).x(dstX).y(dstY).w(dstW).h(dstH);
info.filter(glFilter == GL11.GL_LINEAR ? SDL_GPU_FILTER_LINEAR : SDL_GPU_FILTER_NEAREST);
info.flip_mode(flipMode);
frameManager.noteBlit();
@@ -4,10 +4,10 @@ import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
import com.gtnewhorizons.angelica.glsm.hooks.ShaderWorkSubmitter;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
import com.gtnewhorizons.angelica.glsm.threading.AngelicaWorkers;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import org.taumc.glsl.grammar.GLSLParser;
@@ -146,6 +146,7 @@ public final class ShaderManager {
public void shaderSource(int shader, CharSequence source) {
final ShaderObject obj = shaderObjects.get(shader);
if (obj == null) return;
discardSpirvFuture(obj);
String raw = source.toString();
if (!raw.isEmpty() && raw.charAt(raw.length() - 1) == '\0') {
@@ -158,7 +159,7 @@ public final class ShaderManager {
obj.reflection = hit.reflection();
obj.graphicsBindingMap = hit.graphicsBindingMap();
obj.boolUniforms = hit.boolUniforms();
obj.spirvFuture = CompletableFuture.completedFuture(new SpirvCompiler.Result(hit.spirv(), null, null));
obj.spirvFuture = CompletableFuture.completedFuture(new AsyncCompile(new SpirvCompiler.Result(hit.spirv(), null, null), hit.graphicsBindingMap(), hit.reflection()));
return;
}
@@ -186,22 +187,16 @@ public final class ShaderManager {
obj.boolUniforms = transformed.boolUniforms();
obj.source = transformed.source();
final ShaderWorkSubmitter submitter = GLSMHooks.shaderWorkSubmitter;
if (submitter != null) {
final int shaderKind = shaderKindFor(obj);
final int glType = obj.type;
final String finalSrc = obj.source;
obj.spirvFuture = submitter.submit(() -> {
final SpirvCompiler.Result r = SpirvCompiler.compile(finalSrc, shaderKind, "shader" + shader, SpirvCompiler.Options.vulkanForced460Core());
if (r.spirv() != null && (glType == GL20.GL_VERTEX_SHADER || glType == GL20.GL_FRAGMENT_SHADER)) {
obj.graphicsBindingMap = remapSpirvForSDLGPU(r.spirv(), glType);
obj.reflection = reflectStage(r.spirv(), glType == GL20.GL_VERTEX_SHADER);
}
return r;
});
} else {
obj.spirvFuture = null;
}
final int shaderKind = shaderKindFor(obj);
final int glType = obj.type;
final String finalSrc = obj.source;
obj.spirvFuture = AngelicaWorkers.submit(() -> {
final SpirvCompiler.Result r = SpirvCompiler.compile(finalSrc, shaderKind, "shader" + shader, SpirvCompiler.Options.vulkanForced460Core());
if (r.spirv() != null && (glType == GL20.GL_VERTEX_SHADER || glType == GL20.GL_FRAGMENT_SHADER)) {
return new AsyncCompile(r, remapSpirvForSDLGPU(r.spirv(), glType), reflectStage(r.spirv(), glType == GL20.GL_VERTEX_SHADER));
}
return new AsyncCompile(r, null, null);
});
}
private static int shaderKindFor(ShaderObject obj) {
@@ -312,8 +307,11 @@ public final class ShaderManager {
final SpirvCompiler.Result r;
final boolean alreadyRemapped;
if (obj.spirvFuture != null) {
r = obj.spirvFuture.join();
final AsyncCompile compiled = obj.spirvFuture.join();
obj.spirvFuture = null;
r = compiled.result();
if (compiled.graphicsBindingMap() != null) obj.graphicsBindingMap = compiled.graphicsBindingMap();
if (compiled.reflection() != null) obj.reflection = compiled.reflection();
alreadyRemapped = true;
} else {
r = SpirvCompiler.compile(obj.source, shaderKindFor(obj), "shader" + shader, SpirvCompiler.Options.vulkanForced460Core());
@@ -325,6 +323,7 @@ public final class ShaderManager {
LOG.error("Shader {} compilation failed: {} (dump={})", shader, obj.infoLog, r.dumpPath());
return;
}
if (obj.spirv != null && obj.spirv != r.spirv()) memFree(obj.spirv);
obj.spirv = r.spirv();
if (!alreadyRemapped && (obj.type == GL20.GL_VERTEX_SHADER || obj.type == GL20.GL_FRAGMENT_SHADER)) {
obj.graphicsBindingMap = remapSpirvForSDLGPU(obj.spirv, obj.type);
@@ -342,6 +341,7 @@ public final class ShaderManager {
return;
}
shaderObjects.remove(shader);
discardSpirvFuture(obj);
if (obj.spirv != null) memFree(obj.spirv);
}
@@ -352,10 +352,24 @@ public final class ShaderManager {
if (obj.attachCount > 0) obj.attachCount--;
if (obj.deletePending && obj.attachCount == 0) {
shaderObjects.remove(shader);
discardSpirvFuture(obj);
if (obj.spirv != null) memFree(obj.spirv);
}
}
private static void discardSpirvFuture(ShaderObject obj) {
final CompletableFuture<AsyncCompile> future = obj.spirvFuture;
if (future == null) return;
obj.spirvFuture = null;
future.thenAccept(ShaderManager::freeSpirvResult);
}
private static void freeSpirvResult(AsyncCompile compiled) {
if (compiled != null && compiled.result().spirv() != null) memFree(compiled.result().spirv());
}
private record AsyncCompile(SpirvCompiler.Result result, GraphicsBindingMap graphicsBindingMap, StageReflection reflection) {}
public boolean isCompiled(int shader) {
final ShaderObject obj = shaderObjects.get(shader);
return obj != null && obj.compiled;
@@ -1373,6 +1387,7 @@ public final class ShaderManager {
}
programObjects.clear();
for (ShaderObject obj : shaderObjects.values()) {
discardSpirvFuture(obj);
if (obj.spirv != null) memFree(obj.spirv);
}
shaderObjects.clear();
@@ -1386,7 +1401,7 @@ public final class ShaderManager {
public String infoLog = "";
public boolean deletePending;
public int attachCount;
public CompletableFuture<SpirvCompiler.Result> spirvFuture;
public CompletableFuture<AsyncCompile> spirvFuture;
public StageReflection reflection = StageReflection.EMPTY;
public GraphicsBindingMap graphicsBindingMap = GraphicsBindingMap.EMPTY;
public Set<String> boolUniforms = Set.of();
@@ -19,6 +19,7 @@ import org.lwjgl.opengl.GL30;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import static com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig.SIZE;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -178,6 +179,44 @@ class GlsmSdlCopyBlitTest {
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, 4, SIZE - 5, GREEN, "color aspect of a color+depth blit");
}
private static void blitLevelOneInto(int dstSize, int dstLevel) {
final int source = GlsmSdlHeadlessRig.createSolidMipTexture(SIZE, 1, RED);
uploadLevel(1, SIZE / 2, GREEN);
final int destination = GlsmSdlHeadlessRig.createSolidMipTexture(dstSize, dstLevel, BLUE);
final int readFbo = GlsmSdlHeadlessRig.fboWithColor(source, 1);
final int drawFbo = GlsmSdlHeadlessRig.fboWithColor(destination, dstLevel);
final int dstExtent = Math.max(1, dstSize >> dstLevel);
GLStateManager.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, readFbo);
GLStateManager.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, drawFbo);
GLStateManager.glBlitFramebuffer(0, 0, SIZE / 2, SIZE / 2, 0, 0, dstExtent, dstExtent, GL11.GL_COLOR_BUFFER_BIT, GL11.GL_NEAREST);
GlsmSdlHeadlessRig.assertUniform(GlsmSdlHeadlessRig.readTextureLevel(destination, dstLevel), GREEN, "destination level " + dstLevel + " after blitting source level 1");
GlsmSdlHeadlessRig.assertUniform(GlsmSdlHeadlessRig.readTextureLevel(source, 0), RED, "source level 0 is not the blit source");
if (dstLevel != 0) GlsmSdlHeadlessRig.assertUniform(GlsmSdlHeadlessRig.readTextureLevel(destination, 0), BLUE, "destination level 0 is not the blit target");
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLStateManager.glDeleteFramebuffers(readFbo);
GLStateManager.glDeleteFramebuffers(drawFbo);
GLStateManager.glDeleteTextures(source);
GLStateManager.glDeleteTextures(destination);
}
@Test
void blitFramebufferReadsTheSourceAttachmentLevel() {
blitLevelOneInto(SIZE / 2, 0);
}
@Test
void scaledBlitFramebufferReadsTheSourceAttachmentLevel() {
blitLevelOneInto(SIZE, 0);
}
@Test
void blitFramebufferWritesTheDestinationAttachmentLevel() {
blitLevelOneInto(SIZE, 1);
}
@Test
void blitFramebufferIntoFbo0SurvivesTheFramesFirstFbo0Pass() {
GlsmSdlHeadlessRig.bindTarget();
@@ -245,4 +284,138 @@ class GlsmSdlCopyBlitTest {
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, SIZE / 2, 4, RED, "stencil carried by the blit");
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, SIZE / 2, SIZE - 5, BLUE, "outside the blitted stencil");
}
private static final int UNTOUCHED = 0xFF101010;
private static final int CELL = 8;
private static int pattern(int x, int y) {
return 0xFF000040 | ((x / CELL) * 32 << 16) | ((y / CELL) * 32 << 8);
}
private static int createPatternTexture() {
final int id = GlsmSdlHeadlessRig.newNearestTexture();
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 0);
final ByteBuffer texels = MemoryUtil.memAlloc(SIZE * SIZE * 4);
try {
for (int y = 0; y < SIZE; y++) {
for (int x = 0; x < SIZE; x++) {
final int argb = pattern(x, y);
final int i = (y * SIZE + x) * 4;
texels.put(i, (byte) ((argb >> 16) & 0xFF));
texels.put(i + 1, (byte) ((argb >> 8) & 0xFF));
texels.put(i + 2, (byte) (argb & 0xFF));
texels.put(i + 3, (byte) ((argb >>> 24) & 0xFF));
}
}
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, SIZE, SIZE, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, texels);
} finally {
MemoryUtil.memFree(texels);
}
return id;
}
private static int[] blitPattern(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1) {
final int source = createPatternTexture();
final int destination = GlsmSdlHeadlessRig.createSolidMipTexture(SIZE, 0, UNTOUCHED);
final int readFbo = GlsmSdlHeadlessRig.fboWithColor(source, 0);
final int drawFbo = GlsmSdlHeadlessRig.fboWithColor(destination, 0);
GLStateManager.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, readFbo);
GLStateManager.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, drawFbo);
GLStateManager.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, GL11.GL_COLOR_BUFFER_BIT, GL11.GL_NEAREST);
final int[] pixels = GlsmSdlHeadlessRig.readTextureLevel(destination, 0);
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLStateManager.glDeleteFramebuffers(readFbo);
GLStateManager.glDeleteFramebuffers(drawFbo);
GLStateManager.glDeleteTextures(source);
GLStateManager.glDeleteTextures(destination);
return pixels;
}
@Test
void blitFramebufferWithReversedSourceXMirrorsHorizontally() {
final int[] pixels = blitPattern(SIZE, 0, 0, SIZE, 0, 0, SIZE, SIZE);
for (int y = 0; y < SIZE; y++) {
for (int x = 0; x < SIZE; x++) {
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, x, y, pattern(SIZE - 1 - x, y), "horizontal mirror");
}
}
}
@Test
void blitFramebufferWithReversedDestinationYMirrorsVertically() {
final int[] pixels = blitPattern(0, 0, SIZE, SIZE, 0, SIZE, SIZE, 0);
for (int y = 0; y < SIZE; y++) {
for (int x = 0; x < SIZE; x++) {
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, x, y, pattern(x, SIZE - 1 - y), "vertical mirror");
}
}
}
@Test
void blitFramebufferWithBothRangesReversedDoesNotMirror() {
final int[] pixels = blitPattern(SIZE, SIZE, 0, 0, SIZE, SIZE, 0, 0);
for (int y = 0; y < SIZE; y++) {
for (int x = 0; x < SIZE; x++) {
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, x, y, pattern(x, y), "both ranges reversed");
}
}
}
@Test
void mirroredScaledBlitClipsTheOutOfBoundsSourceOnTheMirroredSide() {
final int[] pixels = blitPattern(SIZE + SIZE / 2, 0, SIZE / 2, SIZE / 2, 0, 0, SIZE / 2, SIZE);
for (int y = 0; y < SIZE; y++) {
for (int x = 0; x < SIZE; x++) {
if (x < SIZE / 4 || x >= SIZE / 2) {
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, x, y, UNTOUCHED, "outside the in-bounds part of the mirrored source");
continue;
}
final int sx = SIZE + SIZE / 2 - 1 - 2 * x;
final int sy = y / 2;
if ((sx - 1) / CELL != (sx + 1) / CELL || (sy - 1) / CELL != (sy + 1) / CELL) continue;
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, x, y, pattern(sx, sy), "mirrored scaled blit");
}
}
}
@Test
void blitFramebufferWritesEveryDrawBuffer() {
final int source = createPatternTexture();
final int first = GlsmSdlHeadlessRig.createSolidMipTexture(SIZE, 0, UNTOUCHED);
final int skipped = GlsmSdlHeadlessRig.createSolidMipTexture(SIZE, 0, UNTOUCHED);
final int third = GlsmSdlHeadlessRig.createSolidMipTexture(SIZE, 0, UNTOUCHED);
final int readFbo = GlsmSdlHeadlessRig.fboWithColor(source, 0);
final int drawFbo = GlsmSdlHeadlessRig.fboWithColor(first, 0);
GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT1, GL11.GL_TEXTURE_2D, skipped, 0);
GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT2, GL11.GL_TEXTURE_2D, third, 0);
final IntBuffer bufs = MemoryUtil.memAllocInt(3);
try {
bufs.put(0, GL30.GL_COLOR_ATTACHMENT0).put(1, GL11.GL_NONE).put(2, GL30.GL_COLOR_ATTACHMENT2);
GLStateManager.glDrawBuffers(bufs);
} finally {
MemoryUtil.memFree(bufs);
}
GLStateManager.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, readFbo);
GLStateManager.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, drawFbo);
GLStateManager.glBlitFramebuffer(0, 0, SIZE, SIZE, 0, 0, SIZE, SIZE, GL11.GL_COLOR_BUFFER_BIT, GL11.GL_NEAREST);
final int[] firstPixels = GlsmSdlHeadlessRig.readTextureLevel(first, 0);
final int[] thirdPixels = GlsmSdlHeadlessRig.readTextureLevel(third, 0);
for (int y = 0; y < SIZE; y++) {
for (int x = 0; x < SIZE; x++) {
GlsmSdlHeadlessRig.assertPixel(firstPixels, SIZE, x, y, pattern(x, y), "draw buffer 0");
GlsmSdlHeadlessRig.assertPixel(thirdPixels, SIZE, x, y, pattern(x, y), "draw buffer 2");
}
}
GlsmSdlHeadlessRig.assertUniform(GlsmSdlHeadlessRig.readTextureLevel(skipped, 0), UNTOUCHED, "attachment 1 behind a GL_NONE draw buffer");
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLStateManager.glDeleteFramebuffers(readFbo);
GLStateManager.glDeleteFramebuffers(drawFbo);
GLStateManager.glDeleteTextures(source);
GLStateManager.glDeleteTextures(first);
GLStateManager.glDeleteTextures(skipped);
GLStateManager.glDeleteTextures(third);
}
}
@@ -213,12 +213,24 @@ public final class GlsmSdlHeadlessRig {
GLStateManager.glFogf(GL11.GL_FOG_END, 1.0e7f);
}
public static int createSolidTexture(int argb) {
public static int newNearestTexture() {
final int id = GLStateManager.glGenTextures();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, id);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
return id;
}
public static int fboWithColor(int texture, int level) {
final int fbo = GLStateManager.glGenFramebuffers();
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo);
GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, texture, level);
return fbo;
}
public static int createSolidTexture(int argb) {
final int id = newNearestTexture();
final ByteBuffer texels = MemoryUtil.memAlloc(2 * 2 * 4);
try {
for (int i = 0; i < 4; i++) {
@@ -313,20 +325,63 @@ public final class GlsmSdlHeadlessRig {
try {
for (int i = 0; i < w * h * 4; i++) pixels.put(i, fill);
GLStateManager.glReadPixels(x, y, w, h, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
final int[] out = new int[w * h];
for (int i = 0; i < out.length; i++) {
final int r = pixels.get(i * 4) & 0xFF;
final int g = pixels.get(i * 4 + 1) & 0xFF;
final int b = pixels.get(i * 4 + 2) & 0xFF;
final int a = pixels.get(i * 4 + 3) & 0xFF;
out[i] = (a << 24) | (r << 16) | (g << 8) | b;
}
return out;
return toArgb(pixels, w * h);
} finally {
MemoryUtil.memFree(pixels);
}
}
public static int createSolidMipTexture(int size, int maxLevel, int argb) {
final int id = GLStateManager.glGenTextures();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, id);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST_MIPMAP_NEAREST);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, maxLevel);
for (int level = 0; level <= maxLevel; level++) {
final int levelSize = Math.max(1, size >> level);
final ByteBuffer texels = MemoryUtil.memAlloc(levelSize * levelSize * 4);
try {
for (int i = 0; i < levelSize * levelSize; i++) {
texels.put(i * 4, (byte) ((argb >> 16) & 0xFF));
texels.put(i * 4 + 1, (byte) ((argb >> 8) & 0xFF));
texels.put(i * 4 + 2, (byte) (argb & 0xFF));
texels.put(i * 4 + 3, (byte) ((argb >>> 24) & 0xFF));
}
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, level, GL11.GL_RGBA8, levelSize, levelSize, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, texels);
} finally {
MemoryUtil.memFree(texels);
}
}
return id;
}
public static int[] readTextureLevel(int texture, int level) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
final int w = GLStateManager.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, level, GL11.GL_TEXTURE_WIDTH);
final int h = GLStateManager.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, level, GL11.GL_TEXTURE_HEIGHT);
final ByteBuffer pixels = MemoryUtil.memAlloc(w * h * 4);
try {
GLStateManager.glGetTexImage(GL11.GL_TEXTURE_2D, level, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
return toArgb(pixels, w * h);
} finally {
MemoryUtil.memFree(pixels);
}
}
public static int[] toArgb(ByteBuffer pixels, int count) {
final int[] out = new int[count];
for (int i = 0; i < count; i++) {
final int r = pixels.get(i * 4) & 0xFF;
final int g = pixels.get(i * 4 + 1) & 0xFF;
final int b = pixels.get(i * 4 + 2) & 0xFF;
final int a = pixels.get(i * 4 + 3) & 0xFF;
out[i] = (a << 24) | (r << 16) | (g << 8) | b;
}
return out;
}
public static int pixelAt(int[] pixels, int rowStride, int x, int y) {
return pixels[y * rowStride + x];
}
@@ -0,0 +1,91 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.junit.jupiter.api.BeforeAll;
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.GL30;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import static com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig.assertUniform;
import static com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig.createSolidMipTexture;
import static com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig.readTextureLevel;
@Tag("glsm-sdl")
@ExtendWith(GlsmSdlFrameExtension.class)
class GlsmSdlMipAttachmentTest {
private static final int TEX_SIZE = 64;
private static final int MAX_LEVEL = 6;
private static final int CLEAR = 0xFF204080;
private static final int BASE = 0xFF10C030;
private static final int RED = 0xFFFF0000;
@BeforeAll
static void boot() {
GlsmSdlHeadlessRig.boot();
}
private static void setClearColor(int argb) {
GLStateManager.glClearColor(((argb >> 16) & 0xFF) / 255.0f, ((argb >> 8) & 0xFF) / 255.0f, (argb & 0xFF) / 255.0f, ((argb >>> 24) & 0xFF) / 255.0f);
}
private static int levelColor(int level) {
return (CLEAR + level) | 0xFF000000;
}
@Test
void clearingEachLevelWritesOnlyThatLevel() {
final int texture = createSolidMipTexture(TEX_SIZE, MAX_LEVEL, BASE);
final int fbo = GlsmSdlHeadlessRig.fboWithColor(texture, 0);
for (int level = 1; level <= MAX_LEVEL; level++) {
final int extent = Math.max(1, TEX_SIZE >> level);
GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, texture, level);
GLStateManager.glViewport(0, 0, extent, extent);
setClearColor(levelColor(level));
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
}
for (int level = 1; level <= MAX_LEVEL; level++) {
assertUniform(readTextureLevel(texture, level), levelColor(level), "level " + level + " after clearing its own attachment");
}
assertUniform(readTextureLevel(texture, 0), BASE, "level 0 untouched by clearing the mip levels");
GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, texture, 1);
final int w = Math.max(1, TEX_SIZE >> 1);
final ByteBuffer pixels = MemoryUtil.memAlloc(w * w * 4);
try {
GLStateManager.glReadPixels(0, 0, w, w, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
assertUniform(GlsmSdlHeadlessRig.toArgb(pixels, w * w), levelColor(1), "readPixels of the level 1 attachment");
} finally {
MemoryUtil.memFree(pixels);
}
GLStateManager.glDeleteFramebuffers(fbo);
GLStateManager.glDeleteTextures(texture);
}
@Test
void pendingLevelZeroClearIsNotAppliedToALevelOnePass() {
final int texture = createSolidMipTexture(TEX_SIZE, MAX_LEVEL, BASE);
final int fbo = GlsmSdlHeadlessRig.fboWithColor(texture, 0);
GLStateManager.glViewport(0, 0, TEX_SIZE, TEX_SIZE);
setClearColor(CLEAR);
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, texture, 1);
GLStateManager.glViewport(0, 0, TEX_SIZE / 2, TEX_SIZE / 2);
GlsmSdlHeadlessRig.solidQuad(1.0f, 0.0f, 0.0f);
assertUniform(readTextureLevel(texture, 1), RED, "level 1 after drawing into it");
assertUniform(readTextureLevel(texture, 0), CLEAR, "level 0 keeps its own pending clear");
GLStateManager.glDeleteFramebuffers(fbo);
GLStateManager.glDeleteTextures(texture);
}
}
@@ -0,0 +1,27 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public final class GlsmSdlOutOfFrameExtension implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
GlsmSdlHeadlessRig.endFrame();
final FrameManager fm = Reflect.get(BackendManager.RENDER_BACKEND, "frameManager");
assertFalse(fm.isFrameActive(), "test must start with no active frame");
}
@Override
public void afterEach(ExtensionContext context) {
GlsmSdlHeadlessRig.endFrame();
assertTrue(GlsmSdlRedirectAgent.failures().isEmpty(), () -> "redirect agent recorded failures: " + GlsmSdlRedirectAgent.failures());
}
}
@@ -0,0 +1,100 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.fail;
@Tag("glsm-sdl")
@ExtendWith(GlsmSdlOutOfFrameExtension.class)
class GlsmSdlOutOfFrameReadbackTest {
private static final int TEX_SIZE = 64;
@BeforeAll
static void boot() {
GlsmSdlHeadlessRig.boot();
}
@Test
void outOfFrameTexImageReadback() {
final ByteBuffer pattern = pattern(TEX_SIZE, TEX_SIZE, 1);
final int tex = GlsmSdlHeadlessRig.newNearestTexture();
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, TEX_SIZE, TEX_SIZE, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pattern);
assertReadback(tex, TEX_SIZE, TEX_SIZE, pattern);
GLStateManager.glDeleteTextures(tex);
}
@Test
void inFrameCopyImageSubDataReadback() {
GlsmSdlHeadlessRig.beginFrame();
copyAndReadBack(3);
}
@Test
void outOfFrameCopyImageSubDataReadback() {
copyAndReadBack(4);
}
private static void copyAndReadBack(int seed) {
final ByteBuffer pattern = pattern(TEX_SIZE, TEX_SIZE, seed);
final int src = GlsmSdlHeadlessRig.newNearestTexture();
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, TEX_SIZE, TEX_SIZE, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pattern);
final int dst = GlsmSdlHeadlessRig.newNearestTexture();
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, TEX_SIZE, TEX_SIZE, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null);
GLStateManager.glCopyImageSubData(src, GL11.GL_TEXTURE_2D, 0, 0, 0, 0, dst, GL11.GL_TEXTURE_2D, 0, 0, 0, 0, TEX_SIZE, TEX_SIZE, 1);
assertReadback(dst, TEX_SIZE, TEX_SIZE, pattern);
GLStateManager.glDeleteTextures(src);
GLStateManager.glDeleteTextures(dst);
}
private static ByteBuffer pattern(int w, int h, int seed) {
final ByteBuffer buf = BufferUtils.createByteBuffer(w * h * 4);
for (int i = 0; i < w * h; i++) {
buf.put((byte) (i * 7 + seed));
buf.put((byte) (i * 13 + 0x40));
buf.put((byte) (i * 29 + seed * 3));
buf.put((byte) (0x80 | i));
}
return buf.flip();
}
private static void assertReadback(int tex, int w, int h, ByteBuffer expected) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex);
final ByteBuffer got = BufferUtils.createByteBuffer(w * h * 4);
for (int i = 0; i < got.capacity(); i++) got.put(i, (byte) 0xCD);
GLStateManager.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, got);
final int n = w * h * 4;
int mismatches = 0;
int zeros = 0;
int untouched = 0;
int first = -1;
for (int i = 0; i < n; i++) {
final int g = got.get(i) & 0xFF;
if (g == 0) zeros++;
if (g == 0xCD) untouched++;
if (g != (expected.get(i) & 0xFF)) {
mismatches++;
if (first < 0) first = i;
}
}
if (mismatches == 0) return;
fail("readback mismatch: " + mismatches + "/" + n + " bytes differ, zeroBytes=" + zeros + " untouched0xCD=" + untouched + ", firstMismatch=" + first + "\n expected[0..16]=" + hex(expected, 16) + "\n actual[0..16] =" + hex(got, 16));
}
private static String hex(ByteBuffer buf, int count) {
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) sb.append(String.format(" %02x", buf.get(i) & 0xFF));
return sb.toString();
}
}
@@ -0,0 +1,183 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.glsm.texture.TextureStaging;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import com.gtnewhorizons.angelica.sdlgpu.resource.TransferThread;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL30;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@Tag("glsm-sdl")
@ExtendWith(GlsmSdlOutOfFrameExtension.class)
class GlsmSdlTextureStagingTest {
private static final int STAGING_SIZE = 64;
private static final int BIG_TEX = 2048;
private static final int BIG_W = 1024;
private static final int BIG_H = 1100;
private static final int SMALL = 16;
private static final int COLOR_A = 0xFF10E020;
private static final int COLOR_B = 0xFF3050C0;
private static TransferThread installed;
@BeforeAll
static void boot() {
GlsmSdlHeadlessRig.boot();
final Object backend = BackendManager.RENDER_BACKEND;
if (Reflect.get(backend, "transferThread") == null) {
final ResourceManager rm = Reflect.get(backend, "resourceManager");
installed = new TransferThread(SDLGPUGate.device(), rm);
Reflect.set(backend, "transferThread", installed);
rm.setTransferThread(installed);
}
}
@AfterAll
static void removeTransferThread() throws InterruptedException {
if (installed == null) return;
GlsmSdlHeadlessRig.endFrame();
installed.shutdown();
installed.getThread().join();
final Object backend = BackendManager.RENDER_BACKEND;
Reflect.set(backend, "transferThread", null);
final ResourceManager rm = Reflect.get(backend, "resourceManager");
rm.setTransferThread(null);
installed = null;
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void stagedBandsRoundTripPerLevel(boolean inFrame) {
if (inFrame) GlsmSdlHeadlessRig.beginFrame();
final int tex = GlsmSdlHeadlessRig.createSolidMipTexture(STAGING_SIZE, 1, 0);
for (int level = 0; level <= 1; level++) {
final int size = STAGING_SIZE >> level;
final int half = size / 2;
stageBand(tex, level, 0, size, half);
stageBand(tex, level, half, size, half);
}
for (int level = 0; level <= 1; level++) {
final int size = STAGING_SIZE >> level;
final int[] got = GlsmSdlHeadlessRig.readTextureLevel(tex, level);
assertEquals(size * size, got.length, "level " + level + " size");
int mismatches = 0;
int first = -1;
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
if (got[y * size + x] != stagedColor(level, x, y)) {
if (first < 0) first = y * size + x;
mismatches++;
}
}
}
if (mismatches != 0) {
final int fx = first % size;
final int fy = first / size;
fail("level " + level + ": " + mismatches + " texels differ; first (" + fx + "," + fy + ") expected " + GlsmSdlHeadlessRig.describe(stagedColor(level, fx, fy)) + " got " + GlsmSdlHeadlessRig.describe(got[first]));
}
}
GLStateManager.glDeleteTextures(tex);
}
@ParameterizedTest
@CsvSource({"false,false", "true,true", "true,false"})
void directUploadLandsAfterEarlierBatchedUpload(boolean inFrame, boolean fboAttached) {
if (inFrame) GlsmSdlHeadlessRig.beginFrame();
final int tex = GlsmSdlHeadlessRig.newNearestTexture();
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, BIG_TEX, BIG_TEX, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null);
int fbo = 0;
if (fboAttached) {
fbo = GLStateManager.glGenFramebuffers();
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo);
GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, tex, 0);
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex);
}
final ByteBuffer small = solidRgba(SMALL, SMALL, COLOR_A);
final ByteBuffer big = solidRgba(BIG_W, BIG_H, COLOR_B);
final ByteBuffer out = MemoryUtil.memAlloc(BIG_TEX * BIG_TEX * 4);
try {
assertTrue(big.remaining() > ResourceManager.BATCH_SEGMENT_CAPACITY, "second upload must take the direct path");
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, SMALL, SMALL, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, small);
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, BIG_W, BIG_H, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, big);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex);
GLStateManager.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, out);
final int[] got = GlsmSdlHeadlessRig.toArgb(out, BIG_TEX * BIG_TEX);
int mismatches = 0;
for (int y = 0; y < BIG_H; y++) {
for (int x = 0; x < BIG_W; x++) {
if (got[y * BIG_TEX + x] != COLOR_B) mismatches++;
}
}
final int corner = got[0];
assertEquals(0, mismatches, () -> "texels not holding the later upload; (0,0) is " + GlsmSdlHeadlessRig.describe(corner));
} finally {
MemoryUtil.memFree(small);
MemoryUtil.memFree(big);
MemoryUtil.memFree(out);
}
if (fbo != 0) GLStateManager.glDeleteFramebuffers(fbo);
GLStateManager.glDeleteTextures(tex);
}
private static void stageBand(int tex, int level, int y0, int w, int h) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex);
final TextureStaging staging = GLStateManager.beginTextureStaging(level, 0, y0, w, h);
assertNotNull(staging, "beginTextureStaging level " + level + " y " + y0);
final ByteBuffer buf = staging.buffer();
assertEquals(w * h * 4, buf.remaining(), "staging capacity");
final boolean bgra = staging.bgra();
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
final int c = stagedColor(level, x, y0 + y);
final int i = (y * w + x) * 4;
final byte r = (byte) (c >> 16);
final byte b = (byte) c;
buf.put(i, bgra ? b : r);
buf.put(i + 1, (byte) (c >> 8));
buf.put(i + 2, bgra ? r : b);
buf.put(i + 3, (byte) (c >>> 24));
}
}
assertTrue(GLStateManager.commitTextureStaging(staging), "commitTextureStaging level " + level + " y " + y0);
}
private static int stagedColor(int level, int x, int y) {
return 0x80000000 | ((level * 0x55 + y) & 0x7F) << 24 | (x * 3 & 0xFF) << 16 | (y * 5 & 0xFF) << 8 | ((x ^ y) + level * 0x40) & 0xFF;
}
private static ByteBuffer solidRgba(int w, int h, int argb) {
final ByteBuffer buf = MemoryUtil.memAlloc(w * h * 4);
for (int i = 0; i < w * h; i++) {
buf.put(i * 4, (byte) (argb >> 16));
buf.put(i * 4 + 1, (byte) (argb >> 8));
buf.put(i * 4 + 2, (byte) argb);
buf.put(i * 4 + 3, (byte) (argb >>> 24));
}
return buf;
}
}
@@ -6,6 +6,7 @@ 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;
class SDLGPUFboTargetCacheTest {
@@ -140,6 +141,30 @@ class SDLGPUFboTargetCacheTest {
assertTrue(fbo.targetsDirty);
}
@Test
void sameTextureAtAnotherLevelChangesStructuralHash() {
final FboState fbo = new FboState();
fbo.colorTextures[0] = 0xAAAAL;
fbo.depthTexture = 0xDDDDL;
fbo.recomputeTargets();
final long level0 = fbo.structuralLayoutHash;
fbo.colorLevels[0] = 1;
fbo.recomputeTargets();
final long colorLevel1 = fbo.structuralLayoutHash;
assertNotEquals(level0, colorLevel1);
fbo.colorLevels[0] = 0;
fbo.depthLevel = 1;
fbo.recomputeTargets();
assertNotEquals(level0, fbo.structuralLayoutHash);
assertNotEquals(colorLevel1, fbo.structuralLayoutHash);
fbo.depthLevel = 0;
fbo.recomputeTargets();
assertEquals(level0, fbo.structuralLayoutHash);
}
@Test
void detachColor_outOfBounds_noOp() {
final FboState fbo = new FboState();
@@ -0,0 +1,161 @@
package com.gtnewhorizons.angelica.client.gui;
import java.util.List;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.EnumChatFormatting;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import com.gtnewhorizon.gtnhlib.config.ConfigurationManager;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.textures.atlas.AtlasPackPolicy;
import net.coderbot.iris.gui.element.IrisGuiSlot;
import net.coderbot.iris.gui.element.widget.IrisButton;
public class AtlasPackScreen extends GuiScreen {
private static final String ENABLED_PREFIX = EnumChatFormatting.GREEN + "[x] " + EnumChatFormatting.WHITE;
private static final String DISABLED_PREFIX = EnumChatFormatting.DARK_GRAY + "[ ] " + EnumChatFormatting.GRAY;
private final GuiScreen parent;
private final String title = I18n.format("options.angelica.atlasPacks.title");
private final String info = I18n.format("options.angelica.atlasPacks.info");
private final String builtinSuffix = " - " + I18n.format("options.angelica.atlasPacks.builtin");
private final List<String> packs;
private boolean dirty;
private PackRowList rowList;
public AtlasPackScreen(GuiScreen parent) {
this.parent = parent;
this.packs = AtlasPackPolicy.knownPackClasses();
}
@Override
public void initGui() {
Keyboard.enableRepeatEvents(true);
this.rowList = new PackRowList();
this.buttonList.add(new IrisButton(this.width / 2 - 75, this.height - 27, 150, 20, I18n.format("gui.done"), button -> this.onClose()));
}
@Override
public void onGuiClosed() {
Keyboard.enableRepeatEvents(false);
}
private void onClose() {
if (this.dirty) {
ConfigurationManager.save(AngelicaConfig.class);
this.dirty = false;
}
this.mc.displayGuiScreen(this.parent);
}
@Override
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
this.drawDefaultBackground();
this.rowList.drawScreen(mouseX, mouseY, partialTicks);
super.drawScreen(mouseX, mouseY, partialTicks);
drawCenteredString(this.fontRendererObj, this.title, this.width / 2, 8, 0xFFFFFF);
drawCenteredString(this.fontRendererObj, this.info, this.width / 2, 20, 0xA0A0A0);
}
@Override
protected void mouseClicked(int mouseX, int mouseY, int mouseButton) {
if (!this.rowList.mouseClicked(mouseX, mouseY, mouseButton)) {
super.mouseClicked(mouseX, mouseY, mouseButton);
}
}
@Override
protected void mouseMovedOrUp(int mouseX, int mouseY, int state) {
if (state == -1 || !this.rowList.mouseReleased(mouseX, mouseY, Mouse.getEventButton())) {
super.mouseMovedOrUp(mouseX, mouseY, state);
}
}
@Override
protected void actionPerformed(GuiButton button) {
if (button.enabled && button instanceof IrisButton irisButton) {
irisButton.onPress();
}
}
@Override
protected void keyTyped(char typedChar, int keyCode) {
if (keyCode == Keyboard.KEY_ESCAPE) {
this.onClose();
return;
}
super.keyTyped(typedChar, keyCode);
}
private class PackRowList extends IrisGuiSlot {
PackRowList() {
super(AtlasPackScreen.this.mc, AtlasPackScreen.this.width, AtlasPackScreen.this.height, 36, AtlasPackScreen.this.height - 44, 18);
this.setRenderBackground(false);
}
@Override
protected int getSize() {
return packs.size();
}
@Override
protected boolean isSelected(int index) {
return false;
}
@Override
protected void drawBackground() {}
@Override
public int getListWidth() {
return Math.min(400, AtlasPackScreen.this.width - 40);
}
@Override
protected boolean elementClicked(int index, boolean doubleClick, int mouseX, int mouseY, int button) {
if (button != 0 || index >= packs.size()) {
return false;
}
final var name = packs.get(index);
AtlasPackPolicy.setTrusted(name, !AtlasPackPolicy.isTrusted(name));
dirty = true;
return true;
}
@Override
protected void drawSlot(int index, int x, int y, int slotHeight, Tessellator tessellator, int mouseX, int mouseY) {
final var name = packs.get(index);
final var trusted = AtlasPackPolicy.isTrusted(name);
int simpleStart = name.lastIndexOf('.') + 1;
final var packageName = simpleStart > 0 ? name.substring(0, simpleStart - 1) : "";
var simpleName = name.substring(simpleStart);
final int innerStart = simpleName.lastIndexOf('$') + 1;
if (innerStart > 0) {
simpleName = simpleName.substring(innerStart);
}
var label = (trusted ? ENABLED_PREFIX : DISABLED_PREFIX) + simpleName + EnumChatFormatting.DARK_GRAY + " (" + packageName + ")";
if (AtlasPackPolicy.isBuiltIn(name)) {
label += builtinSuffix;
}
drawString(fontRendererObj, fontRendererObj.trimStringToWidth(label, this.getListWidth() - 8), x, y + 5, 0xFFFFFF);
}
}
}
@@ -25,6 +25,36 @@ public class AngelicaConfig {
@Config.RequiresWorldRestart
public static int chunkBuilderThreadCount;
@Config.Comment("Decode block and item textures on multiple threads for faster startup. Disable if you notice missing or broken textures.")
@Config.DefaultBoolean(true)
public static boolean enableParallelAtlasDecode;
@Config.Comment("Generate texture mipmaps on multiple threads for faster startup. Disable if you notice broken mipmaps.")
@Config.DefaultBoolean(true)
public static boolean enableParallelAtlasMipmaps;
@Config.Comment("Upload the block and item texture atlas in large batches instead of one texture at a time. Disable if you notice missing or broken textures.")
@Config.DefaultBoolean(true)
public static boolean enableBatchedAtlasUpload;
@Config.Comment("Resource pack classes the player allows to be read on worker threads during texture loading.")
@Config.DefaultStringList({})
public static String[] atlasTrustedPacks;
@Config.Comment("Built-in trusted resource pack classes the player has turned off.")
@Config.DefaultStringList({})
public static String[] atlasDistrustedPacks;
@Config.Comment("Resource pack classes seen during texture loading that are not trusted. Recorded automatically; trust them from Video Settings > Performance.")
@Config.DefaultStringList({})
public static String[] atlasObservedPacks;
@Config.Comment("Worker threads for startup and shader loading work. 0 = automatic.")
@Config.DefaultInt(0)
@Config.RangeInt(min = 0, max = 32)
@Config.RequiresMcRestart
public static int workerThreadCount;
@Config.Comment("Enable NotFine Options")
@Config.DefaultBoolean(false)
public static boolean enableNotFineOptions;
@@ -10,12 +10,9 @@ import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.ShaderTransformPostProcessor;
import com.gtnewhorizons.angelica.glsm.shader.ShaderType;
import org.taumc.glsl.grammar.GLSLParser;
import com.gtnewhorizons.angelica.glsm.hooks.ShaderWorkSubmitter;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
import net.coderbot.iris.Iris;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import net.coderbot.iris.gbuffer_overrides.state.StateTracker;
import net.coderbot.iris.gl.blending.AlphaTestStorage;
import net.coderbot.iris.gl.blending.BlendModeStorage;
@@ -104,12 +101,6 @@ public class IrisGLSMBridge {
public static void register() {
GLSMConfig.expandVertexFormats = Iris.enabled;
IrisSamplers.initRenderer();
GLSMHooks.shaderWorkSubmitter = new ShaderWorkSubmitter() {
@Override
public <T> CompletableFuture<T> submit(Supplier<T> work) {
return Iris.ShaderTransformExecutor.submitTracked(work);
}
};
installPostTransformHook();
GLSMHooks.blendHandler = new DeferredBlendHandler() {
@Override
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.loading.rfb.transformers;
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
import com.gtnewhorizons.angelica.loading.shared.transformers.CeleritasBlockTransform;
import com.gtnewhorizons.angelica.loading.shared.transformers.TileEntityMarkerTransform;
import com.gtnewhorizons.retrofuturabootstrap.api.BytePatternMatcher;
import com.gtnewhorizons.retrofuturabootstrap.api.ClassHeaderMetadata;
import com.gtnewhorizons.retrofuturabootstrap.api.ClassNodeHandle;
import com.gtnewhorizons.retrofuturabootstrap.api.ExtensibleClassLoader;
@@ -19,10 +20,12 @@ public class RFBCeleritasBlockTransformer implements RfbClassTransformer {
private final CeleritasBlockTransform inner;
private final TileEntityMarkerTransform tileEntities;
private final BytePatternMatcher cstPoolMatcher;
public RFBCeleritasBlockTransformer(boolean isObf) {
inner = new CeleritasBlockTransform(isObf);
tileEntities = new TileEntityMarkerTransform(isObf, RetroFuturaBootstrap.API.newestAsmVersion());
cstPoolMatcher = new BytePatternMatcher(inner.getConstantPoolSearchStrings(), BytePatternMatcher.Mode.Equals);
}
@Pattern("[a-z0-9-]+")
@@ -68,7 +71,7 @@ public class RFBCeleritasBlockTransformer implements RfbClassTransformer {
tileEntities.track(thisName, superName);
if (tileEntities.markersFor(thisName, originalBytes) != 0) return true;
}
return inner.shouldTransform(originalBytes);
return metadata.matchesBytes(originalBytes, cstPoolMatcher);
}
@Override
@@ -43,6 +43,7 @@ public final class CeleritasBlockTransform implements Opcodes {
// Block subclass owners we shouldn't redirect because they shadow some fields we want to redirect
private final Set<String> blockSubclassExclusions = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final ClassConstantPoolParser cstPoolParser;
private final String[] constantPoolSearchStrings;
public CeleritasBlockTransform(boolean isObf) {
final List<Pair<String, String>> mappings = ImmutableList.of(
@@ -58,7 +59,12 @@ public final class CeleritasBlockTransform implements Opcodes {
this.fieldNameToRedirect.put(name, pair.getLeft());
}
this.cstPoolParser = new ClassConstantPoolParser(this.fieldNameToRedirect.keySet().toArray(new String[0]));
this.constantPoolSearchStrings = this.fieldNameToRedirect.keySet().toArray(new String[0]);
this.cstPoolParser = new ClassConstantPoolParser(this.constantPoolSearchStrings);
}
public String[] getConstantPoolSearchStrings() {
return constantPoolSearchStrings.clone();
}
private boolean isVanillaBlockSubclass(String className) {
@@ -74,7 +80,7 @@ public final class CeleritasBlockTransform implements Opcodes {
}
private boolean isBlockSubclass(String className) {
return isVanillaBlockSubclass(className) || moddedBlockSubclasses.contains(className);
return className != null && (isVanillaBlockSubclass(className) || moddedBlockSubclasses.contains(className));
}
// This method needs to be called for every class, including the ones we don't want to transform.
@@ -214,6 +214,14 @@ public enum Mixins implements IMixins {
)
),
ANGELICA_PARALLEL_ATLAS_LOADING(new MixinBuilder("Decode atlas sprites and generate their mipmaps on worker threads")
.setPhase(Phase.EARLY)
.addClientMixins(
"angelica.textures.MixinTextureMap_ParallelLoad"
)
),
THAUMCRAFT_SCANNER_SCREEN(new MixinBuilder("Render the held Thaumometer screen after world transparency")
.setPhase(Phase.LATE)
.addRequiredMod(TargetedMod.THAUMCRAFT)
@@ -17,4 +17,8 @@ public interface SpriteExtension {
int angelica$getGutterWidth();
void angelica$setGutterWidth(int gutter);
void angelica$setUploaded(boolean uploaded);
boolean angelica$takeUploaded();
}
@@ -0,0 +1,284 @@
package com.gtnewhorizons.angelica.textures.atlas;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.texture.TextureStaging;
import com.gtnewhorizons.angelica.glsm.threading.AngelicaWorkers;
import com.gtnewhorizons.angelica.rendering.celeritas.SpriteExtension;
import com.gtnewhorizons.angelica.utils.SpritePadding;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
public final class AtlasAssembler {
private static final Logger LOGGER = LogManager.getLogger("Angelica");
private static final long BAND_BYTES = 4L << 20;
private AtlasAssembler() {}
private record Placement(TextureAtlasSprite sprite, int[][] frame, int width, int height, int gutter, int x, int y, int paddedWidth, int paddedHeight) {}
private static final class Band {
final int y0;
final int y1;
final int width;
final TextureStaging[] stagings;
CompletableFuture<Void> work;
Band(int y0, int y1, int width, int levels) {
this.y0 = y0;
this.y1 = y1;
this.width = width;
this.stagings = new TextureStaging[levels];
}
}
public static void upload(List<TextureAtlasSprite> sprites, int mipmapLevels, int atlasWidth, int atlasHeight) {
for (TextureAtlasSprite sprite : sprites) {
((SpriteExtension) sprite).angelica$setUploaded(false);
}
if (!AngelicaConfig.enableBatchedAtlasUpload || sprites.isEmpty() || atlasWidth <= 0 || atlasHeight <= 0) return;
if (Minecraft.getMinecraft().gameSettings.anaglyph) return;
final Placement[] placements = collect(sprites, mipmapLevels, atlasWidth, atlasHeight);
if (placements == null || placements.length == 0) return;
final int align = 1 << mipmapLevels;
final int levels = mipmapLevels + 1;
final int bandRows = Math.max(align, (int) Math.min(atlasHeight, BAND_BYTES / (atlasWidth * 4L)) / align * align);
final int bandCount = (atlasHeight + bandRows - 1) / bandRows;
final boolean[] bandOk = new boolean[bandCount];
int maxPaddedHeight = 0;
for (Placement p : placements) maxPaddedHeight = Math.max(maxPaddedHeight, p.paddedHeight);
Band previous = null;
Band current = null;
try {
for (int b = 0; b < bandCount; b++) {
final int y0 = b * bandRows;
current = open(y0, Math.min(atlasHeight, y0 + bandRows), levels, atlasWidth);
if (current != null) submit(current, placements, maxPaddedHeight, align);
if (previous != null) {
final Band done = previous;
previous = null;
bandOk[done.y0 / bandRows] = finish(done);
}
previous = current;
current = null;
if (previous == null) break;
}
if (previous != null) {
final Band done = previous;
previous = null;
bandOk[done.y0 / bandRows] = finish(done);
}
} catch (Throwable t) {
LOGGER.warn("Batched atlas upload failed, remaining sprites use the per-sprite path", t);
discard(previous);
discard(current);
} finally {
GLStateManager.trimTextureStaging();
}
final TextureAtlasSprite last = sprites.getLast();
for (Placement p : placements) {
if (p.sprite != last && covered(bandOk, bandRows, p.y, p.y + p.paddedHeight)) {
((SpriteExtension) p.sprite).angelica$setUploaded(true);
}
}
}
private static Placement[] collect(List<TextureAtlasSprite> sprites, int mipmapLevels, int atlasWidth, int atlasHeight) {
final int alignMask = (1 << mipmapLevels) - 1;
final Set<TextureAtlasSprite> seen = Collections.newSetFromMap(new IdentityHashMap<>(sprites.size() * 2));
final ArrayList<Placement> all = new ArrayList<>(sprites.size());
final ArrayList<Placement> eligible = new ArrayList<>(sprites.size());
boolean anyNonSquare = false;
int maxPaddedHeight = 0;
for (TextureAtlasSprite sprite : sprites) {
if (!seen.add(sprite)) continue;
final int w = sprite.getIconWidth();
final int h = sprite.getIconHeight();
final int g = Math.max(0, ((SpriteExtension) sprite).angelica$getGutterWidth());
final int x = sprite.getOriginX() - g;
final int y = sprite.getOriginY() - g;
final int pw = w + 2 * g;
final int ph = h + 2 * g;
if (w <= 0 || h <= 0 || x < 0 || y < 0 || (x & alignMask) != 0 || (y & alignMask) != 0 || (long) x + pw > atlasWidth || (long) y + ph > atlasHeight) {
return null;
}
final Placement p = new Placement(sprite, eligibleFrame(sprite, w, h, g, mipmapLevels), w, h, g, x, y, pw, ph);
all.add(p);
if (p.frame != null) eligible.add(p);
anyNonSquare |= w != h;
maxPaddedHeight = Math.max(maxPaddedHeight, ph);
}
if (anyNonSquare && overlaps(all, maxPaddedHeight)) return null;
final Placement[] result = eligible.toArray(Placement[]::new);
Arrays.sort(result, Comparator.comparingInt(Placement::y));
return result;
}
private static int[][] eligibleFrame(TextureAtlasSprite sprite, int w, int h, int g, int mipmapLevels) {
if (sprite.getFrameCount() <= 0) return null;
final int[][] frame = sprite.getFrameTextureData(0);
if (frame == null || frame.length != mipmapLevels + 1) return null;
for (int l = 0; l <= mipmapLevels; l++) {
final int lw = w >> l;
final int lh = h >> l;
final int lg = SpritePadding.gutterForLevel(g, l);
if (lw <= 0 || lh <= 0 || frame[l] == null || frame[l].length < lw * lh) return null;
if (((w + 2 * g) >> l) != lw + 2 * lg || ((h + 2 * g) >> l) != lh + 2 * lg) return null;
}
return frame;
}
private static boolean overlaps(ArrayList<Placement> all, int maxPaddedHeight) {
final Placement[] sorted = all.toArray(Placement[]::new);
Arrays.sort(sorted, Comparator.comparingInt(Placement::y));
for (final Placement s : sorted) {
if (s.width == s.height) continue;
for (int j = lowerBound(sorted, s.y - maxPaddedHeight + 1); j < sorted.length && sorted[j].y < s.y + s.paddedHeight; j++) {
final Placement o = sorted[j];
if (o != s && o.y + o.paddedHeight > s.y && o.x < s.x + s.paddedWidth && o.x + o.paddedWidth > s.x) {
return true;
}
}
}
return false;
}
private static Band open(int y0, int y1, int levels, int atlasWidth) {
final Band band = new Band(y0, y1, atlasWidth, levels);
try {
for (int l = 0; l < levels; l++) {
final int ly0 = y0 >> l;
final int rows = (y1 >> l) - ly0;
final int lw = atlasWidth >> l;
if (rows <= 0 || lw <= 0) continue;
final TextureStaging staging = GLStateManager.beginTextureStaging(l, 0, ly0, lw, rows);
if (staging == null) {
abandon(band, 0);
return null;
}
band.stagings[l] = staging;
}
} catch (Throwable t) {
abandon(band, 0);
throw t;
}
return band;
}
private static void submit(Band band, Placement[] placements, int maxPaddedHeight, int align) {
final int threads = AngelicaWorkers.threads();
final int perStripe = (band.y1 - band.y0 + threads - 1) / threads;
final int stripeRows = Math.max(align, (perStripe + align - 1) / align * align);
final ArrayList<CompletableFuture<Void>> stripes = new ArrayList<>(threads);
try {
for (int sy0 = band.y0; sy0 < band.y1; sy0 += stripeRows) {
final int stripeY0 = sy0;
final int stripeY1 = Math.min(band.y1, sy0 + stripeRows);
final int lo = lowerBound(placements, stripeY0 - maxPaddedHeight + 1);
final int hi = lowerBound(placements, stripeY1);
stripes.add(AngelicaWorkers.run(() -> fillStripe(band, placements, lo, hi, stripeY0, stripeY1)));
}
} finally {
band.work = CompletableFuture.allOf(stripes.toArray(CompletableFuture[]::new));
}
}
private static void fillStripe(Band band, Placement[] placements, int lo, int hi, int stripeY0, int stripeY1) {
for (int l = 0; l < band.stagings.length; l++) {
final TextureStaging staging = band.stagings[l];
if (staging == null) continue;
final int stride = (band.width >> l) * 4;
final int levelY0 = band.y0 >> l;
final long address = MemoryUtilities.memAddress(staging.buffer());
final int clipY0 = (stripeY0 >> l) - levelY0;
final int clipY1 = (stripeY1 >> l) - levelY0;
if (clipY1 <= clipY0) continue;
MemoryUtilities.memSet(address + (long) clipY0 * stride, 0, (long) (clipY1 - clipY0) * stride);
final boolean swap = !staging.bgra();
for (int i = lo; i < hi; i++) {
final Placement p = placements[i];
if (p.y + p.paddedHeight <= stripeY0) continue;
SpritePadding.writePaddedLevel(p.frame[l], p.width >> l, p.height >> l, SpritePadding.gutterForLevel(p.gutter, l),
address, stride, p.x >> l, (p.y >> l) - levelY0, clipY0, clipY1, swap);
}
}
}
private static boolean finish(Band band) {
try {
band.work.join();
} catch (Throwable t) {
LOGGER.warn("Batched atlas upload failed for rows {}-{}, those sprites use the per-sprite path", band.y0, band.y1, t);
abandon(band, 0);
return false;
}
for (int l = 0; l < band.stagings.length; l++) {
final TextureStaging staging = band.stagings[l];
if (staging == null) continue;
band.stagings[l] = null;
boolean committed = false;
try {
committed = GLStateManager.commitTextureStaging(staging);
} finally {
if (!committed) abandon(band, l + 1);
}
if (!committed) {
LOGGER.warn("Batched atlas upload could not commit level {} for rows {}-{}, those sprites use the per-sprite path", l, band.y0, band.y1);
return false;
}
}
return true;
}
private static void discard(Band band) {
if (band == null) return;
if (band.work != null) band.work.handle((v, e) -> null).join();
abandon(band, 0);
}
private static void abandon(Band band, int fromLevel) {
for (int l = fromLevel; l < band.stagings.length; l++) {
final TextureStaging staging = band.stagings[l];
if (staging == null) continue;
band.stagings[l] = null;
GLStateManager.abandonTextureStaging(staging);
}
}
private static boolean covered(boolean[] bandOk, int bandRows, int y0, int y1) {
for (int b = y0 / bandRows, last = (y1 - 1) / bandRows; b <= last; b++) {
if (!bandOk[b]) return false;
}
return true;
}
private static int lowerBound(Placement[] placements, int y) {
int lo = 0;
int hi = placements.length;
while (lo < hi) {
final int mid = (lo + hi) >>> 1;
if (placements[mid].y < y) lo = mid + 1;
else hi = mid;
}
return lo;
}
}
@@ -0,0 +1,275 @@
package com.gtnewhorizons.angelica.textures.atlas;
import com.gtnewhorizon.gtnhlib.config.ConfigurationManager;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.glsm.threading.AngelicaWorkers;
import com.gtnewhorizons.angelica.utils.PrefetchLane;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.client.resources.IResource;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.client.resources.IResourcePack;
import net.minecraft.client.resources.SimpleReloadableResourceManager;
import net.minecraft.client.resources.data.IMetadataSerializer;
import net.minecraft.util.ResourceLocation;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import java.util.function.Function;
public final class AtlasLoadSession {
private static final Logger LOGGER = LogManager.getLogger("Angelica");
private static final Consumer<TextureAtlasSprite> KEEP = _ -> {};
public record Decoded(ResourceLocation location, IResource resource, BufferedImage image) {}
private final IResourceManager manager;
private final int textureType;
private final boolean decodeEnabled;
private final boolean mipmapsEnabled;
private int eligibleDomains;
private PrefetchLane<TextureAtlasSprite, Decoded> decodes;
private Function<InputStream, BufferedImage> decoder;
private InputStream pendingStream;
private BufferedImage pendingImage;
private int substituted;
private PrefetchLane<TextureAtlasSprite, TextureAtlasSprite> mipmaps;
private int mipmapsOffloaded;
private int mipmapLevels;
private boolean closed;
private AtlasLoadSession(TextureMap map, IResourceManager manager, boolean decodeEnabled, boolean mipmapsEnabled) {
this.manager = manager;
this.textureType = map.getTextureType();
this.decodeEnabled = decodeEnabled;
this.mipmapsEnabled = mipmapsEnabled;
}
public static AtlasLoadSession begin(TextureMap map, IResourceManager manager) {
final boolean decodeEnabled = AngelicaConfig.enableParallelAtlasDecode;
final boolean mipmapsEnabled = AngelicaConfig.enableParallelAtlasMipmaps;
if ((!decodeEnabled && !mipmapsEnabled) || map.skipFirst) {
return null;
}
final AtlasLoadSession session = new AtlasLoadSession(map, manager, decodeEnabled, mipmapsEnabled);
if (!decodeEnabled) {
return session;
}
if (manager.getClass() != SimpleReloadableResourceManager.class || !metadataReady()) {
LOGGER.warn("Parallel atlas decode disabled for atlas {}: resource manager {} or metadata serializer not supported", session.textureType, manager.getClass().getName());
return session;
}
final String[] observed = AngelicaConfig.atlasObservedPacks;
final Map<String, List<IResourcePack>> eligible = AtlasPackPolicy.eligibleDomains((SimpleReloadableResourceManager) manager);
if (AngelicaConfig.atlasObservedPacks != observed) {
ConfigurationManager.save(AngelicaConfig.class);
}
session.eligibleDomains = eligible.size();
if (eligible.isEmpty()) {
return session;
}
final PrefetchLane<TextureAtlasSprite, Decoded> lane = new PrefetchLane<>(AngelicaWorkers::submit, lookahead(), AtlasLoadSession::dispose);
final Map<String, TextureAtlasSprite> sprites = map.mapRegisteredSprites;
for (Map.Entry<String, TextureAtlasSprite> entry : sprites.entrySet()) {
final TextureAtlasSprite sprite = entry.getValue();
lane.add(sprite, session.decodeTask(map, eligible, entry.getKey(), sprite));
}
session.decodes = lane;
return session;
}
private static int lookahead() {
return 32 * AngelicaWorkers.threads();
}
private static boolean metadataReady() {
final IMetadataSerializer serializer = Minecraft.getMinecraft().metadataSerializer_;
return serializer != null && serializer.gson != null;
}
private Callable<Decoded> decodeTask(TextureMap map, Map<String, List<IResourcePack>> eligible, String key,
TextureAtlasSprite sprite) {
if (sprite == null || sprite.getClass() != TextureAtlasSprite.class) {
return null;
}
final ResourceLocation candidate;
final List<IResourcePack> claimChecked;
try {
final ResourceLocation location = new ResourceLocation(key);
claimChecked = eligible.get(location.getResourceDomain());
if (claimChecked == null) {
return null;
}
candidate = map.completeResourceLocation(location, 0);
} catch (RuntimeException _) {
return null;
}
if (!claimChecked.isEmpty() && AtlasPackPolicy.isClaimed(claimChecked, candidate)) {
return null;
}
return () -> decode(candidate);
}
private Decoded decode(ResourceLocation candidate) throws IOException {
IResource resource = null;
BufferedImage image = null;
try {
resource = this.manager.getResource(candidate);
image = this.decoder.apply(resource.getInputStream());
if (image == null) {
IOUtils.closeQuietly(resource.getInputStream());
return null;
}
resource.getMetadata("texture");
resource.getMetadata("animation");
return new Decoded(candidate, resource, image);
} catch (Throwable t) {
closeImage(image);
if (resource != null) {
IOUtils.closeQuietly(resource.getInputStream());
}
throw t;
}
}
public IResource takeResource(IResourceManager manager, TextureAtlasSprite sprite, ResourceLocation location) {
if (this.decodes == null) {
return null;
}
releasePending();
final Decoded decoded = this.decodes.take(sprite);
if (decoded == null) {
return null;
}
if (manager != this.manager || !location.equals(decoded.location())) {
dispose(decoded);
return null;
}
this.pendingStream = decoded.resource().getInputStream();
this.pendingImage = decoded.image();
this.substituted++;
return decoded.resource();
}
public BufferedImage takeImage(InputStream stream) {
if (this.pendingStream == null || stream != this.pendingStream) {
return null;
}
final BufferedImage image = this.pendingImage;
this.pendingStream = null;
this.pendingImage = null;
return image;
}
public boolean wantsDecoder() {
return this.decodes != null && this.decoder == null;
}
public void onMainDecode(Function<InputStream, BufferedImage> decoder) {
if (this.decodes == null || this.decoder != null) {
return;
}
this.decoder = decoder;
this.decodes.start();
}
public void beginMipmaps(Collection<TextureAtlasSprite> sprites, int levels) {
closeDecodes();
if (!this.mipmapsEnabled || this.mipmaps != null) {
return;
}
this.mipmapLevels = levels;
final PrefetchLane<TextureAtlasSprite, TextureAtlasSprite> lane = new PrefetchLane<>(AngelicaWorkers::submit, lookahead(), KEEP);
final Set<TextureAtlasSprite> seen = Collections.newSetFromMap(new IdentityHashMap<>());
for (TextureAtlasSprite sprite : sprites) {
if (sprite != null && MipmapSafety.isSafe(sprite.getClass()) && seen.add(sprite)) {
lane.add(sprite, () -> {
sprite.generateMipmaps(levels);
return sprite;
});
} else {
lane.add(sprite, null);
}
}
this.mipmaps = lane;
lane.start();
}
public boolean joinMipmaps(TextureAtlasSprite sprite, int levels) {
if (this.mipmaps == null) {
return false;
}
final boolean joined = this.mipmaps.take(sprite) != null;
if (joined && levels == this.mipmapLevels) {
this.mipmapsOffloaded++;
return true;
}
if (this.mipmaps.isDesynced()) {
this.mipmaps.awaitQuiescence();
}
return false;
}
public void close() {
if (this.closed) {
return;
}
this.closed = true;
closeDecodes();
if (this.mipmaps != null) {
this.mipmaps.close();
this.mipmaps.awaitQuiescence();
}
final PrefetchLane<TextureAtlasSprite, Decoded> d = this.decodes;
final PrefetchLane<TextureAtlasSprite, TextureAtlasSprite> m = this.mipmaps;
LOGGER.debug("Parallel atlas load {}: {} eligible domains, {} decodes submitted, {} substituted, {} decode failures (first: {}), decode desync {}, {} mipmaps offloaded, {} mipmap failures (first: {}), mipmap desync {}",
this.textureType, this.eligibleDomains,
d == null ? 0 : d.submitted(), this.substituted,
d == null ? 0 : d.failures(), d == null ? null : d.firstFailure(), d != null && d.isDesynced(),
this.mipmapsOffloaded,
m == null ? 0 : m.failures(), m == null ? null : m.firstFailure(), m != null && m.isDesynced());
}
private void closeDecodes() {
if (this.decodes != null) {
this.decodes.close();
this.decodes.awaitQuiescence();
}
releasePending();
}
private void releasePending() {
if (this.pendingImage != null) {
closeImage(this.pendingImage);
}
this.pendingStream = null;
this.pendingImage = null;
}
private static void dispose(Decoded decoded) {
closeImage(decoded.image());
IOUtils.closeQuietly(decoded.resource().getInputStream());
}
private static void closeImage(BufferedImage image) {
if (image instanceof AutoCloseable closeable) {
try {
closeable.close();
} catch (Exception _) {}
}
}
}
@@ -0,0 +1,250 @@
package com.gtnewhorizons.angelica.textures.atlas;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import cpw.mods.fml.client.FMLFileResourcePack;
import cpw.mods.fml.client.FMLFolderResourcePack;
import net.minecraft.client.resources.DefaultResourcePack;
import net.minecraft.client.resources.FallbackResourceManager;
import net.minecraft.client.resources.FileResourcePack;
import net.minecraft.client.resources.FolderResourcePack;
import net.minecraft.client.resources.IResourcePack;
import net.minecraft.client.resources.SimpleReloadableResourceManager;
import net.minecraft.util.ResourceLocation;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
public final class AtlasPackPolicy {
private static final Logger LOGGER = LogManager.getLogger("Angelica");
private static final Set<String> BUILT_IN_TRUSTED = Set.of(
"glowredman.txloader.TXResourcePack",
"com.hfstudio.guidenh.guide.internal.DefaultGuideResourcePackManager$NamedFileResourcePack",
"com.mitchej123.hodgepodge.mixins.hooks.IC2ResourcePack",
"ganymedes01.etfuturum.client.BuiltInResourcePack$BuiltInFileResourcePack",
"ganymedes01.etfuturum.client.BuiltInResourcePack$BuiltInFolderResourcePack",
"ganymedes01.etfuturum.client.GrayscaleWaterResourcePack",
"ganymedes01.etfuturum.client.DynamicSoundsResourcePack");
private static final Set<String> BUILT_IN_CLAIM_CHECKED = Set.of(
"com.rwtema.extrautils.modintegration.TConTextureResourcePackBedrockium",
"com.rwtema.extrautils.modintegration.TConTextureResourcePackMagicWood",
"com.rwtema.extrautils.modintegration.TConTextureResourcePackUnstableIngot",
"fox.spiteful.avaritia.compat.ticon.InfinityIcons",
"fox.spiteful.avaritia.compat.ticon.NeutroniumIcons",
"com.arc.bloodarsenal.common.tinkers.TextureResourcePackBloodInfusedIron");
private static final ResourceLocation PREWARM = new ResourceLocation("angelica", "atlas_prefetch_prewarm");
private static final String PREWARM_ITEM_PATH = "textures/items/angelica_atlas_prefetch_prewarm.png";
private static final Set<String> LOGGED_UNTRUSTED = ConcurrentHashMap.newKeySet();
private enum Kind {
PLAIN, ZIP, AUDITED, CLAIM_CHECKED
}
private static final Map<Class<?>, Kind> BASE_KINDS = buildBaseKinds();
private static Map<Class<?>, Kind> buildBaseKinds() {
final Map<Class<?>, Kind> map = new HashMap<>();
map.put(DefaultResourcePack.class, Kind.PLAIN);
map.put(FolderResourcePack.class, Kind.PLAIN);
map.put(FMLFolderResourcePack.class, Kind.PLAIN);
map.put(FileResourcePack.class, Kind.ZIP);
map.put(FMLFileResourcePack.class, Kind.ZIP);
return map;
}
private static final class Trust {
final Set<String> trusted = new HashSet<>(Arrays.asList(AngelicaConfig.atlasTrustedPacks));
final Set<String> distrusted = new HashSet<>(Arrays.asList(AngelicaConfig.atlasDistrustedPacks));
final Set<String> observed = new HashSet<>(Arrays.asList(AngelicaConfig.atlasObservedPacks));
}
private AtlasPackPolicy() {
}
public static Map<String, List<IResourcePack>> eligibleDomains(SimpleReloadableResourceManager manager) {
final Trust trust = new Trust();
final Map<String, List<IResourcePack>> eligible = new HashMap<>();
final Map<String, List<String>> untrustedDomains = new HashMap<>();
for (Map.Entry<String, FallbackResourceManager> entry : manager.domainResourceManagers.entrySet()) {
final List<IResourcePack> claimChecked = eligiblePacks(entry.getKey(), entry.getValue(), trust, untrustedDomains);
if (claimChecked != null) {
eligible.put(entry.getKey(), claimChecked);
}
}
recordUntrusted(trust, untrustedDomains);
return eligible;
}
private static List<IResourcePack> eligiblePacks(String domainName, FallbackResourceManager domain, Trust trust,
Map<String, List<String>> untrustedDomains) {
if (domain == null || domain.getClass() != FallbackResourceManager.class) {
return null;
}
boolean eligible = true;
List<IResourcePack> claimChecked = List.of();
for (IResourcePack pack : domain.resourcePacks) {
if (pack == null) {
eligible = false;
continue;
}
Kind kind = BASE_KINDS.get(pack.getClass());
if (kind == null) {
final String className = pack.getClass().getName();
if (trust.distrusted.contains(className)) {
eligible = false;
continue;
}
if (BUILT_IN_TRUSTED.contains(className) || trust.trusted.contains(className)) {
kind = Kind.AUDITED;
} else if (BUILT_IN_CLAIM_CHECKED.contains(className)) {
kind = Kind.CLAIM_CHECKED;
} else {
eligible = false;
untrustedDomains.computeIfAbsent(className, _ -> new ArrayList<>()).add(domainName);
continue;
}
}
switch (kind) {
case ZIP -> {
try {
((FileResourcePack) pack).getResourcePackZipFile();
} catch (IOException | RuntimeException _) {
eligible = false;
}
}
case AUDITED -> {
try {
pack.resourceExists(PREWARM);
} catch (RuntimeException _) {
eligible = false;
}
}
case CLAIM_CHECKED -> {
try {
pack.resourceExists(new ResourceLocation(domainName, PREWARM_ITEM_PATH));
if (claimChecked.isEmpty()) {
claimChecked = new ArrayList<>(4);
}
claimChecked.add(pack);
} catch (RuntimeException _) {
eligible = false;
}
}
case PLAIN -> {}
}
}
return eligible ? claimChecked : null;
}
private static void recordUntrusted(Trust trust, Map<String, List<String>> untrustedDomains) {
if (untrustedDomains.isEmpty()) {
return;
}
boolean changed = false;
final TreeSet<String> observed = new TreeSet<>(trust.observed);
for (Map.Entry<String, List<String>> entry : untrustedDomains.entrySet()) {
final String className = entry.getKey();
if (LOGGED_UNTRUSTED.add(className)) {
LOGGER.info("Resource pack {} is not trusted for parallel texture loading; domains {} load serially. Trust it in Video Settings > Performance > Texture loading.", className, entry.getValue());
}
if (observed.add(className)) {
changed = true;
}
}
if (changed) {
AngelicaConfig.atlasObservedPacks = observed.toArray(String[]::new);
}
}
static boolean isClaimed(List<IResourcePack> claimCheckedPacks, ResourceLocation candidate) {
if (claimCheckedPacks.isEmpty()) {
return false;
}
final ResourceLocation mcmeta = new ResourceLocation(candidate.getResourceDomain(), candidate.getResourcePath() + ".mcmeta");
for (IResourcePack pack : claimCheckedPacks) {
try {
if (pack.resourceExists(candidate) || pack.resourceExists(mcmeta)) {
return true;
}
} catch (RuntimeException _) {
return true;
}
}
return false;
}
public static Set<String> builtInTrusted() {
return BUILT_IN_TRUSTED;
}
public static Set<String> builtInClaimChecked() {
return BUILT_IN_CLAIM_CHECKED;
}
public static boolean isBuiltIn(String className) {
return BUILT_IN_TRUSTED.contains(className) || BUILT_IN_CLAIM_CHECKED.contains(className);
}
public static boolean isTrusted(String className) {
if (Arrays.asList(AngelicaConfig.atlasDistrustedPacks).contains(className)) {
return false;
}
if (isBuiltIn(className)) {
return true;
}
return Arrays.asList(AngelicaConfig.atlasTrustedPacks).contains(className);
}
public static void setTrusted(String className, boolean trusted) {
if (isBuiltIn(className)) {
if (trusted) {
AngelicaConfig.atlasDistrustedPacks = withRemoved(AngelicaConfig.atlasDistrustedPacks, className);
} else {
AngelicaConfig.atlasDistrustedPacks = withAdded(AngelicaConfig.atlasDistrustedPacks, className);
}
return;
}
if (trusted) {
AngelicaConfig.atlasTrustedPacks = withAdded(AngelicaConfig.atlasTrustedPacks, className);
AngelicaConfig.atlasObservedPacks = withRemoved(AngelicaConfig.atlasObservedPacks, className);
} else {
AngelicaConfig.atlasTrustedPacks = withRemoved(AngelicaConfig.atlasTrustedPacks, className);
AngelicaConfig.atlasObservedPacks = withAdded(AngelicaConfig.atlasObservedPacks, className);
}
}
public static List<String> knownPackClasses() {
final TreeSet<String> all = new TreeSet<>();
all.addAll(BUILT_IN_TRUSTED);
all.addAll(BUILT_IN_CLAIM_CHECKED);
all.addAll(Arrays.asList(AngelicaConfig.atlasTrustedPacks));
all.addAll(Arrays.asList(AngelicaConfig.atlasDistrustedPacks));
all.addAll(Arrays.asList(AngelicaConfig.atlasObservedPacks));
return new ArrayList<>(all);
}
private static String[] withAdded(String[] array, String value) {
final TreeSet<String> set = new TreeSet<>(Arrays.asList(array));
set.add(value);
return set.toArray(String[]::new);
}
private static String[] withRemoved(String[] array, String value) {
final TreeSet<String> set = new TreeSet<>(Arrays.asList(array));
set.remove(value);
return set.toArray(String[]::new);
}
}
@@ -0,0 +1,61 @@
package com.gtnewhorizons.angelica.textures.atlas;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
final class MipmapSafety {
private static final Method SET_FRAMES = resolveSetFrames();
private static final ClassValue<Boolean> CACHE = new ClassValue<Boolean>() {
@Override
protected Boolean computeValue(Class<?> type) {
if (SET_FRAMES == null) {
return false;
}
for (Class<?> c = type; c != null && c != TextureAtlasSprite.class; c = c.getSuperclass()) {
for (Method m : c.getDeclaredMethods()) {
final String name = m.getName();
final Class<?>[] params = m.getParameterTypes();
if ((name.equals("generateMipmaps") || name.equals("func_147963_d")) && params.length == 1
&& params[0] == int.class) {
return false;
}
if (name.equals(SET_FRAMES.getName()) && params.length == 1 && params[0] == List.class) {
return false;
}
}
}
return true;
}
};
private MipmapSafety() {
}
public static boolean isSafe(Class<?> spriteClass) {
if (spriteClass == TextureAtlasSprite.class) {
return true;
}
return CACHE.get(spriteClass);
}
private static Method resolveSetFrames() {
Method found = null;
for (Method m : TextureAtlasSprite.class.getDeclaredMethods()) {
final Class<?>[] params = m.getParameterTypes();
if (m.getReturnType() == void.class && params.length == 1 && params[0] == List.class
&& !Modifier.isStatic(m.getModifiers())) {
if (found != null) {
return null;
}
found = m;
}
}
return found;
}
}
@@ -0,0 +1,217 @@
package com.gtnewhorizons.angelica.utils;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public final class PrefetchLane<K, V> {
private final Function<Supplier<V>, CompletableFuture<V>> submitter;
private final int window;
private final Consumer<V> sink;
private final ArrayList<Slot<K, V>> slots = new ArrayList<>();
private final ArrayList<CompletableFuture<V>> inflight = new ArrayList<>();
private int pruneAt;
private int cursor;
private int next;
private int ahead;
private boolean started;
private boolean closed;
private boolean desynced;
private int submitted;
private int failures;
private Throwable firstFailure;
public PrefetchLane(Function<Supplier<V>, CompletableFuture<V>> submitter, int window, Consumer<? super V> disposer) {
if (window < 1) {
throw new IllegalArgumentException("window must be positive: " + window);
}
this.submitter = submitter;
this.window = window;
this.pruneAt = window;
this.sink = value -> {
if (value != null) {
disposer.accept(value);
}
};
}
public void add(K key, Callable<? extends V> task) {
if (this.closed) {
throw new IllegalStateException("PrefetchLane is closed");
}
this.slots.add(new Slot<>(key, task));
}
public void start() {
if (this.started || this.closed) {
return;
}
this.started = true;
fill();
}
public V take(K key) {
if (this.closed) {
return null;
}
final int size = this.slots.size();
for (int i = this.cursor; i < size; i++) {
final Slot<K, V> slot = this.slots.get(i);
if (slot.key != key) {
continue;
}
for (int j = this.cursor; j < i; j++) {
release(this.slots.set(j, null));
}
this.slots.set(i, null);
this.cursor = i + 1;
if (slot.future != null) {
this.ahead--;
}
fill();
return join(slot);
}
this.desynced = true;
close();
return null;
}
public void close() {
if (this.closed) {
return;
}
this.closed = true;
final int size = this.slots.size();
for (int i = this.cursor; i < size; i++) {
release(this.slots.get(i));
}
this.slots.clear();
this.cursor = 0;
this.next = 0;
this.ahead = 0;
}
public void awaitQuiescence() {
final int size = this.inflight.size();
for (int i = 0; i < size; i++) {
try {
this.inflight.get(i).join();
} catch (CompletionException | CancellationException _) {
}
}
this.inflight.clear();
this.pruneAt = this.window;
}
public int submitted() {
return this.submitted;
}
public int failures() {
return this.failures;
}
public Throwable firstFailure() {
return this.firstFailure;
}
public boolean isDesynced() {
return this.desynced;
}
private void fill() {
if (!this.started || this.closed) {
return;
}
if (this.next < this.cursor) {
this.next = this.cursor;
}
final int size = this.slots.size();
while (this.ahead < this.window && this.next < size) {
final Slot<K, V> slot = this.slots.get(this.next++);
if (slot.task == null) {
continue;
}
try {
slot.future = this.submitter.apply(slot);
} catch (RuntimeException e) {
fail(e);
continue;
}
pruneInflight();
this.inflight.add(slot.future);
this.submitted++;
this.ahead++;
}
}
private void pruneInflight() {
if (this.inflight.size() < this.pruneAt) {
return;
}
this.inflight.removeIf(CompletableFuture::isDone);
this.pruneAt = Math.max(this.window, (int) Math.min(Integer.MAX_VALUE, 2L * this.inflight.size()));
}
private void release(Slot<K, V> slot) {
slot.discarded = true;
if (slot.future != null) {
this.ahead--;
slot.future.thenAccept(this.sink);
}
}
private V join(Slot<K, V> slot) {
if (slot.future == null) {
return null;
}
try {
return slot.future.join();
} catch (CompletionException e) {
fail(e.getCause() != null ? e.getCause() : e);
} catch (CancellationException e) {
fail(e);
}
return null;
}
private void fail(Throwable t) {
this.failures++;
if (this.firstFailure == null) {
this.firstFailure = t;
}
}
private static final class Slot<K, V> implements Supplier<V> {
final K key;
final Callable<? extends V> task;
CompletableFuture<V> future;
volatile boolean discarded;
Slot(K key, Callable<? extends V> task) {
this.key = key;
this.task = task;
}
@Override
public V get() {
if (this.discarded) {
return null;
}
try {
return this.task.call();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new CompletionException(e);
}
}
}
}
@@ -2,6 +2,10 @@ package com.gtnewhorizons.angelica.utils;
import net.minecraft.client.renderer.texture.TextureUtil;
import java.util.Arrays;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
public final class SpritePadding {
private static int[][] scratch;
@@ -20,6 +24,86 @@ public final class SpritePadding {
return 1 << mipmapLevels;
}
public static int gutterForLevel(int gutter, int level) {
return gutter >> level;
}
public static void writePaddedLevel(int[] src, int levelW, int levelH, int gutter, long dstAddress,
int dstStrideBytes, int dstX, int dstY, int clipY0, int clipY1, boolean swapRB) {
writeLevel(src, levelW, levelH, gutter, levelH, null, 0, 0, dstAddress, dstStrideBytes, dstX, dstY, clipY0,
clipY1, swapRB);
}
private static void writeLevel(int[] src, int levelW, int levelH, int gutter, int availableRows,
int[] dstArray, int dstArrayStride, int dstArrayOffset, long dstAddress, int dstStrideBytes, int dstX,
int dstY, int clipY0, int clipY1, boolean swapRB) {
final int paddedH = levelH + 2 * gutter;
final int rowStart = Math.max(0, clipY0 - dstY);
final int rowEnd = Math.min(paddedH, clipY1 - dstY);
final int lastAvailableRow = availableRows - 1;
for (int py = rowStart; py < rowEnd; py++) {
int cy = clampCoord(py - gutter, levelH);
if (cy > lastAvailableRow) {
cy = lastAvailableRow;
}
if (cy < 0) {
continue;
}
final int rowBase = cy * levelW;
final int firstValue = src[rowBase];
final int lastValue = src[rowBase + levelW - 1];
if (dstArray != null) {
final int rowOffset = dstArrayOffset + (dstY + py) * dstArrayStride + dstX;
Arrays.fill(dstArray, rowOffset, rowOffset + gutter, firstValue);
System.arraycopy(src, rowBase, dstArray, rowOffset + gutter, levelW);
Arrays.fill(dstArray, rowOffset + gutter + levelW, rowOffset + gutter + levelW + gutter, lastValue);
} else {
final long rowAddress = dstAddress + (long) (dstY + py) * dstStrideBytes + (long) dstX * 4;
final long interiorAddress = rowAddress + (long) gutter * 4;
final long rightAddress = interiorAddress + (long) levelW * 4;
if (swapRB) {
final int firstSwapped = swapRedBlue(firstValue);
final int lastSwapped = swapRedBlue(lastValue);
for (int i = 0; i < gutter; i++) {
memPutInt(rowAddress + (long) i * 4, firstSwapped);
}
for (int i = 0; i < levelW; i++) {
memPutInt(interiorAddress + (long) i * 4, swapRedBlue(src[rowBase + i]));
}
for (int i = 0; i < gutter; i++) {
memPutInt(rightAddress + (long) i * 4, lastSwapped);
}
} else {
for (int i = 0; i < gutter; i++) {
memPutInt(rowAddress + (long) i * 4, firstValue);
}
for (int i = 0; i < levelW; i++) {
memPutInt(interiorAddress + (long) i * 4, src[rowBase + i]);
}
for (int i = 0; i < gutter; i++) {
memPutInt(rightAddress + (long) i * 4, lastValue);
}
}
}
}
}
private static int swapRedBlue(int argb) {
return (argb & 0xFF00FF00) | ((argb & 0x00FF0000) >>> 16) | ((argb & 0x000000FF) << 16);
}
private static int clampCoord(int coord, int limit) {
if (coord < 0) {
return 0;
}
if (coord >= limit) {
return limit - 1;
}
return coord;
}
public static void uploadPadded(int[][] frameData, int width, int height, int originX, int originY, int gutter,
boolean blur, boolean clamp) {
if (gutter <= 0) {
@@ -45,9 +129,10 @@ public final class SpritePadding {
final int levelWidth = width >> level;
final int levelHeight = height >> level;
final int levelGutter = gutter >> level;
final int levelGutter = gutterForLevel(gutter, level);
final int stride = levelWidth + 2 * levelGutter;
final int size = stride * (levelHeight + 2 * levelGutter);
final int paddedHeight = levelHeight + 2 * levelGutter;
final int size = stride * paddedHeight;
int[] target = padded[level];
if (target == null || target.length < size) {
@@ -55,44 +140,10 @@ public final class SpritePadding {
padded[level] = target;
}
int remaining = Math.min(levelData.length, levelWidth * levelHeight);
for (int y = 0; y < levelHeight && remaining > 0; y++) {
final int run = Math.min(levelWidth, remaining);
System.arraycopy(levelData, y * levelWidth, target,
(y + levelGutter) * stride + levelGutter, run);
remaining -= run;
}
clampBorderToEdge(target, stride, levelHeight + 2 * levelGutter, levelGutter);
final int availableRows = levelWidth > 0 ? Math.min(levelHeight, levelData.length / levelWidth) : 0;
writeLevel(levelData, levelWidth, levelHeight, levelGutter, availableRows, target, stride, 0, 0L, 0, 0,
0, 0, paddedHeight, false);
}
return padded;
}
private static void clampBorderToEdge(int[] image, int paddedWidth, int paddedHeight, int border) {
if (border <= 0) {
return;
}
final int contentWidth = paddedWidth - 2 * border;
final int contentHeight = paddedHeight - 2 * border;
if (contentWidth <= 0 || contentHeight <= 0) {
return;
}
for (int y = 0; y < contentHeight; y++) {
final int row = border + (y + border) * paddedWidth;
final int left = image[row];
final int right = image[row + contentWidth - 1];
for (int i = 1; i <= border; i++) {
image[row - i] = left;
image[row + contentWidth - 1 + i] = right;
}
}
final int top = border * paddedWidth;
final int bottom = (border + contentHeight - 1) * paddedWidth;
for (int i = 1; i <= border; i++) {
System.arraycopy(image, top, image, top - i * paddedWidth, paddedWidth);
System.arraycopy(image, bottom, image, bottom + i * paddedWidth, paddedWidth);
}
}
}
@@ -3,7 +3,8 @@ package com.prupe.mcpatcher.mal.resource;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -31,8 +32,7 @@ public class ResourceList {
private static final Map<IResourcePack, Integer> resourcePackOrder = new WeakHashMap<>();
private final IResourcePack resourcePack;
private final Set<ResourceLocationWithSource> allResources = new TreeSet<>(
new ResourceLocationWithSource.Comparator1());
private final Map<ResourceLocationWithSource, ResourceLocationWithSource> resources = new HashMap<>();
public static ResourceList getInstance() {
if (instance == null) {
@@ -43,7 +43,7 @@ public class ResourceList {
resourcePackOrder.put(resourcePack, order);
order--;
}
instance = new ResourceList();
instance = new ResourceList(resourcePacks);
}
return instance;
}
@@ -57,9 +57,9 @@ public class ResourceList {
return i == null ? Integer.MAX_VALUE : i;
}
private ResourceList() {
private ResourceList(List<IResourcePack> resourcePacks) {
this.resourcePack = null;
for (IResourcePack resourcePack : TexturePackAPI.getResourcePacks(null)) {
for (IResourcePack resourcePack : resourcePacks) {
ResourceList sublist;
if (resourcePack instanceof FileResourcePack) {
sublist = new ResourceList((FileResourcePack) resourcePack);
@@ -70,12 +70,15 @@ public class ResourceList {
} else {
continue;
}
allResources.removeAll(sublist.allResources);
allResources.addAll(sublist.allResources);
for (ResourceLocationWithSource resource : sublist.resources.values()) {
resources.put(resource, resource);
}
}
logger.fine("new %s", this);
if (logger.isLoggable(Level.FINEST)) {
for (ResourceLocationWithSource resource : allResources) {
List<ResourceLocationWithSource> sorted = new ArrayList<>(resources.values());
sorted.sort(new ResourceLocationWithSource.Comparator1());
for (ResourceLocationWithSource resource : sorted) {
logger.finest(
"%s -> %s",
resource,
@@ -120,7 +123,7 @@ public class ResourceList {
addResource(resource, file.isFile(), file.isDirectory());
}
}
if (!allResources.isEmpty()) {
if (!resources.isEmpty()) {
logger.fine("new %s", this);
}
}
@@ -147,7 +150,9 @@ public class ResourceList {
if (zipFile == null) {
return;
}
for (ZipEntry entry : Collections.list(zipFile.entries())) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String path = entry.getName();
ResourceLocation resource = TexturePackAPI.parsePath(path);
if (resource != null) {
@@ -173,16 +178,20 @@ public class ResourceList {
private void addResource(ResourceLocation resource, boolean isFile, boolean isDirectory) {
if (isFile) {
allResources.add(new ResourceLocationWithSource(resourcePack, resource));
put(new ResourceLocationWithSource(resourcePack, resource));
} else if (isDirectory) {
if (!resource.getResourcePath()
.endsWith("/")) {
resource = new ResourceLocation(resource.getResourceDomain(), resource.getResourcePath() + '/');
}
allResources.add(new ResourceLocationWithSource(resourcePack, resource));
put(new ResourceLocationWithSource(resourcePack, resource));
}
}
private void put(ResourceLocationWithSource resource) {
resources.putIfAbsent(resource, resource);
}
public List<ResourceLocation> listResources(String directory, String suffix, boolean sortByFilename) {
return listResources(directory, suffix, true, false, sortByFilename);
}
@@ -206,7 +215,7 @@ public class ResourceList {
Set<ResourceLocationWithSource> tmpList = new TreeSet<>(
new ResourceLocationWithSource.Comparator1(true, sortByFilename ? suffix : null));
boolean allNamespaces = MCPatcherUtils.isNullOrEmpty(namespace);
for (ResourceLocationWithSource resource : allResources) {
for (ResourceLocationWithSource resource : resources.values()) {
if (directories != resource.isDirectory()) {
continue;
}
@@ -244,7 +253,7 @@ public class ResourceList {
int fileCount = 0;
int directoryCount = 0;
Set<String> namespaces = new HashSet<>();
for (ResourceLocationWithSource resource : allResources) {
for (ResourceLocationWithSource resource : resources.values()) {
if (resource.isDirectory()) {
directoryCount++;
} else {
@@ -14,6 +14,7 @@ import net.minecraft.util.MathHelper;
import com.cardinalstar.cubicchunks.api.compat.CubicChunksVideoSettings;
import com.google.common.collect.ImmutableList;
import com.gtnewhorizons.angelica.client.gui.AtlasPackScreen;
import com.gtnewhorizons.angelica.compat.ModStatus;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.rendering.FpsReducer;
@@ -31,6 +32,7 @@ import me.jellysquid.mods.sodium.client.gui.options.OptionGroup;
import me.jellysquid.mods.sodium.client.gui.options.OptionImpact;
import me.jellysquid.mods.sodium.client.gui.options.OptionImpl;
import me.jellysquid.mods.sodium.client.gui.options.OptionPage;
import me.jellysquid.mods.sodium.client.gui.options.SubScreenOption;
import me.jellysquid.mods.sodium.client.gui.options.control.ControlValueFormatter;
import me.jellysquid.mods.sodium.client.gui.options.control.CyclingControl;
import me.jellysquid.mods.sodium.client.gui.options.control.SliderControl;
@@ -558,6 +560,42 @@ public class SodiumGameOptionPages {
.build());
groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
.setName(I18n.format("options.angelica.parallelAtlasDecode"))
.setTooltip(I18n.format("options.angelica.parallelAtlasDecode.tooltip"))
.setControl(TickBoxControl::new)
.setImpact(OptionImpact.LOW)
.setBinding((opts, value) -> AngelicaConfig.enableParallelAtlasDecode = value, opts -> AngelicaConfig.enableParallelAtlasDecode)
.setFlags(OptionFlag.REQUIRES_ASSET_RELOAD)
.build())
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
.setName(I18n.format("options.angelica.parallelAtlasMipmaps"))
.setTooltip(I18n.format("options.angelica.parallelAtlasMipmaps.tooltip"))
.setControl(TickBoxControl::new)
.setImpact(OptionImpact.LOW)
.setBinding((opts, value) -> AngelicaConfig.enableParallelAtlasMipmaps = value, opts -> AngelicaConfig.enableParallelAtlasMipmaps)
.setFlags(OptionFlag.REQUIRES_ASSET_RELOAD)
.build())
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
.setName(I18n.format("options.angelica.batchedAtlasUpload"))
.setTooltip(I18n.format("options.angelica.batchedAtlasUpload.tooltip"))
.setControl(TickBoxControl::new)
.setImpact(OptionImpact.LOW)
.setBinding((opts, value) -> AngelicaConfig.enableBatchedAtlasUpload = value, opts -> AngelicaConfig.enableBatchedAtlasUpload)
.setFlags(OptionFlag.REQUIRES_ASSET_RELOAD)
.build())
.add(OptionImpl.createBuilder(int.class, angelicaOpts)
.setName(I18n.format("options.angelica.workerThreads"))
.setTooltip(I18n.format("options.angelica.workerThreads.tooltip"))
.setControl(o -> new SliderControl(o, 0, 32, 1, ControlValueFormatter.quantityOrDisabled("sodium.options.threads.value", "sodium.options.default")))
.setImpact(OptionImpact.MEDIUM)
.setBinding((opts, value) -> AngelicaConfig.workerThreadCount = value, opts -> AngelicaConfig.workerThreadCount)
.setFlags(OptionFlag.REQUIRES_GAME_RESTART)
.build())
.add(new SubScreenOption(I18n.format("options.angelica.atlasPacks"), I18n.format("options.angelica.atlasPacks.tooltip"), AtlasPackScreen::new))
.build());
if (Iris.enabled) {
final OptionImpl<AngelicaConfig, Integer> shadowGraphAngleDelta =
OptionImpl.createBuilder(int.class, angelicaOpts)
+14 -157
View File
@@ -8,6 +8,7 @@ import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
import com.gtnewhorizons.angelica.glsm.threading.AngelicaWorkers;
import com.gtnewhorizons.angelica.iris.ImmediateExtendedAttribs;
import com.gtnewhorizons.angelica.iris.IrisGLSMBridge;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
@@ -70,18 +71,9 @@ import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.zip.ZipException;
@@ -148,90 +140,27 @@ public class Iris {
}
}
/**
* Lazy executor for parallelizing shader transformations during shader pack loading.
* Creates threads on demand and shuts down after a period of inactivity.
*/
public static final class ShaderTransformExecutor {
private static final long IDLE_TIMEOUT_SECONDS = 120;
private static final int THREAD_COUNT = Math.max(2, Math.min(12, Runtime.getRuntime().availableProcessors() / 2));
private static final Object lock = new Object();
private static ExecutorService executor;
private static ScheduledExecutorService scheduler;
private static volatile long lastActivityTime;
private static final AtomicInteger inFlight = new AtomicInteger(0);
private static boolean idleCheckScheduled;
private static final ThreadLocal<Boolean> ON_WORKER = ThreadLocal.withInitial(() -> Boolean.FALSE);
static {
AngelicaWorkers.onIdleShutdown(() -> {
TransformPatcher.clearCache();
ShaderTransformer.clearCache();
GlslVulkanPreprocess.clearCache();
if (RenderSystem.isGLES() || BackendManager.RENDER_BACKEND.isSDLGPU()) SpirvCompiler.clearCache();
SDLGPUGate.clearShaderPrewarmCache();
});
}
private ShaderTransformExecutor() {}
public static boolean isOnWorker() {
return ON_WORKER.get();
return AngelicaWorkers.isWorkerThread();
}
private static void noteActivity() {
lastActivityTime = System.nanoTime();
}
public static ExecutorService get() {
synchronized (lock) {
noteActivity();
if (executor != null && !executor.isShutdown()) {
return executor;
}
final AtomicInteger threadCounter = new AtomicInteger();
final ThreadFactory factory = r -> {
final Thread t = new Thread(r, "Shader-Transform-" + threadCounter.getAndIncrement());
t.setDaemon(true);
return t;
};
final ThreadPoolExecutor tpe = new ThreadPoolExecutor(
THREAD_COUNT,
THREAD_COUNT,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
factory);
tpe.prestartAllCoreThreads();
executor = tpe;
logger.debug("Created shader transform executor with " + THREAD_COUNT + " prestarted threads");
if (scheduler == null || scheduler.isShutdown()) {
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "Shader-Transform-Scheduler");
t.setDaemon(true);
return t;
});
}
if (!idleCheckScheduled) {
idleCheckScheduled = true;
scheduleIdleCheck(IDLE_TIMEOUT_SECONDS);
}
return executor;
}
}
private static void scheduleIdleCheck(long delaySeconds) {
try {
scheduler.schedule(ShaderTransformExecutor::checkIdleShutdown, delaySeconds, TimeUnit.SECONDS);
} catch (Exception e) {
// If scheduling fails, reset the flag so a future get() can try again
idleCheckScheduled = false;
logger.warn("Failed to schedule idle check", e);
}
}
/** Idempotent: ensures the pool is created. {@link #get()} prestarts all workers. */
public static void prepare() {
get();
AngelicaWorkers.prestart();
}
/**
* Warm up the threadpool by running a representative shader transformation.
*/
public static void warmup() {
final String vertexShader = "#version 120\nvoid main() { gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; }";
final String fragmentShader = "#version 120\nvoid main() { gl_FragColor = vec4(1.0); }";
@@ -246,83 +175,11 @@ public class Iris {
}
public static <T> CompletableFuture<T> submitTracked(Supplier<T> supplier) {
Objects.requireNonNull(supplier);
noteActivity();
inFlight.incrementAndGet();
try {
return CompletableFuture.supplyAsync(() -> {
ON_WORKER.set(Boolean.TRUE);
try {
return supplier.get();
} finally {
ON_WORKER.set(Boolean.FALSE);
noteActivity();
inFlight.decrementAndGet();
}
}, get());
} catch (Exception e) {
inFlight.decrementAndGet();
throw e;
}
return AngelicaWorkers.submit(supplier);
}
public static CompletableFuture<Void> submitTracked(Runnable runnable) {
Objects.requireNonNull(runnable);
noteActivity();
inFlight.incrementAndGet();
try {
return CompletableFuture.runAsync(() -> {
ON_WORKER.set(Boolean.TRUE);
try {
runnable.run();
} finally {
ON_WORKER.set(Boolean.FALSE);
noteActivity();
inFlight.decrementAndGet();
}
}, get());
} catch (Exception e) {
inFlight.decrementAndGet();
throw e;
}
}
private static void checkIdleShutdown() {
synchronized (lock) {
final ExecutorService current = executor;
if (current == null || current.isShutdown()) {
// Executor already gone, shut down scheduler
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
scheduler = null;
}
idleCheckScheduled = false;
return;
}
final long idleNanos = System.nanoTime() - lastActivityTime;
final long idleSeconds = TimeUnit.NANOSECONDS.toSeconds(idleNanos);
if (idleSeconds >= IDLE_TIMEOUT_SECONDS && inFlight.get() == 0) {
logger.debug("Shutting down idle shader transform executor after " + idleSeconds + " seconds");
current.shutdown();
executor = null;
scheduler.shutdown();
scheduler = null;
idleCheckScheduled = false;
// Clear transformation caches - no longer needed after loading
TransformPatcher.clearCache();
ShaderTransformer.clearCache();
GlslVulkanPreprocess.clearCache();
if (RenderSystem.isGLES() || BackendManager.RENDER_BACKEND.isSDLGPU()) SpirvCompiler.clearCache();
SDLGPUGate.clearShaderPrewarmCache();
} else {
// Still active (or in-flight), schedule another check.
final long remainingSeconds = Math.max(1, IDLE_TIMEOUT_SECONDS - idleSeconds + 1);
scheduleIdleCheck(remainingSeconds);
}
}
return AngelicaWorkers.run(runnable);
}
}
@@ -57,6 +57,7 @@ public net.minecraft.client.renderer.texture.TextureMap field_147636_j # mipmapL
public net.minecraft.client.renderer.texture.TextureMap field_147637_k # anisotropicFiltering
public net.minecraft.client.renderer.texture.TextureMap func_147634_a(Lnet/minecraft/util/ResourceLocation;I)Lnet/minecraft/util/ResourceLocation; # ResourceLocation completeResourceLocation(ResourceLocation, int)
public net.minecraft.client.renderer.texture.TextureMap field_110574_e # mapRegisteredSprites
public net.minecraft.client.renderer.texture.TextureMap skipFirst # skipFirst
public net.minecraft.client.gui.GuiVideoSettings field_146498_f # parentGuiScreen
public net.minecraft.entity.Entity field_70151_c # fire
@@ -83,6 +84,8 @@ public net.minecraft.world.chunk.NibbleArray field_76583_b # depthBits
public net.minecraft.world.chunk.NibbleArray field_76584_c # depthBitsPlusFour
public net.minecraft.client.Minecraft field_71428_T # timer
public net.minecraft.client.Minecraft field_110452_an # metadataSerializer_
public net.minecraft.client.resources.data.IMetadataSerializer field_110507_c # gson
public net.minecraft.client.renderer.texture.SimpleTexture field_110568_b # textureLocation
@@ -289,6 +289,19 @@ options.angelica.itemdisplaylistcount=Item Renderer Cache Limit
options.angelica.itemdisplaylistcount.tooltip=The upper limit of item meshes to cache in the optimized item renderer, covering VBOs and batched dropped and held item templates. Higher values will significantly increase performance, but may result in increased memory and VRAM usage.
options.angelica.threadedChunkBuilding=Threaded Chunk Building
options.angelica.threadedChunkBuilding.tooltip=If enabled, chunk meshing will be performed on multiple threads in parallel, significantly improving chunk build speed. Disable if you experience rendering issues with mods that have custom block renderers. Requires renderer reload to take effect.
options.angelica.parallelAtlasDecode=Parallel Texture Decode
options.angelica.parallelAtlasDecode.tooltip=Decode block and item textures on several threads. Faster startup and resource reloads. Turn off if textures are missing or broken.
options.angelica.parallelAtlasMipmaps=Parallel Mipmaps
options.angelica.parallelAtlasMipmaps.tooltip=Generate texture atlas mipmaps on several threads. Faster startup and resource reloads. Turn off if textures are missing or broken.
options.angelica.batchedAtlasUpload=Batched Atlas Upload
options.angelica.batchedAtlasUpload.tooltip=Upload the texture atlas in large batches instead of one texture at a time.
options.angelica.workerThreads=Worker Threads
options.angelica.workerThreads.tooltip=Threads used for startup texture loading and shader pack loading. Default picks a count from your CPU.
options.angelica.atlasPacks=Texture Loading Packs...
options.angelica.atlasPacks.tooltip=Choose which resource packs may be read on several threads while textures load.
options.angelica.atlasPacks.title=Texture Loading Resource Packs
options.angelica.atlasPacks.info=Checked packs load textures in parallel. Changes apply on the next resource reload (F3+T).
options.angelica.atlasPacks.builtin=built-in
options.angelica.gpuCullingMode=GPU Culling Mode
options.angelica.gpuCullingMode.tooltip=Selects the chunk culling pipeline. CPU Only uses the CPU emitter. Compute runs frustum culling on the GPU. Requires GL 4.3 / ARB_compute_shader + SSBO + Indirect Multi-Draw.
options.angelica.gpuCullingMode.cpu_only=CPU Only
@@ -28,6 +28,7 @@ public class MixinInitGLStateManager {
@Inject(method = "initializeTextures", at = @At("HEAD"))
private static void angelica$installSDLDrawable(CallbackInfo ci) {
GLSMConfig.workerThreadCount = AngelicaConfig.workerThreadCount;
SDLGPUGate.ensureDrawableInstalled();
}
@@ -0,0 +1,110 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.textures;
import com.gtnewhorizons.angelica.textures.atlas.AtlasLoadSession;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.client.resources.IResource;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.util.ResourceLocation;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.awt.image.BufferedImage;
import java.io.InputStream;
@Mixin(TextureMap.class)
public abstract class MixinTextureMap_ParallelLoad {
@Unique
private AtlasLoadSession angelica$atlasSession;
@Inject(
method = "loadTextureAtlas",
at = @At(value = "INVOKE", target = "Ljava/util/Map;entrySet()Ljava/util/Set;", remap = false))
private void angelica$beginAtlasSession(IResourceManager manager, CallbackInfo ci) {
if (this.angelica$atlasSession != null) {
this.angelica$atlasSession.close();
this.angelica$atlasSession = null;
}
this.angelica$atlasSession = AtlasLoadSession.begin((TextureMap) (Object) this, manager);
}
@WrapOperation(
method = "loadTextureAtlas",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/resources/IResourceManager;getResource(Lnet/minecraft/util/ResourceLocation;)Lnet/minecraft/client/resources/IResource;",
ordinal = 0))
private IResource angelica$takePrefetchedResource(IResourceManager manager, ResourceLocation location, Operation<IResource> original, @Local(ordinal = 0) TextureAtlasSprite sprite) {
final AtlasLoadSession session = this.angelica$atlasSession;
if (session != null) {
final IResource resource = session.takeResource(manager, sprite, location);
if (resource != null) {
return resource;
}
}
return original.call(manager, location);
}
@WrapOperation(
method = "loadTextureAtlas",
at = @At(
value = "INVOKE",
target = "Ljavax/imageio/ImageIO;read(Ljava/io/InputStream;)Ljava/awt/image/BufferedImage;",
ordinal = 0,
remap = false))
private BufferedImage angelica$takePrefetchedImage(InputStream stream, Operation<BufferedImage> original) {
final AtlasLoadSession session = this.angelica$atlasSession;
if (session == null) {
return original.call(stream);
}
final BufferedImage prefetched = session.takeImage(stream);
if (prefetched != null) {
return prefetched;
}
final BufferedImage image = original.call(stream);
if (session.wantsDecoder()) {
session.onMainDecode(s -> original.call(s));
}
return image;
}
@Inject(
method = "loadTextureAtlas",
at = @At(value = "INVOKE", target = "Ljava/util/Map;values()Ljava/util/Collection;", ordinal = 0, remap = false))
private void angelica$beginMipmaps(IResourceManager manager, CallbackInfo ci) {
final AtlasLoadSession session = this.angelica$atlasSession;
if (session != null) {
final TextureMap map = (TextureMap) (Object) this;
session.beginMipmaps(map.mapRegisteredSprites.values(), map.mipmapLevels);
}
}
@WrapOperation(
method = "loadTextureAtlas",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;generateMipmaps(I)V",
ordinal = 0))
private void angelica$joinMipmaps(TextureAtlasSprite sprite, int levels, Operation<Void> original) {
final AtlasLoadSession session = this.angelica$atlasSession;
if (session == null || !session.joinMipmaps(sprite, levels)) {
original.call(sprite, levels);
}
}
@Inject(method = "loadTextureAtlas", at = @At("RETURN"))
private void angelica$closeAtlasSession(CallbackInfo ci) {
final AtlasLoadSession session = this.angelica$atlasSession;
if (session != null) {
this.angelica$atlasSession = null;
session.close();
}
}
}
@@ -54,6 +54,8 @@ public abstract class MixinTextureAtlasSprite implements SpriteExtension {
private boolean celeritas$alwaysTranslucent;
@Unique
private int angelica$gutterWidth;
@Unique
private boolean angelica$uploaded;
@Unique
private static boolean celeritas$isAlwaysTranslucent(String name) {
@@ -189,6 +191,18 @@ public abstract class MixinTextureAtlasSprite implements SpriteExtension {
this.angelica$gutterWidth = gutter;
}
@Override
public void angelica$setUploaded(boolean uploaded) {
this.angelica$uploaded = uploaded;
}
@Override
public boolean angelica$takeUploaded() {
final boolean uploaded = this.angelica$uploaded;
this.angelica$uploaded = false;
return uploaded;
}
@Override
public SpriteTransparencyLevel celeritas$getTransparencyLevel() {
return this.celeritas$transparencyLevel;
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.mixins.early.celeritas.features.textures;
import com.gtnewhorizons.angelica.mixins.interfaces.TextureMetadataExtension;
import com.gtnewhorizons.angelica.rendering.celeritas.SpriteExtension;
import com.gtnewhorizons.angelica.rendering.celeritas.TextureMapExtension;
import com.gtnewhorizons.angelica.textures.atlas.AtlasAssembler;
import com.google.common.collect.Lists;
import com.gtnewhorizons.angelica.utils.MipmapStrategies;
import com.gtnewhorizons.angelica.utils.MipmapStrategy;
@@ -32,6 +33,7 @@ import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.List;
import java.util.Map;
/**
@@ -140,8 +142,18 @@ public class MixinTextureMap implements TextureMapExtension {
target = "Lnet/minecraft/client/renderer/texture/TextureUtil;uploadTextureMipmap([[IIIIIZZ)V"))
private void angelica$uploadPaddedSprite(int[][] frameData, int width, int height, int originX, int originY,
boolean blur, boolean clamp, @Local(ordinal = 0) TextureAtlasSprite sprite) {
SpritePadding.uploadPadded(frameData, width, height, originX, originY,
((SpriteExtension) sprite).angelica$getGutterWidth(), blur, clamp);
final SpriteExtension ext = (SpriteExtension) sprite;
if (ext.angelica$takeUploaded()) return;
SpritePadding.uploadPadded(frameData, width, height, originX, originY, ext.angelica$getGutterWidth(), blur, clamp);
}
@WrapOperation(
method = "loadTextureAtlas",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/texture/Stitcher;getStichSlots()Ljava/util/List;"))
private List<TextureAtlasSprite> angelica$assembleAtlas(Stitcher stitcher, Operation<List<TextureAtlasSprite>> original) {
final List<TextureAtlasSprite> sprites = original.call(stitcher);
AtlasAssembler.upload(sprites, this.mipmapLevels, stitcher.getCurrentWidth(), stitcher.getCurrentHeight());
return sprites;
}
/**
@@ -0,0 +1,242 @@
package com.gtnewhorizons.angelica.textures.atlas;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.glsm.texture.TextureStaging;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.nio.ByteBuffer;
import java.util.Random;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
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;
class AtlasAssemblerStripeTest {
private static final byte SENTINEL = (byte) 0xCD;
private static final Class<?> BAND_CLASS = resolveNested("Band");
private static final Class<?> PLACEMENT_CLASS = resolveNested("Placement");
private static final Class<?> PLACEMENT_ARRAY_CLASS = Array.newInstance(PLACEMENT_CLASS, 0).getClass();
private static Class<?> resolveNested(String name) {
try {
return Class.forName("com.gtnewhorizons.angelica.textures.atlas.AtlasAssembler$" + name);
} catch (ClassNotFoundException e) {
throw new AssertionError(e);
}
}
private static Object newBand(int y0, int y1, int width, int levels) {
try {
final Constructor<?> ctor = BAND_CLASS.getDeclaredConstructor(int.class, int.class, int.class, int.class);
ctor.setAccessible(true);
return ctor.newInstance(y0, y1, width, levels);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
private static Object newPlacement(int[][] frame, int width, int height, int gutter, int x, int y,
int paddedWidth, int paddedHeight) {
try {
final Constructor<?> ctor = PLACEMENT_CLASS.getDeclaredConstructor(TextureAtlasSprite.class,
int[][].class, int.class, int.class, int.class, int.class, int.class, int.class, int.class);
ctor.setAccessible(true);
return ctor.newInstance(null, frame, width, height, gutter, x, y, paddedWidth, paddedHeight);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
private static Object placementArray(Object... placements) {
final Object array = Array.newInstance(PLACEMENT_CLASS, placements.length);
for (int i = 0; i < placements.length; i++) {
Array.set(array, i, placements[i]);
}
return array;
}
private static void fillStripe(Object band, Object placements, int lo, int hi, int stripeY0, int stripeY1) {
Reflect.invokeStatic(AtlasAssembler.class, "fillStripe",
new Class[] { BAND_CLASS, PLACEMENT_ARRAY_CLASS, int.class, int.class, int.class, int.class }, band,
placements, lo, hi, stripeY0, stripeY1);
}
private static int lowerBound(Object placements, int y) {
return Reflect.invokeStatic(AtlasAssembler.class, "lowerBound",
new Class[] { PLACEMENT_ARRAY_CLASS, int.class }, placements, y);
}
private static boolean covered(boolean[] bandOk, int bandRows, int y0, int y1) {
return Reflect.invokeStatic(AtlasAssembler.class, "covered",
new Class[] { boolean[].class, int.class, int.class, int.class }, bandOk, bandRows, y0, y1);
}
private static final class FakeStaging implements TextureStaging {
private final ByteBuffer buffer;
private final boolean bgra;
private final int level;
FakeStaging(ByteBuffer buffer, boolean bgra, int level) {
this.buffer = buffer;
this.bgra = bgra;
this.level = level;
}
@Override
public ByteBuffer buffer() {
return buffer;
}
@Override
public boolean bgra() {
return bgra;
}
@Override
public int level() {
return level;
}
}
private static Object buildBand(int y0, int y1, int width, int levels, boolean bgra) {
final Object band = newBand(y0, y1, width, levels);
final TextureStaging[] stagings = Reflect.get(band, "stagings");
for (int l = 0; l < levels; l++) {
final int rows = (y1 >> l) - (y0 >> l);
final int lw = width >> l;
if (rows <= 0 || lw <= 0) {
continue;
}
final ByteBuffer buffer = ByteBuffer.allocateDirect(lw * rows * 4);
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put(i, SENTINEL);
}
stagings[l] = new FakeStaging(buffer, bgra, l);
}
return band;
}
private static byte[] snapshot(Object band, int level) {
final TextureStaging[] stagings = Reflect.get(band, "stagings");
final ByteBuffer buffer = stagings[level].buffer();
final byte[] out = new byte[buffer.capacity()];
for (int i = 0; i < out.length; i++) {
out[i] = buffer.get(i);
}
return out;
}
private static void runStripes(Object band, Object placements, int maxPaddedHeight, int atlasHeight,
int stripeRows) {
for (int sy0 = 0; sy0 < atlasHeight; sy0 += stripeRows) {
final int sy1 = Math.min(atlasHeight, sy0 + stripeRows);
final int lo = lowerBound(placements, sy0 - maxPaddedHeight + 1);
final int hi = lowerBound(placements, sy1);
fillStripe(band, placements, lo, hi, sy0, sy1);
}
}
private static int[][] randomFrame(Random rnd, int width, int height, int levels) {
final int[][] frame = new int[levels][];
for (int l = 0; l < levels; l++) {
final int lw = width >> l;
final int lh = height >> l;
final int[] data = new int[lw * lh];
for (int i = 0; i < data.length; i++) {
data[i] = rnd.nextInt();
}
frame[l] = data;
}
return frame;
}
private static int swapRedBlue(int argb) {
return (argb & 0xFF00FF00) | ((argb & 0x00FF0000) >>> 16) | ((argb & 0x000000FF) << 16);
}
@Test
void oneStripeMatchesSplitStripes() {
final int mipmapLevels = 2;
final int levels = mipmapLevels + 1;
final int align = 1 << mipmapLevels;
final int atlasWidth = 32;
final int atlasHeight = 32;
final int maxPaddedHeight = 16;
final Random rnd = new Random(42);
final int[][] frameA = randomFrame(rnd, 8, 8, levels);
final int[][] frameB = randomFrame(rnd, 8, 8, levels);
final int[][] frameC = randomFrame(rnd, 4, 4, levels);
final Object placements = placementArray(newPlacement(frameA, 8, 8, 0, 0, 0, 8, 8),
newPlacement(frameB, 8, 8, 4, 16, 8, 16, 16), newPlacement(frameC, 4, 4, 0, 0, 28, 4, 4));
for (boolean bgra : new boolean[] { true, false }) {
final Object whole = buildBand(0, atlasHeight, atlasWidth, levels, bgra);
fillStripe(whole, placements, lowerBound(placements, 0 - maxPaddedHeight + 1), lowerBound(placements, atlasHeight), 0, atlasHeight);
final Object twoStripes = buildBand(0, atlasHeight, atlasWidth, levels, bgra);
runStripes(twoStripes, placements, maxPaddedHeight, atlasHeight, 16);
final Object alignStripes = buildBand(0, atlasHeight, atlasWidth, levels, bgra);
runStripes(alignStripes, placements, maxPaddedHeight, atlasHeight, align);
for (int l = 0; l < levels; l++) {
final byte[] expected = snapshot(whole, l);
final int fl = l;
assertArrayEquals(expected, snapshot(twoStripes, l), () -> "level " + fl + " mismatch, stripeRows=16 bgra=" + bgra);
assertArrayEquals(expected, snapshot(alignStripes, l), () -> "level " + fl + " mismatch, stripeRows=" + align + " bgra=" + bgra);
}
final long addrLevel0 = memAddress(((TextureStaging[]) Reflect.get(whole, "stagings"))[0].buffer());
final int strideLevel0 = atlasWidth * 4;
final int pixelA = memGetInt(addrLevel0 + 0L * strideLevel0 + 0L * 4);
assertEquals(bgra ? frameA[0][0] : swapRedBlue(frameA[0][0]), pixelA, "sprite A texel, bgra=" + bgra);
final int pixelC = memGetInt(addrLevel0 + 28L * strideLevel0 + 0L * 4);
assertEquals(bgra ? frameC[0][0] : swapRedBlue(frameC[0][0]), pixelC, "sprite C texel, bgra=" + bgra);
final int pixelB = memGetInt(addrLevel0 + 12L * strideLevel0 + 20L * 4);
assertEquals(bgra ? frameB[0][0] : swapRedBlue(frameB[0][0]), pixelB, "sprite B texel, bgra=" + bgra);
}
}
@Test
void coveredRequiresAllOverlappingBandsOk() {
final boolean[] bandOk = { true, true, false, true };
final int bandRows = 8;
assertTrue(covered(bandOk, bandRows, 0, 8));
assertTrue(covered(bandOk, bandRows, 8, 16));
assertFalse(covered(bandOk, bandRows, 0, 24));
assertFalse(covered(bandOk, bandRows, 15, 20));
assertTrue(covered(bandOk, bandRows, 24, 32));
}
@Test
void lowerBoundFindsInsertionPoint() {
final Object placements = placementArray(newPlacement(null, 0, 0, 0, 0, 0, 0, 0),
newPlacement(null, 0, 0, 0, 0, 4, 0, 0), newPlacement(null, 0, 0, 0, 0, 4, 0, 0),
newPlacement(null, 0, 0, 0, 0, 9, 0, 0));
assertEquals(0, lowerBound(placements, -5));
assertEquals(0, lowerBound(placements, 0));
assertEquals(1, lowerBound(placements, 1));
assertEquals(1, lowerBound(placements, 4));
assertEquals(3, lowerBound(placements, 5));
assertEquals(3, lowerBound(placements, 9));
assertEquals(4, lowerBound(placements, 10));
}
}
@@ -0,0 +1,223 @@
package com.gtnewhorizons.angelica.textures.atlas;
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;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import net.minecraft.client.resources.FallbackResourceManager;
import net.minecraft.client.resources.IResourcePack;
import net.minecraft.client.resources.SimpleReloadableResourceManager;
import net.minecraft.client.resources.data.IMetadataSection;
import net.minecraft.client.resources.data.IMetadataSerializer;
import net.minecraft.util.ResourceLocation;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class AtlasPackPolicyTest {
private String[] savedTrusted;
private String[] savedDistrusted;
private String[] savedObserved;
@BeforeEach
void saveAndResetConfig() {
savedTrusted = AngelicaConfig.atlasTrustedPacks;
savedDistrusted = AngelicaConfig.atlasDistrustedPacks;
savedObserved = AngelicaConfig.atlasObservedPacks;
AngelicaConfig.atlasTrustedPacks = new String[0];
AngelicaConfig.atlasDistrustedPacks = new String[0];
AngelicaConfig.atlasObservedPacks = new String[0];
}
@AfterEach
void restoreConfig() {
AngelicaConfig.atlasTrustedPacks = savedTrusted;
AngelicaConfig.atlasDistrustedPacks = savedDistrusted;
AngelicaConfig.atlasObservedPacks = savedObserved;
}
private static final ResourceLocation PNG = new ResourceLocation("tinker", "textures/items/foo.png");
private static final ResourceLocation MCMETA = new ResourceLocation("tinker", "textures/items/foo.png.mcmeta");
private static final class FakePack implements IResourcePack {
private final ResourceLocation claims;
private final RuntimeException throwOnExists;
FakePack(ResourceLocation claims) {
this.claims = claims;
this.throwOnExists = null;
}
FakePack(RuntimeException throwOnExists) {
this.claims = null;
this.throwOnExists = throwOnExists;
}
@Override
public InputStream getInputStream(ResourceLocation location) throws IOException {
throw new IOException("not used");
}
@Override
public boolean resourceExists(ResourceLocation location) {
if (throwOnExists != null) {
throw throwOnExists;
}
return location.equals(claims);
}
@Override
public Set<String> getResourceDomains() {
return Collections.emptySet();
}
@Override
public IMetadataSection getPackMetadata(IMetadataSerializer serializer, String name) throws IOException {
return null;
}
@Override
public BufferedImage getPackImage() throws IOException {
return null;
}
@Override
public String getPackName() {
return "fake";
}
}
@Test
void noPackClaimsAllowsPrefetch() {
final List<IResourcePack> packs = List.of(new FakePack(new ResourceLocation("tinker", "unrelated.png")));
assertFalse(AtlasPackPolicy.isClaimed(packs, PNG));
}
@Test
void packClaimingPngForcesSerial() {
final List<IResourcePack> packs = List.of(new FakePack(PNG));
assertTrue(AtlasPackPolicy.isClaimed(packs, PNG));
}
@Test
void packClaimingOnlyMcmetaForcesSerial() {
final List<IResourcePack> packs = List.of(new FakePack(MCMETA));
assertTrue(AtlasPackPolicy.isClaimed(packs, PNG));
}
@Test
void resourceExistsThrowingForcesSerial() {
final List<IResourcePack> packs = List.of(new FakePack(new IllegalStateException("boom")));
assertTrue(AtlasPackPolicy.isClaimed(packs, PNG));
}
@Test
void setTrustedBuiltInRoundTrips() {
final String builtIn = AtlasPackPolicy.builtInTrusted().iterator().next();
AtlasPackPolicy.setTrusted(builtIn, false);
assertArrayEquals(new String[] { builtIn }, AngelicaConfig.atlasDistrustedPacks);
assertFalse(AtlasPackPolicy.isTrusted(builtIn));
AtlasPackPolicy.setTrusted(builtIn, true);
assertArrayEquals(new String[0], AngelicaConfig.atlasDistrustedPacks);
assertTrue(AtlasPackPolicy.isTrusted(builtIn));
}
@Test
void setTrustedNonBuiltInMovesBetweenTrustedAndObserved() {
final String name = "com.example.SomePack";
AngelicaConfig.atlasObservedPacks = new String[] { name };
AtlasPackPolicy.setTrusted(name, true);
assertArrayEquals(new String[] { name }, AngelicaConfig.atlasTrustedPacks);
assertArrayEquals(new String[0], AngelicaConfig.atlasObservedPacks);
assertTrue(AtlasPackPolicy.isTrusted(name));
AtlasPackPolicy.setTrusted(name, false);
assertArrayEquals(new String[0], AngelicaConfig.atlasTrustedPacks);
assertArrayEquals(new String[] { name }, AngelicaConfig.atlasObservedPacks);
assertFalse(AtlasPackPolicy.isTrusted(name));
}
@Test
void setTrustedArraysStaySortedAndDeduplicated() {
AtlasPackPolicy.setTrusted("b.Pack", true);
AtlasPackPolicy.setTrusted("a.Pack", true);
AtlasPackPolicy.setTrusted("a.Pack", true);
assertArrayEquals(new String[] { "a.Pack", "b.Pack" }, AngelicaConfig.atlasTrustedPacks);
AtlasPackPolicy.setTrusted("a.Pack", false);
assertArrayEquals(new String[] { "b.Pack" }, AngelicaConfig.atlasTrustedPacks);
assertArrayEquals(new String[] { "a.Pack" }, AngelicaConfig.atlasObservedPacks);
}
@Test
void knownPackClassesIsSortedUnion() {
AngelicaConfig.atlasTrustedPacks = new String[] { "z.Trusted" };
AngelicaConfig.atlasDistrustedPacks = new String[] { "m.Distrusted" };
AngelicaConfig.atlasObservedPacks = new String[] { "a.Observed" };
final List<String> known = AtlasPackPolicy.knownPackClasses();
assertTrue(known.contains("z.Trusted"));
assertTrue(known.contains("m.Distrusted"));
assertTrue(known.contains("a.Observed"));
for (String builtIn : AtlasPackPolicy.builtInTrusted()) {
assertTrue(known.contains(builtIn));
}
for (String builtIn : AtlasPackPolicy.builtInClaimChecked()) {
assertTrue(known.contains(builtIn));
}
final List<String> sorted = new ArrayList<>(known);
Collections.sort(sorted);
assertEquals(sorted, known);
}
@Test
void eligibleDomainsTrustsConfiguredPackClass() {
AngelicaConfig.atlasTrustedPacks = new String[] { FakePack.class.getName() };
final IMetadataSerializer serializer = new IMetadataSerializer();
final SimpleReloadableResourceManager manager = new SimpleReloadableResourceManager(serializer);
final FallbackResourceManager domain = new FallbackResourceManager(serializer);
domain.resourcePacks.add(new FakePack(new ResourceLocation("tinker", "unrelated.png")));
manager.domainResourceManagers.put("domainB", domain);
final Map<String, List<IResourcePack>> result = AtlasPackPolicy.eligibleDomains(manager);
assertTrue(result.containsKey("domainB"));
assertTrue(result.get("domainB").isEmpty());
}
@Test
void eligibleDomainsRecordsUnknownPackOnce() {
final IMetadataSerializer serializer = new IMetadataSerializer();
final SimpleReloadableResourceManager manager = new SimpleReloadableResourceManager(serializer);
final FallbackResourceManager domain = new FallbackResourceManager(serializer);
domain.resourcePacks.add(new FakePack(new ResourceLocation("tinker", "unrelated.png")));
manager.domainResourceManagers.put("domainC", domain);
assertFalse(AtlasPackPolicy.eligibleDomains(manager).containsKey("domainC"));
final String[] recorded = AngelicaConfig.atlasObservedPacks;
assertArrayEquals(new String[] { FakePack.class.getName() }, recorded);
assertFalse(AtlasPackPolicy.eligibleDomains(manager).containsKey("domainC"));
assertTrue(recorded == AngelicaConfig.atlasObservedPacks);
}
}
@@ -0,0 +1,60 @@
package com.gtnewhorizons.angelica.textures.atlas;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class MipmapSafetyTest {
private static final class PlainSprite extends TextureAtlasSprite {
PlainSprite() {
super("plain");
}
}
private static final class MipmapOverrideSprite extends TextureAtlasSprite {
MipmapOverrideSprite() {
super("mipmap");
}
@Override
public void generateMipmaps(int level) {}
}
private static final class SetFramesOverrideSprite extends TextureAtlasSprite {
SetFramesOverrideSprite() {
super("setframes");
}
@Override
public void setFramesTextureData(List<int[][]> data) {}
}
@Test
void baseSpriteIsSafe() {
assertTrue(MipmapSafety.isSafe(TextureAtlasSprite.class));
}
@Test
void plainSubclassIsSafe() {
assertTrue(MipmapSafety.isSafe(PlainSprite.class));
}
@Test
void generateMipmapsOverrideIsUnsafe() {
assertFalse(MipmapSafety.isSafe(MipmapOverrideSprite.class));
}
@Test
void setFramesTextureDataOverrideIsUnsafe() {
assertFalse(MipmapSafety.isSafe(SetFramesOverrideSprite.class));
}
}
@@ -0,0 +1,276 @@
package com.gtnewhorizons.angelica.utils;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
class PrefetchLaneTest {
private static final class ManualSubmitter<V> implements Function<Supplier<V>, CompletableFuture<V>> {
private final ArrayList<Supplier<V>> queuedSuppliers = new ArrayList<>();
private final ArrayList<CompletableFuture<V>> queuedFutures = new ArrayList<>();
private int submitCount;
@Override
public CompletableFuture<V> apply(Supplier<V> supplier) {
submitCount++;
final CompletableFuture<V> future = new CompletableFuture<>();
queuedSuppliers.add(supplier);
queuedFutures.add(future);
return future;
}
int submitCount() {
return submitCount;
}
void runNext() {
run(0);
}
void run(int index) {
final Supplier<V> supplier = queuedSuppliers.remove(index);
final CompletableFuture<V> future = queuedFutures.remove(index);
try {
future.complete(supplier.get());
} catch (Throwable t) {
future.completeExceptionally(t);
}
}
}
@Test
void submissionsNeverRunMoreThanWindowAheadOfCursor() {
final ManualSubmitter<String> submitter = new ManualSubmitter<>();
final List<String> disposed = new ArrayList<>();
final PrefetchLane<Object, String> lane = new PrefetchLane<>(submitter, 2, disposed::add);
final Object[] keys = new Object[5];
for (int i = 0; i < keys.length; i++) {
keys[i] = new Object();
final String value = "v" + i;
lane.add(keys[i], () -> value);
}
lane.start();
assertEquals(2, submitter.submitCount());
assertEquals(2, lane.submitted());
for (int i = 0; i < keys.length; i++) {
submitter.runNext();
assertEquals("v" + i, lane.take(keys[i]));
assertEquals(Math.min(2 + i + 1, keys.length), lane.submitted());
}
}
@Test
void skippedSlotsAreDisposedExactlyOnce() {
final ManualSubmitter<String> submitter = new ManualSubmitter<>();
final List<String> disposed = new ArrayList<>();
final PrefetchLane<Object, String> lane = new PrefetchLane<>(submitter, 4, disposed::add);
final Object keyA = new Object();
final Object keyB = new Object();
final Object keyC = new Object();
final Object keyD = new Object();
lane.add(keyA, () -> "vA");
lane.add(keyB, () -> "vB");
lane.add(keyC, () -> "vC");
lane.add(keyD, () -> "vD");
lane.start();
submitter.runNext();
submitter.runNext();
submitter.runNext();
submitter.runNext();
assertEquals("vD", lane.take(keyD));
Collections.sort(disposed);
assertEquals(List.of("vA", "vB", "vC"), disposed);
}
@Test
void slotDiscardedBeforeItsTaskStartsNeverRunsTheTask() {
final ManualSubmitter<String> submitter = new ManualSubmitter<>();
final List<String> disposed = new ArrayList<>();
final PrefetchLane<Object, String> lane = new PrefetchLane<>(submitter, 2, disposed::add);
final Object keyA = new Object();
final Object keyB = new Object();
final AtomicBoolean keyARan = new AtomicBoolean();
lane.add(keyA, () -> {
keyARan.set(true);
return "vA";
});
lane.add(keyB, () -> "vB");
lane.start();
submitter.run(1);
assertEquals("vB", lane.take(keyB));
assertFalse(keyARan.get());
assertTrue(disposed.isEmpty());
submitter.runNext();
assertFalse(keyARan.get());
assertTrue(disposed.isEmpty());
}
@Test
void discardWhileRunningDisposesResultOnCompletionExactlyOnce() throws Exception {
final ExecutorService pool = Executors.newFixedThreadPool(2);
try {
final CountDownLatch taskStarted = new CountDownLatch(1);
final CountDownLatch releaseTask = new CountDownLatch(1);
final List<String> disposed = Collections.synchronizedList(new ArrayList<>());
final CountDownLatch disposedOnce = new CountDownLatch(1);
final Function<Supplier<String>, CompletableFuture<String>> submitter = supplier -> CompletableFuture
.supplyAsync(supplier, pool);
final PrefetchLane<Object, String> lane = new PrefetchLane<>(submitter, 2, value -> {
disposed.add(value);
disposedOnce.countDown();
});
final Object keyA = new Object();
final Object keyB = new Object();
lane.add(keyA, () -> {
taskStarted.countDown();
assertTrue(releaseTask.await(5, TimeUnit.SECONDS));
return "vA";
});
lane.add(keyB, () -> "vB");
lane.start();
assertTrue(taskStarted.await(5, TimeUnit.SECONDS));
assertEquals("vB", lane.take(keyB));
releaseTask.countDown();
assertTrue(disposedOnce.await(5, TimeUnit.SECONDS));
Thread.sleep(50);
assertEquals(List.of("vA"), disposed);
} finally {
pool.shutdownNow();
}
}
@Test
void placeholdersNeverSubmitAndTakeReturnsNull() {
final ManualSubmitter<String> submitter = new ManualSubmitter<>();
final PrefetchLane<Object, String> lane = new PrefetchLane<>(submitter, 3, v -> {});
final Object keyA = new Object();
final Object keyB = new Object();
final Object keyC = new Object();
lane.add(keyA, () -> "vA");
lane.add(keyB, null);
lane.add(keyC, () -> "vC");
lane.start();
assertEquals(2, submitter.submitCount());
submitter.runNext();
submitter.runNext();
assertEquals("vA", lane.take(keyA));
assertNull(lane.take(keyB));
assertEquals("vC", lane.take(keyC));
}
@Test
void failingTaskReturnsNullFromTakeAndIsCounted() {
final ManualSubmitter<String> submitter = new ManualSubmitter<>();
final PrefetchLane<Object, String> lane = new PrefetchLane<>(submitter, 1, v -> {});
final Object keyA = new Object();
final RuntimeException boom = new RuntimeException("boom");
lane.add(keyA, () -> {
throw boom;
});
lane.start();
submitter.runNext();
assertNull(lane.take(keyA));
assertEquals(1, lane.failures());
assertSame(boom, lane.firstFailure());
}
@Test
void takeForUnknownKeyDesyncsAndDisposesEverythingOutstandingExactlyOnce() {
final ManualSubmitter<String> submitter = new ManualSubmitter<>();
final List<String> disposed = new ArrayList<>();
final PrefetchLane<Object, String> lane = new PrefetchLane<>(submitter, 3, disposed::add);
final Object keyA = new Object();
final Object keyB = new Object();
final Object keyC = new Object();
lane.add(keyA, () -> "vA");
lane.add(keyB, () -> "vB");
lane.add(keyC, () -> "vC");
lane.start();
submitter.runNext();
submitter.runNext();
submitter.runNext();
assertNull(lane.take(new Object()));
assertTrue(lane.isDesynced());
Collections.sort(disposed);
assertEquals(List.of("vA", "vB", "vC"), disposed);
}
@Test
void closeNeverBlocksEvenWithARunningTaskAndDisposesItOnceItFinishes() throws Exception {
final ExecutorService pool = Executors.newFixedThreadPool(2);
try {
final CountDownLatch taskStarted = new CountDownLatch(1);
final CountDownLatch releaseTask = new CountDownLatch(1);
final List<String> disposed = Collections.synchronizedList(new ArrayList<>());
final AtomicInteger disposeCount = new AtomicInteger();
final CountDownLatch disposedOnce = new CountDownLatch(1);
final Function<Supplier<String>, CompletableFuture<String>> submitter = supplier -> CompletableFuture
.supplyAsync(supplier, pool);
final PrefetchLane<Object, String> lane = new PrefetchLane<>(submitter, 2, value -> {
disposed.add(value);
disposeCount.incrementAndGet();
disposedOnce.countDown();
});
final Object keyA = new Object();
lane.add(keyA, () -> {
taskStarted.countDown();
assertTrue(releaseTask.await(5, TimeUnit.SECONDS));
return "vA";
});
lane.start();
assertTrue(taskStarted.await(5, TimeUnit.SECONDS));
assertTimeoutPreemptively(Duration.ofSeconds(2), lane::close);
releaseTask.countDown();
assertTrue(disposedOnce.await(5, TimeUnit.SECONDS));
Thread.sleep(50);
assertEquals(1, disposeCount.get());
assertEquals(List.of("vA"), disposed);
} finally {
pool.shutdownNow();
}
}
}
@@ -0,0 +1,206 @@
package com.gtnewhorizons.angelica.utils;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Random;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SpritePaddingTest {
private static final int[] GUTTERS = { 0, 1, 2, 4 };
private static final int DST_X = 3;
private static final int DST_Y = 2;
private static final int MARGIN = 5;
private static final byte SENTINEL = (byte) 0xCD;
private static int[][] padFrame(int[][] frameData, int width, int height, int gutter) {
try {
final Method method = SpritePadding.class.getDeclaredMethod("padFrame", int[][].class, int.class, int.class, int.class, int[][].class);
method.setAccessible(true);
return (int[][]) method.invoke(null, frameData, width, height, gutter, (Object) null);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
private static void putLittleEndian(byte[] dst, int offset, int value) {
dst[offset] = (byte) value;
dst[offset + 1] = (byte) (value >>> 8);
dst[offset + 2] = (byte) (value >>> 16);
dst[offset + 3] = (byte) (value >>> 24);
}
private static final class Level {
final int[] src;
final int levelW;
final int levelH;
final int gutter;
final int paddedW;
final int paddedH;
final int strideBytes;
final int totalHeight;
Level(Random rnd, int level, int baseGutter, int levelW, int levelH) {
this.levelW = levelW;
this.levelH = levelH;
this.gutter = SpritePadding.gutterForLevel(baseGutter, level);
this.src = new int[levelW * levelH];
for (int i = 0; i < src.length; i++) {
src[i] = rnd.nextInt();
}
this.paddedW = levelW + 2 * gutter;
this.paddedH = levelH + 2 * gutter;
this.strideBytes = (DST_X + paddedW + MARGIN) * 4;
this.totalHeight = DST_Y + paddedH + MARGIN;
}
}
private static final class NativeBuffer {
final ByteBuffer buffer;
final long address;
NativeBuffer(int size) {
this.buffer = ByteBuffer.allocateDirect(size);
for (int i = 0; i < size; i++) {
this.buffer.put(i, SENTINEL);
}
this.address = memAddress(this.buffer, 0);
}
byte[] snapshot() {
final byte[] out = new byte[buffer.capacity()];
for (int i = 0; i < out.length; i++) {
out[i] = buffer.get(i);
}
return out;
}
}
@Test
void clippedHalvesReassembleWhole() {
final Random rnd = new Random(987654321);
for (int level = 0; level <= 4; level++) {
for (int baseGutter : GUTTERS) {
final int fLevel = level;
final int levelW = 1 + rnd.nextInt(64);
final int levelH = 1 + rnd.nextInt(64);
final Level fixture = new Level(rnd, level, baseGutter, levelW, levelH);
final int splitRow = fixture.paddedH <= 1 ? 0 : 1 + rnd.nextInt(fixture.paddedH - 1);
final int size = fixture.strideBytes * fixture.totalHeight;
for (boolean swapRB : new boolean[] { false, true }) {
final NativeBuffer whole = new NativeBuffer(size);
SpritePadding.writePaddedLevel(fixture.src, fixture.levelW, fixture.levelH, fixture.gutter,
whole.address, fixture.strideBytes, DST_X, DST_Y, DST_Y, DST_Y + fixture.paddedH, swapRB);
final NativeBuffer split = new NativeBuffer(size);
SpritePadding.writePaddedLevel(fixture.src, fixture.levelW, fixture.levelH, fixture.gutter, split.address, fixture.strideBytes, DST_X, DST_Y, DST_Y, DST_Y + splitRow, swapRB);
SpritePadding.writePaddedLevel(fixture.src, fixture.levelW, fixture.levelH, fixture.gutter, split.address, fixture.strideBytes, DST_X, DST_Y, DST_Y + splitRow, DST_Y + fixture.paddedH,
swapRB);
assertTrue(Arrays.equals(whole.snapshot(), split.snapshot()), () -> "split reassembly mismatch level=" + fLevel + " gutter=" + baseGutter + " swapRB=" + swapRB + " splitRow=" + splitRow);
}
}
}
}
@Test
void bytesOutsideRectAreUntouched() {
final Random rnd = new Random(555111333);
for (int level = 0; level <= 4; level++) {
for (int baseGutter : GUTTERS) {
final int fLevel = level;
final int levelW = 1 + rnd.nextInt(64);
final int levelH = 1 + rnd.nextInt(64);
final Level fixture = new Level(rnd, level, baseGutter, levelW, levelH);
final NativeBuffer nb = new NativeBuffer(fixture.strideBytes * fixture.totalHeight);
SpritePadding.writePaddedLevel(fixture.src, fixture.levelW, fixture.levelH, fixture.gutter, nb.address, fixture.strideBytes, DST_X, DST_Y, DST_Y, DST_Y + fixture.paddedH, false);
final byte[] actual = nb.snapshot();
for (int row = 0; row < fixture.totalHeight; row++) {
final boolean insideRow = row >= DST_Y && row < DST_Y + fixture.paddedH;
for (int col = 0; col < fixture.strideBytes; col++) {
final boolean insideCol = col >= DST_X * 4 && col < (DST_X + fixture.paddedW) * 4;
if (!(insideRow && insideCol)) {
final int index = row * fixture.strideBytes + col;
final int fRow = row;
final int fCol = col;
assertTrue(actual[index] == SENTINEL, () -> "byte outside rect touched at row=" + fRow + " col=" + fCol + " level=" + fLevel + " gutter=" + baseGutter);
}
}
}
}
}
}
@Test
void intArraySinkMatchesNativeSink() {
final Random rnd = new Random(24681012);
final int gutter = 4;
final int levels = 3;
final int width = 32;
final int height = 24;
final int[][] frameData = new int[levels][];
for (int level = 0; level < levels; level++) {
final int lw = width >> level;
final int lh = height >> level;
final int[] data = new int[lw * lh];
for (int i = 0; i < data.length; i++) {
data[i] = rnd.nextInt();
}
frameData[level] = data;
}
final int[][] padded = padFrame(frameData, width, height, gutter);
for (int level = 0; level < levels; level++) {
final int fLevel = level;
final int lw = width >> level;
final int lh = height >> level;
final int lg = SpritePadding.gutterForLevel(gutter, level);
final int paddedW = lw + 2 * lg;
final int paddedH = lh + 2 * lg;
final int strideBytes = paddedW * 4;
final NativeBuffer nb = new NativeBuffer(strideBytes * paddedH);
SpritePadding.writePaddedLevel(frameData[level], lw, lh, lg, nb.address, strideBytes, 0, 0, 0, paddedH,
false);
final int[] fromArray = padded[level];
final byte[] expected = new byte[strideBytes * paddedH];
for (int py = 0; py < paddedH; py++) {
for (int px = 0; px < paddedW; px++) {
putLittleEndian(expected, py * strideBytes + px * 4, fromArray[py * paddedW + px]);
}
}
assertTrue(Arrays.equals(expected, nb.snapshot()), () -> "int[] vs native sink mismatch level=" + fLevel);
final NativeBuffer nbSwapped = new NativeBuffer(strideBytes * paddedH);
SpritePadding.writePaddedLevel(frameData[level], lw, lh, lg, nbSwapped.address, strideBytes, 0, 0, 0,
paddedH, true);
final byte[] expectedSwapped = new byte[strideBytes * paddedH];
for (int py = 0; py < paddedH; py++) {
for (int px = 0; px < paddedW; px++) {
final int v = fromArray[py * paddedW + px];
final int swapped = (v & 0xFF00FF00) | ((v >> 16) & 0xFF) | ((v & 0xFF) << 16);
putLittleEndian(expectedSwapped, py * strideBytes + px * 4, swapped);
}
}
assertTrue(Arrays.equals(expectedSwapped, nbSwapped.snapshot()), () -> "int[] vs native sink (swapRB) mismatch level=" + fLevel);
}
}
}
@@ -0,0 +1,148 @@
package com.prupe.mcpatcher.mal.resource;
import net.minecraft.client.resources.FileResourcePack;
import net.minecraft.client.resources.IResourcePack;
import net.minecraft.util.ResourceLocation;
import org.junit.jupiter.api.Test;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ResourceListTest {
private static File buildZip(String prefix, String... entries) throws IOException {
File file = File.createTempFile(prefix, ".zip");
file.deleteOnExit();
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(file))) {
for (String entry : entries) {
zos.putNextEntry(new ZipEntry(entry));
if (!entry.endsWith("/")) {
zos.write(("data:" + entry).getBytes(StandardCharsets.UTF_8));
}
zos.closeEntry();
}
}
return file;
}
private static ResourceList newAggregate(List<IResourcePack> packs) throws ReflectiveOperationException {
Map<IResourcePack, Integer> orderMap = Reflect.getStatic(ResourceList.class, "resourcePackOrder");
orderMap.clear();
int order = packs.size();
for (IResourcePack pack : packs) {
orderMap.put(pack, order);
order--;
}
Constructor<ResourceList> ctor = ResourceList.class.getDeclaredConstructor(List.class);
ctor.setAccessible(true);
return ctor.newInstance(packs);
}
private static List<String> describe(Iterable<? extends ResourceLocation> resources, List<IResourcePack> packs) {
List<String> out = new ArrayList<>();
for (ResourceLocation r : resources) {
ResourceLocationWithSource rs = (ResourceLocationWithSource) r;
out.add(rs.getResourceDomain() + ":" + rs.getResourcePath() + (rs.isDirectory() ? "/" : "") + "@"
+ "ABC".charAt(packs.indexOf(rs.getSource())));
}
return out;
}
@Test
void laterPacksWinAndFirstEntryWinsWithinPack() throws Exception {
File zipA = buildZip(
"packA",
"assets/minecraft/textures/blocks/foo.png",
"assets/minecraft/textures/blocks/",
"assets/minecraft/textures/blocks/nested/bar.png",
"assets/minecraft/textures/blocks/nested/",
"assets/minecraft/textures/shared.png",
"assets/minecraft/mcpatcher/ctm/rule1.properties",
"assets/other/textures/thing.png",
"assets/minecraft/textures/dupe",
"assets/minecraft/textures/dupe/",
"pack.mcmeta");
File zipB = buildZip(
"packB",
"assets/minecraft/textures/blocks/foo.png",
"assets/minecraft/textures/onlyB.png",
"assets/minecraft/textures/blocks/",
"META-INF/MANIFEST.MF");
File zipC = buildZip(
"packC",
"assets/minecraft/textures/blocks/foo.png",
"assets/minecraft/textures/shared.png",
"assets/minecraft/mcpatcher/ctm/rule1.properties",
"assets/minecraft/textures/blocks/nested/bar.png",
"assets/minecraft/textures/onlyC.png",
"assets/minecraft/mcpatcher/ctm/",
"assets/minecraft/textures/",
"README.txt");
FileResourcePack packA = new FileResourcePack(zipA);
FileResourcePack packB = new FileResourcePack(zipB);
FileResourcePack packC = new FileResourcePack(zipC);
List<IResourcePack> packs = List.of(packA, packB, packC);
try {
ResourceList list = newAggregate(packs);
Map<ResourceLocationWithSource, ResourceLocationWithSource> resources = Reflect.get(list, "resources");
List<ResourceLocationWithSource> sorted = new ArrayList<>(resources.values());
sorted.sort(new ResourceLocationWithSource.Comparator1());
assertEquals(
List.of(
"minecraft:mcpatcher/ctm/@C",
"minecraft:mcpatcher/ctm/rule1.properties@C",
"minecraft:textures/@C",
"minecraft:textures/blocks/@B",
"minecraft:textures/blocks/foo.png@C",
"minecraft:textures/blocks/nested/@A",
"minecraft:textures/blocks/nested/bar.png@C",
"minecraft:textures/dupe@A",
"minecraft:textures/onlyB.png@B",
"minecraft:textures/onlyC.png@C",
"minecraft:textures/shared.png@C",
"other:textures/thing.png@A"),
describe(sorted, packs));
assertEquals(
List.of(
"minecraft:textures/blocks/foo.png@C",
"minecraft:textures/blocks/nested/bar.png@C",
"minecraft:textures/onlyC.png@C",
"minecraft:textures/shared.png@C",
"minecraft:textures/onlyB.png@B",
"other:textures/thing.png@A"),
describe(list.listResources(null, "textures", ".png", true, false, false), packs));
assertEquals(
List.of(
"minecraft:textures/onlyC.png@C",
"minecraft:textures/shared.png@C",
"minecraft:textures/onlyB.png@B",
"other:textures/thing.png@A"),
describe(list.listResources(null, "textures", ".png", false, false, false), packs));
assertEquals(
List.of("minecraft:mcpatcher/ctm/rule1.properties@C"),
describe(list.listResources("minecraft", "mcpatcher/ctm", ".properties", true, false, true), packs));
} finally {
packA.close();
packB.close();
packC.close();
}
}
}