Compare commits

..
9 Commits
Author SHA1 Message Date
9cb35c67cd Dark mode text recoloring with resource packs configs (#2118)
Co-authored-by: Ranzu <[email protected]>
Co-authored-by: Alexdoru <[email protected]>
Co-authored-by: Jason Mitchell <[email protected]>
2026-09-30 16:12:54 -07:00
Li XiangrongandEclipse-Sol cbd16b1a76 Fix shadow pass receiving a full-bright lightmap constant and misreporting terrain renderStage (#2170)
* Fix shadow pass lightmap availability

* Report per-layer terrain renderStage

---------

Co-authored-by: Eclipse-Sol <[email protected]>
2026-09-30 12:51:38 -04:00
Jason Mitchell cd0a9608d3 Fix async atlas with KaizPatchX (#2175) 2026-09-30 08:53:41 -07:00
KAMKEEL fa71eda3db Font Texture Crash Fix on Splash Screen (#2169) 2026-09-29 19:18:16 -07:00
KAMKEEL 9705722469 Fix Depth Mask Issue for Nametags, etc for Text (#2168) 2026-09-29 18:49:05 -07:00
Lumarin 977db3d604 Add option to change mouse movement from Cinematic to Standard while in Angelica Zoom (#2166) 2026-09-29 17:02:55 -07:00
Jason Mitchell c10d30b2da Parallelize atlas loading and fix SDL-GPU mip-level targets (#2167) 2026-09-29 16:57:53 -07:00
micvogandEclipse-Sol 2f4ac2a9b7 Apply the baked transform to vertices written directly into BakedTransformCapture (#2164)
While a direct capture is active, Tessellator.instance is the capture itself, so
RenderBlocks.renderBlockAsItem writes its vertices directly instead of going
through interceptDraw. Its own rotate/translate was therefore never baked into
the cached block mesh, which left held and dropped blocks off-center (e.g. the
snow golem's pumpkin).

Co-authored-by: Eclipse-Sol <[email protected]>
2026-09-29 18:26:31 -04:00
Death Fuel 9081f4c3e8 Various font improvements and fixes (#2165) 2026-09-29 14:58:53 -07:00
84 changed files with 5428 additions and 620 deletions

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();
@@ -29,6 +29,7 @@ import net.coderbot.iris.layer.GbufferPrograms;
import net.coderbot.iris.pipeline.DeferredWorldRenderingPipeline;
import net.coderbot.iris.pipeline.PipelineManager;
import net.coderbot.iris.uniforms.CapturedRenderingState;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.util.MathHelper;
@@ -83,6 +84,7 @@ public class BatchingFontRenderer {
private final int AAMode;
private final int AAStrength;
private final int fontBrightness;
private final int alphaTestRefLocation;
private final int mvpMatrixLocation;
private final int lightmapLocation;
@@ -126,10 +128,10 @@ public class BatchingFontRenderer {
FontProviderMC.get(this.isSGA).charWidth = this.charWidth;
FontProviderMC.get(this.isSGA).locationFontTexture = this.locationFontTexture;
//noinspection deprecation
fontShaderId = FontAAShader.getProgram().getProgramId();
AAMode = GLStateManager.glGetUniformLocation(fontShaderId, "aaMode");
AAStrength = GLStateManager.glGetUniformLocation(fontShaderId, "strength");
fontBrightness = GLStateManager.glGetUniformLocation(fontShaderId, "fontBrightness");
alphaTestRefLocation = GLStateManager.glGetUniformLocation(fontShaderId, "alphaTestRef");
mvpMatrixLocation = GLStateManager.glGetUniformLocation(fontShaderId, "u_MVPMatrix");
lightmapLocation = GLStateManager.glGetUniformLocation(fontShaderId, "u_Lightmap");
@@ -144,8 +146,6 @@ public class BatchingFontRenderer {
// === Batched rendering
private static final int INITIAL_BATCH_SIZE = 2048;
private static final ResourceLocation DUMMY_RESOURCE_LOCATION = new ResourceLocation("angelica$dummy",
"this is invalid!");
// Layout in data:
// [v, v, t, t, c, c, c, c, tb, tb, tb, tb]
@@ -257,6 +257,7 @@ public class BatchingFontRenderer {
float normalX, normalY, normalZ;
int blockEntityId;
boolean depthTest;
boolean depthMask;
}
// 16-bit EBO index range
@@ -402,11 +403,23 @@ public class BatchingFontRenderer {
private static final int LAYER_BACKGROUND = -1;
private static final int LAYER_DEFAULT = 0;
private void pushDrawCmd(int startIdx, int idxCount, ResourceLocation texture, boolean isUnicode) {
/**
* FML's splash font renderer draws before the texture manager exists and binds its own
* font texture, so let it bind and read the id back.
*/
private int textureFor(FontProvider fontProvider, char chr) {
if (fontProvider instanceof FontProviderCustom || Minecraft.getMinecraft().getTextureManager() != null) {
return fontProvider.getTexture(chr);
}
((FontRendererAccessor) underlying).angelica$bindTexture(locationFontTexture);
return GLStateManager.getBoundTextureForServerState();
}
private void pushDrawCmd(int startIdx, int idxCount, int texture, boolean isUnicode) {
pushDrawCmd(startIdx, idxCount, texture, isUnicode, LAYER_DEFAULT);
}
private void pushDrawCmd(int startIdx, int idxCount, ResourceLocation texture, boolean isUnicode, int layer) {
private void pushDrawCmd(int startIdx, int idxCount, int texture, boolean isUnicode, int layer) {
arenaOwner = this;
// Never coalesce into a command below the watermark - it belongs to a sealed segment.
if (batchCommands.size() > batchSealedEnd) {
@@ -433,7 +446,7 @@ public class BatchingFontRenderer {
public int startVtx;
public int idxCount;
public boolean isUnicode;
public ResourceLocation texture;
public int texture;
/**
* Sorted ahead of the texture, so lower layers draw first. Sorting by texture
* means submission order is not kept, so anything belonging under the glyphs
@@ -441,11 +454,11 @@ public class BatchingFontRenderer {
*/
public int layer;
public void reset(int startVtx, int vtxCount, ResourceLocation texture, boolean isUnicode) {
public void reset(int startVtx, int vtxCount, int texture, boolean isUnicode) {
reset(startVtx, vtxCount, texture, isUnicode, LAYER_DEFAULT);
}
public void reset(int startVtx, int vtxCount, ResourceLocation texture, boolean isUnicode, int layer) {
public void reset(int startVtx, int vtxCount, int texture, boolean isUnicode, int layer) {
this.layer = layer;
this.startVtx = startVtx;
this.idxCount = vtxCount;
@@ -458,8 +471,7 @@ public class BatchingFontRenderer {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (FontDrawCmd) obj;
return this.startVtx == that.startVtx && this.idxCount == that.idxCount && Objects.equals(this.texture,
that.texture);
return this.startVtx == that.startVtx && this.idxCount == that.idxCount && this.texture == that.texture;
}
@Override
@@ -483,10 +495,8 @@ public class BatchingFontRenderer {
public static final Comparator<FontDrawCmd> DRAW_ORDER_COMPARATOR =
Comparator.comparingInt((FontDrawCmd fdc) -> fdc.layer)
.thenComparing(fdc -> fdc.texture,
Comparator.nullsLast(Comparator.comparing(ResourceLocation::getResourceDomain)
.thenComparing(ResourceLocation::getResourcePath)))
.thenComparing(fdc -> fdc.startVtx);
.thenComparingInt(fdc -> fdc.texture == 0 ? Integer.MAX_VALUE : fdc.texture)
.thenComparingInt(fdc -> fdc.startVtx);
}
/**
@@ -546,6 +556,7 @@ public class BatchingFontRenderer {
segment.normalZ = normal.z;
segment.blockEntityId = CapturedRenderingState.INSTANCE.getCurrentRenderedBlockEntity();
segment.depthTest = GLStateManager.getDepthTest().isEnabled();
segment.depthMask = GLStateManager.isEffectiveDepthMaskEnabled();
batchSegments.add(segment);
batchSealedEnd = end;
}
@@ -599,7 +610,7 @@ public class BatchingFontRenderer {
deferredIdxPos = 0;
truncateBatchToWatermark();
arenaOwner = null;
flushLastTexture = null;
flushLastTexture = 0;
}
private void deferBatch() {
@@ -658,7 +669,6 @@ public class BatchingFontRenderer {
GLStateManager.enableAlphaTest();
GLStateManager.enableBlend();
GLStateManager.tryBlendFuncSeparate(first.blendSrcRGB, first.blendDstRGB, GL11.GL_ONE, GL11.GL_ZERO);
GLStateManager.glDepthMask(false);
final Boolean prevTranslucency = GbufferPrograms.beginTranslucencyDeclaration(Boolean.TRUE);
GbufferPrograms.setBlockEntityDefaults();
@@ -667,6 +677,7 @@ public class BatchingFontRenderer {
GLStateManager.glPushMatrix();
try {
boolean curDepthTest = GLStateManager.getDepthTest().isEnabled();
boolean curDepthMask = GLStateManager.isEffectiveDepthMaskEnabled();
for (final TextSegment segment : deferredSegments) {
final BatchingFontRenderer owner = segment.owner;
if (segment.cmdStart == segment.cmdEnd) continue;
@@ -678,6 +689,10 @@ public class BatchingFontRenderer {
if (segment.depthTest) GLStateManager.enableDepthTest(); else GLStateManager.disableDepthTest();
curDepthTest = segment.depthTest;
}
if (segment.depthMask != curDepthMask) {
GLStateManager.glDepthMask(segment.depthMask);
curDepthMask = segment.depthMask;
}
final FontDrawCmd[] cmdsData = owner.batchCommands.elements();
Arrays.sort(cmdsData, segment.cmdStart, segment.cmdEnd, FontDrawCmd.DRAW_ORDER_COMPARATOR);
@@ -703,12 +718,12 @@ public class BatchingFontRenderer {
try {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
deferredSegments.get(0).owner.setupFontDrawState();
GLStateManager.glDepthMask(false);
flushLastTexture = DUMMY_RESOURCE_LOCATION;
flushLastTexture = -1;
resetFlushLightmap();
try (MemoryStack stack = stackPush()) {
final FloatBuffer mvpBuf = stack.mallocFloat(16);
boolean curDepthTest = GLStateManager.getDepthTest().isEnabled();
boolean curDepthMask = GLStateManager.isEffectiveDepthMaskEnabled();
for (final TextSegment segment : deferredSegments) {
mvpBuf.clear();
segment.mvp.get(mvpBuf);
@@ -719,6 +734,10 @@ public class BatchingFontRenderer {
if (segment.depthTest) GLStateManager.enableDepthTest(); else GLStateManager.disableDepthTest();
curDepthTest = segment.depthTest;
}
if (segment.depthMask != curDepthMask) {
GLStateManager.glDepthMask(segment.depthMask);
curDepthMask = segment.depthMask;
}
drawCommands(segment.owner.batchCommands.elements(), segment.cmdStart, segment.cmdEnd, segment.owner);
}
}
@@ -769,6 +788,7 @@ public class BatchingFontRenderer {
private static final Matrix4f scratchModelView = new Matrix4f();
private int fontAAModeLast = -1;
private int fontAAStrengthLast = -1;
private float fontBrightnessLast = -1;
private void flushBatch() {
final boolean locked = GLStateManager.acquireDrawLock();
@@ -827,7 +847,7 @@ public class BatchingFontRenderer {
final int d = GLStateManager.pushState(StateSet.FONT);
try {
setupFontDrawState();
flushLastTexture = DUMMY_RESOURCE_LOCATION;
flushLastTexture = -1;
resetFlushLightmap();
try (MemoryStack stack = stackPush()) {
final FloatBuffer mvpBuf = stack.mallocFloat(16);
@@ -874,6 +894,10 @@ public class BatchingFontRenderer {
fontAAStrengthLast = FontConfig.fontAAStrength;
GLStateManager.glUniform1f(AAStrength, FontConfig.fontAAStrength / 120.f);
}
if (FontConfig.fontBrightness != fontBrightnessLast) {
fontBrightnessLast = FontConfig.fontBrightness;
GLStateManager.glUniform1f(fontBrightness, 0.625f * FontConfig.fontBrightness + 0.1875f);
}
GLStateManager.glUniform1f(alphaTestRefLocation, GLStateManager.getAlphaState().getReference());
if (lightmapSamplerProgram != fontShaderId) {
GLStateManager.glUniform1i(lightmapSamplerLocation, LIGHTMAP_TEX_UNIT);
@@ -913,7 +937,7 @@ public class BatchingFontRenderer {
}
}
private static ResourceLocation flushLastTexture;
private static int flushLastTexture;
private static float flushLastLightmapU;
private static float flushLastLightmapV;
@@ -950,14 +974,14 @@ public class BatchingFontRenderer {
private static void drawCommands(FontDrawCmd[] cmdsData, int from, int to, BatchingFontRenderer owner) {
for (int i = from; i < to; i++) {
final FontDrawCmd cmd = cmdsData[i];
if (!Objects.equals(flushLastTexture, cmd.texture)) {
if (flushLastTexture == null) {
if (flushLastTexture != cmd.texture) {
if (flushLastTexture == 0) {
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
} else if (cmd.texture == null) {
} else if (cmd.texture == 0) {
GLStateManager.glDisable(GL11.GL_TEXTURE_2D);
}
if (cmd.texture != null) {
((FontRendererAccessor) owner.underlying).angelica$bindTexture(cmd.texture);
if (cmd.texture != 0) {
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, cmd.texture);
}
flushLastTexture = cmd.texture;
}
@@ -1028,21 +1052,21 @@ public class BatchingFontRenderer {
private static void emitRangeThroughPipeline(BatchingFontRenderer owner, FontDrawCmd[] cmdsData, int from, int to,
int packedLight, float normalX, float normalY, float normalZ) {
final Tessellator tessellator = Tessellator.instance;
ResourceLocation lastTexture = DUMMY_RESOURCE_LOCATION;
int lastTexture = -1;
boolean drawing = false;
try {
for (int i = from; i < to; i++) {
final FontDrawCmd cmd = cmdsData[i];
if (!Objects.equals(lastTexture, cmd.texture)) {
if (lastTexture != cmd.texture) {
if (drawing) {
tessellator.draw();
drawing = false;
}
if (cmd.texture == null) {
if (cmd.texture == 0) {
GLStateManager.disableTexture();
} else {
GLStateManager.enableTexture();
((FontRendererAccessor) owner.underlying).angelica$bindTexture(cmd.texture);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, cmd.texture);
}
lastTexture = cmd.texture;
}
@@ -1108,6 +1132,32 @@ public class BatchingFontRenderer {
lightmapPackedLight = -1;
}
public static boolean darkModeRecolorEnabled = false;
public static boolean enterRecolorSection(boolean enable) {
boolean prev = darkModeRecolorEnabled;
darkModeRecolorEnabled = enable;
return prev;
}
public static void exitRecolorSection(boolean prev) {
darkModeRecolorEnabled = prev;
}
// Inside a button section, text drawn in one of the button's own three colors is swapped for button_font's color.
// Button sections can't be nested: only the on/off flag is restored on exit, not the colors.
private static boolean buttonSectionActive = false;
private static int buttonEnabledColor, buttonHoveredColor, buttonDisabledColor;
public static boolean enterButtonSection(int enabledColor, int hoveredColor, int disabledColor) {
boolean prev = buttonSectionActive;
buttonSectionActive = true;
buttonEnabledColor = enabledColor;
buttonHoveredColor = hoveredColor;
buttonDisabledColor = disabledColor;
return prev;
}
public static void exitButtonSection(boolean prev) {
buttonSectionActive = prev;
}
// === Actual text mesh generation
public static boolean charInRange(char what, char fromInclusive, char toInclusive) {
@@ -1191,12 +1241,15 @@ public class BatchingFontRenderer {
private static final double WAVE_TIME_SCALE = 5e-9;
private static final float WAVE_FREQUENCY = 0.5f;
public float drawString(final float anchorX, final float anchorY, final int color, final boolean enableShadow,
public float drawString(final float anchorX, final float anchorY, final int inputColor, final boolean enableShadow,
final boolean unicodeFlag, final CharSequence string, int stringOffset, int stringLength) {
// noinspection SizeReplaceableByIsEmpty
if (string == null || string.length() == 0) {
return anchorX + (enableShadow ? 1.0f : 0.0f);
}
final int color = buttonSectionActive
? DarkModeUtils.recolorButtonText(inputColor, buttonEnabledColor, buttonHoveredColor, buttonDisabledColor)
: inputColor;
final int shadowColor = (color & 0xfcfcfc) >> 2 | color & 0xff000000;
FontProviderMC.get(this.isSGA).charWidth = this.charWidth;
@@ -1257,7 +1310,7 @@ public class BatchingFontRenderer {
if (curUnderline && underlineStartX != underlineEndX) {
final int ulIdx = idxWriterIndex;
pushUntexRect(underlineStartX, underlineY, underlineEndX - underlineStartX, glyphScaleY, curColor);
pushDrawCmd(ulIdx, 6, null, false);
pushDrawCmd(ulIdx, 6, 0, false);
underlineStartX = underlineEndX;
}
if (curStrikethrough && strikethroughStartX != strikethroughEndX) {
@@ -1268,7 +1321,7 @@ public class BatchingFontRenderer {
strikethroughEndX - strikethroughStartX,
glyphScaleY,
curColor);
pushDrawCmd(ulIdx, 6, null, false);
pushDrawCmd(ulIdx, 6, 0, false);
strikethroughStartX = strikethroughEndX;
}
@@ -1290,6 +1343,9 @@ public class BatchingFontRenderer {
curColor = (curColor & 0xFF000000) | (rgb & 0x00FFFFFF);
curShadowColor = (curShadowColor & 0xFF000000) | ((rgb & 0xFCFCFC) >> 2);
charIdx += SECTION_X_PAYLOAD;
if (darkModeRecolorEnabled) {
curShadow = false;
}
}
} else {
final boolean is09 = charInRange(fmtCode, '0', '9');
@@ -1310,6 +1366,9 @@ public class BatchingFontRenderer {
curColor = (curColor & 0xFF000000) | (rgb & 0x00FFFFFF);
final int shadowRgb = this.colorCode[colorIdx + 16];
curShadowColor = (curShadowColor & 0xFF000000) | (shadowRgb & 0x00FFFFFF);
if (darkModeRecolorEnabled) {
curShadow = false;
}
} else if (fmtCode == 'k') {
curRandom = true;
} else if (fmtCode == 'l') {
@@ -1442,11 +1501,11 @@ public class BatchingFontRenderer {
final float glyphW = fontProvider.getGlyphW(chr) * glyphScaleX;
final float uSz = fontProvider.getUSize(chr);
final float vSz = fontProvider.getVSize(chr);
final float itOff = curItalic ? 1.0F : 0.0F; // italic offset
final float itOff = curItalic ? glyphScaleX : 0.0F; // italic offset
final float shadowOffset = fontProvider.getShadowOffset();
final int shadowCopies = FontConfig.shadowCopies;
final int boldCopies = FontConfig.boldCopies;
final ResourceLocation texture = fontProvider.getTexture(chr);
final int texture = textureFor(fontProvider, chr);
// Wave: Y offset via sine wave
float renderY = heightNorth;
@@ -1455,6 +1514,15 @@ public class BatchingFontRenderer {
renderY += (float) Math.sin(visibleCharIndex * WAVE_FREQUENCY + time) * AngelicaConfig.waveAmplitude;
}
if (darkModeRecolorEnabled) {
long recolor = DarkModeUtils.computeGuiFontRecolor(curColor);
if (recolor != DarkModeUtils.NO_RECOLOR) {
curColor = DarkModeUtils.unpackColor(recolor);
curShadowColor = (curShadowColor & 0xFF000000) | DarkModeUtils.unpackShadowRgb(recolor);
curShadow = DarkModeUtils.unpackShadow(recolor);
}
}
float renderX = curX;
int glyphColor = curColor;
int glyphShadowColor = curShadowColor;
@@ -1477,13 +1545,13 @@ public class BatchingFontRenderer {
if (glyphBackground != 0) {
final int bgIdx = idxWriterIndex;
pushUntexRect(renderX, renderY, glyphW, heightSouth, glyphBackground);
pushDrawCmd(bgIdx, 6, null, false, LAYER_BACKGROUND);
pushDrawCmd(bgIdx, 6, 0, false, LAYER_BACKGROUND);
}
// After the background, so this command covers only what is counted below.
final int idxId = idxWriterIndex;
final boolean drawShadow = enableShadow || curShadow;
final boolean drawShadow = enableShadow || curShadow || (darkModeRecolorEnabled && DarkModeUtils.shadowsGlobal());
if (drawShadow) {
final int effectiveShadowColor = curShadowCustomColor
? ((glyphColor & 0xFF000000) | (curShadowColorOverride & 0x00FFFFFF))
@@ -1531,13 +1599,13 @@ public class BatchingFontRenderer {
if (perGlyphColor && curUnderline && underlineStartX != underlineEndX) {
final int ulIdx = idxWriterIndex;
pushUntexRect(underlineStartX, underlineY, underlineEndX - underlineStartX, glyphScaleY, glyphColor);
pushDrawCmd(ulIdx, 6, null, false);
pushDrawCmd(ulIdx, 6, 0, false);
underlineStartX = underlineEndX;
}
if (perGlyphColor && curStrikethrough && strikethroughStartX != strikethroughEndX) {
final int stIdx = idxWriterIndex;
pushUntexRect(strikethroughStartX, strikethroughY, strikethroughEndX - strikethroughStartX, glyphScaleY, glyphColor);
pushDrawCmd(stIdx, 6, null, false);
pushDrawCmd(stIdx, 6, 0, false);
strikethroughStartX = strikethroughEndX;
}
}
@@ -1545,7 +1613,7 @@ public class BatchingFontRenderer {
if (curUnderline && underlineStartX != underlineEndX) {
final int ulIdx = idxWriterIndex;
pushUntexRect(underlineStartX, underlineY, underlineEndX - underlineStartX, glyphScaleY, curColor);
pushDrawCmd(ulIdx, 6, null, false);
pushDrawCmd(ulIdx, 6, 0, false);
}
if (curStrikethrough && strikethroughStartX != strikethroughEndX) {
final int ulIdx = idxWriterIndex;
@@ -1555,7 +1623,7 @@ public class BatchingFontRenderer {
strikethroughEndX - strikethroughStartX,
glyphScaleY,
curColor);
pushDrawCmd(ulIdx, 6, null, false);
pushDrawCmd(ulIdx, 6, 0, false);
}
} finally {
@@ -0,0 +1,316 @@
package com.gtnewhorizons.angelica.client.font;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.IResourcePack;
import net.minecraft.client.resources.ResourcePackRepository;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
import com.prupe.mcpatcher.mal.resource.TexturePackChangeHandler;
public class DarkModeUtils {
private static final Logger LOGGER = LogManager.getLogger("DarkModeUtils");
private static final String ROOT_KEY = "dark_mode_utils";
private static final int SUPPORTED_SCHEMA = 1;
private static final String DEBUG_PULSE = "debug_pulse";
private static final double DEBUG_PULSE_TIME_SCALE = 5e-9;
private static final String[] INPUT_STREAM_METHOD_NAMES = { "getInputStreamByName", "func_110591_a" };
private static final Map<Class<?>, Method> INPUT_STREAM_METHOD_CACHE = new HashMap<>();
private static FontRecolorRule guiFontRule = null;
private static ButtonFontRules buttonFontRules = null;
private static boolean shadowsGlobal = false;
static {
TexturePackChangeHandler.register(new TexturePackChangeHandler("Angelica Dark Mode Utils", 1) {
@Override
public void beforeChange() {}
@Override
public void afterChange() {
reload();
}
});
}
private DarkModeUtils() {}
public static void init() { /* see static block */ }
private static void reload() {
FontRecolorRule guiFont = null;
ButtonFontRules buttonFont = null;
boolean shadowsGlobalValue = false;
ResourcePackRepository repository = Minecraft.getMinecraft().getResourcePackRepository();
for (ResourcePackRepository.Entry entry : repository.getRepositoryEntries()) {
IResourcePack pack = entry.getResourcePack();
PackDarkModeRules rules = readPackRules(pack);
if (rules == null) {
continue;
}
if (rules.guiFont() != null) {
guiFont = rules.guiFont();
}
if (rules.buttonFont() != null) {
buttonFont = rules.buttonFont();
}
if (rules.shadowsGlobal() != null) {
shadowsGlobalValue = rules.shadowsGlobal();
}
}
guiFontRule = guiFont;
buttonFontRules = buttonFont;
shadowsGlobal = shadowsGlobalValue;
}
// Mechanism similar to GTNHLib's PackMcmetaReader. Will be refactored into one common implementation if a third use case arises.
private static PackDarkModeRules readPackRules(IResourcePack pack) {
try (InputStream stream = openPackMcmeta(pack)) {
if (stream == null) {
return null;
}
JsonElement rootElement = new JsonParser().parse(new InputStreamReader(stream, StandardCharsets.UTF_8));
if (!rootElement.isJsonObject()) {
return null;
}
JsonObject root = rootElement.getAsJsonObject();
if (!root.has(ROOT_KEY) || !root.get(ROOT_KEY).isJsonObject()) {
return null;
}
JsonObject darkModeUtils = root.getAsJsonObject(ROOT_KEY);
int schema = darkModeUtils.has("schema") ? darkModeUtils.get("schema").getAsInt() : -1;
if (schema != SUPPORTED_SCHEMA) {
LOGGER.warn("Unsupported dark_mode_utils schema {} in pack {}", schema, pack.getPackName());
return null;
}
JsonObject target = darkModeUtils.has("target") && darkModeUtils.get("target").isJsonObject()
? darkModeUtils.getAsJsonObject("target")
: null;
if (target == null) {
return null;
}
FontRecolorRule guiFont = target.has("gui_font") && target.get("gui_font").isJsonObject()
? parseFontRecolorRule(target.getAsJsonObject("gui_font"), pack.getPackName(), "gui_font")
: null;
ButtonFontRules buttonFont = target.has("button_font") && target.get("button_font").isJsonObject()
? parseButtonFontRules(target.getAsJsonObject("button_font"), pack.getPackName())
: null;
Boolean shadowsGlobal = target.has("shadows_global")
? target.get("shadows_global").getAsBoolean()
: null;
return new PackDarkModeRules(guiFont, buttonFont, shadowsGlobal);
} catch (IOException e) {
// pack.mcmeta missing/unreadable
return null;
} catch (RuntimeException e) {
LOGGER.warn("Invalid dark_mode_utils block in pack {}: {}", pack.getPackName(), e.toString());
return null;
}
}
private static ButtonFontRules parseButtonFontRules(JsonObject buttonFont, String packName) {
ButtonColorRule enabled = parseButtonState(buttonFont, "enabled", packName);
ButtonColorRule hovered = parseButtonState(buttonFont, "hovered", packName);
ButtonColorRule disabled = parseButtonState(buttonFont, "disabled", packName);
if (enabled == null && hovered == null && disabled == null) {
return null;
}
return new ButtonFontRules(enabled, hovered, disabled);
}
private static ButtonColorRule parseButtonState(JsonObject buttonFont, String state, String packName) {
if (!buttonFont.has(state) || !buttonFont.get(state).isJsonObject()) {
return null;
}
try {
String outputValue = buttonFont.getAsJsonObject(state).get("output").getAsString().trim();
boolean debugPulse = DEBUG_PULSE.equalsIgnoreCase(outputValue);
return new ButtonColorRule(debugPulse ? 0 : parseHexColor(outputValue), debugPulse);
} catch (RuntimeException e) {
LOGGER.warn("Invalid dark_mode_utils button_font.{} rule in pack {}: {}", state, packName, e.toString());
return null;
}
}
private static FontRecolorRule parseFontRecolorRule(JsonObject obj, String packName, String context) {
try {
JsonObject input = obj.getAsJsonObject("input");
int minimum = parseHexColor(input.get("minimum").getAsString());
int maximum = parseHexColor(input.get("maximum").getAsString());
String outputValue = obj.get("output").getAsString().trim();
boolean debugPulse = DEBUG_PULSE.equalsIgnoreCase(outputValue);
int output = debugPulse ? 0 : parseHexColor(outputValue);
boolean shadow = obj.has("shadow") && obj.get("shadow").getAsBoolean();
return new FontRecolorRule(minimum, maximum, output, debugPulse, shadow);
} catch (RuntimeException e) {
LOGGER.warn("Invalid dark_mode_utils {} rule in pack {}: {}", context, packName, e.toString());
return null;
}
}
private static int parseHexColor(String value) {
return Integer.decode(value.trim()) & 0x00FFFFFF;
}
private static InputStream openPackMcmeta(IResourcePack pack) throws IOException {
Method method = findGetInputStreamByName(pack.getClass());
if (method == null) {
return null;
}
try {
return (InputStream) method.invoke(pack, "pack.mcmeta");
} catch (InvocationTargetException e) {
if (e.getCause() instanceof IOException ioException) {
throw ioException;
}
return null;
} catch (IllegalAccessException e) {
return null;
}
}
private static Method findGetInputStreamByName(Class<?> type) {
if (INPUT_STREAM_METHOD_CACHE.containsKey(type)) {
return INPUT_STREAM_METHOD_CACHE.get(type);
}
Method found = null;
outer: for (Class<?> current = type; current != null; current = current.getSuperclass()) {
for (String name : INPUT_STREAM_METHOD_NAMES) {
try {
found = current.getDeclaredMethod(name, String.class);
found.setAccessible(true);
break outer;
} catch (NoSuchMethodException ignored) {
}
}
}
INPUT_STREAM_METHOD_CACHE.put(type, found);
return found;
}
public static final long NO_RECOLOR = -1L;
public static long computeGuiFontRecolor(int argbColor) {
FontRecolorRule rule = guiFontRule;
if (rule == null) {
return NO_RECOLOR;
}
return rule.tryRecolor(argbColor);
}
public static boolean shadowsGlobal() {
return shadowsGlobal;
}
// layout: newColor (32), shadowRgb (24, no alpha), zeroes (7), shadow (1)
private static long packRecolor(int newColor, int shadowRgb, boolean shadow) {
return ((long) newColor << 32) | ((long) (shadowRgb & 0xFFFFFF) << 8) | (shadow ? 1L : 0L);
}
public static int unpackColor(long packed) {
return (int) (packed >>> 32);
}
public static int unpackShadowRgb(long packed) {
return (int) ((packed >>> 8) & 0xFFFFFF);
}
public static boolean unpackShadow(long packed) {
return (packed & 1L) != 0;
}
/**
* Inside a button section, swaps text drawn in one of the button's own three colors for the matching button_font
* color. Pure white also counts as the enabled color, since it's the most likely override of it. Any other color
* is returned untouched.
*/
public static int recolorButtonText(int argbColor, int enabledColor, int hoveredColor, int disabledColor) {
final ButtonFontRules rules = buttonFontRules;
if (rules == null) {
return argbColor;
}
final int rgb = argbColor & 0x00FFFFFF;
final ButtonColorRule rule;
if (rgb == enabledColor || rgb == 0xFFFFFF) { rule = rules.enabled(); }
else if (rgb == hoveredColor) { rule = rules.hovered(); }
else if (rgb == disabledColor) { rule = rules.disabled(); }
else { return argbColor; }
if (rule == null) {
return argbColor;
}
return (argbColor & 0xFF000000) | (rule.debugPulse() ? computeDebugPulseRgb() : rule.output());
}
private static int computeDebugPulseRgb() {
final float time = HUDCaching.renderingCacheOverride ? 0f
: (float) ((System.nanoTime() & 0xFFFFFFFFFFFFL) * DEBUG_PULSE_TIME_SCALE);
final int animated = (int) (Math.round(0x00007F80 * (Math.sin(2 * time) + 1)) & 0x0000FFFF);
return 0x00FF0000 | animated;
}
private record PackDarkModeRules(FontRecolorRule guiFont, ButtonFontRules buttonFont, Boolean shadowsGlobal) {}
private record ButtonFontRules(ButtonColorRule enabled, ButtonColorRule hovered, ButtonColorRule disabled) {}
private record ButtonColorRule(int output, boolean debugPulse) {}
private static final class FontRecolorRule {
final int minR, minG, minB;
final int maxR, maxG, maxB;
final int output;
final boolean debugPulse;
final boolean shadow;
FontRecolorRule(int minimum, int maximum, int output, boolean debugPulse, boolean shadow) {
this.minR = (minimum >> 16) & 0xFF;
this.minG = (minimum >> 8) & 0xFF;
this.minB = minimum & 0xFF;
this.maxR = (maximum >> 16) & 0xFF;
this.maxG = (maximum >> 8) & 0xFF;
this.maxB = maximum & 0xFF;
this.output = output;
this.debugPulse = debugPulse;
this.shadow = shadow;
}
long tryRecolor(int argbColor) {
final int r = (argbColor >> 16) & 0xFF;
final int g = (argbColor >> 8) & 0xFF;
final int b = argbColor & 0xFF;
final boolean nearBlack = r <= minR && g <= minG && b <= minB;
final boolean nearWhite = r >= maxR && g >= maxG && b >= maxB;
if (!nearBlack && !nearWhite) {
return NO_RECOLOR;
}
final int outputRgb = debugPulse ? computeDebugPulseRgb() : output;
final int newColor = (argbColor & 0xFF000000) | outputRgb;
final int shadowRgb = (outputRgb & 0xFCFCFC) >> 2;
return packRecolor(newColor, shadowRgb, shadow);
}
}
}
@@ -1,7 +1,5 @@
package com.gtnewhorizons.angelica.client.font;
import net.minecraft.util.ResourceLocation;
public interface FontProvider {
/** Tries before {@link #getRandomReplacement} gives up and leaves the character alone. */
@@ -20,6 +18,6 @@ public interface FontProvider {
float getUSize(char chr);
float getVSize(char chr);
float getShadowOffset();
ResourceLocation getTexture(char chr);
int getTexture(char chr);
float getYScaleMultiplier();
}
@@ -1,34 +1,36 @@
package com.gtnewhorizons.angelica.client.font;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAllocInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
import com.gtnewhorizons.angelica.config.FontConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import jss.util.RandomXoshiro256StarStar;
import lombok.Setter;
import lombok.Value;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.ITextureObject;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.client.renderer.texture.TextureUtil;
import net.minecraft.util.ResourceLocation;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import javax.imageio.ImageIO;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;
import java.awt.image.ColorModel;
import java.awt.image.DataBufferInt;
import java.awt.image.Raster;
import java.nio.IntBuffer;
import java.util.Objects;
import static com.gtnewhorizons.angelica.client.font.FontStrategist.getFontName;
public final class FontProviderCustom implements FontProvider {
public static final Logger LOGGER = LogManager.getLogger("Angelica");
public static final String FONT_DIR = "fonts/custom/";
static final int ATLAS_SIZE = 128;
static final int ATLAS_COUNT = 512;
private final byte id; // 0 - primary font, 1 - fallback font
@@ -52,7 +54,7 @@ public final class FontProviderCustom implements FontProvider {
}
int fontPos = -1;
for (int i = 0; i < availableFonts.length; i++) {
if (Objects.equals(myFontName, availableFonts[i].getFontName())) {
if (Objects.equals(myFontName, getFontName(availableFonts[i]))) {
fontPos = i;
break;
}
@@ -75,45 +77,15 @@ public final class FontProviderCustom implements FontProvider {
this.currentFontQuality = FontConfig.customFontQuality;
this.font = FontStrategist.getAvailableFonts()[fontID].deriveFont(this.currentFontQuality);
File[] files = new File(getFontDir()).listFiles();
if (files != null) {
for (File f : files) {
if (!Files.isSymbolicLink(f.toPath())) {
f.delete();
}
}
}
TextureManager tm = Minecraft.getMinecraft().getTextureManager();
Map mapTextureObjects = tm.mapTextureObjects;
for (int i = 0; i < ATLAS_COUNT; i++) {
ResourceLocation key = new ResourceLocation(getAtlasResourceName(i));
if (mapTextureObjects.containsKey(key)) {
ITextureObject obj = (ITextureObject) mapTextureObjects.get(key);
TextureUtil.deleteTexture(obj.getGlTextureId());
mapTextureObjects.remove(key);
for (FontAtlas atlas : fontAtlases) {
if (atlas != null && atlas.texture != 0) {
GLStateManager.glDeleteTextures(atlas.texture);
}
}
this.fontAtlases = new FontAtlas[ATLAS_COUNT];
}
private String getFontDir() {
return FONT_DIR + "f" + this.id + "/";
}
private String getAtlasFilename(int atlasId) {
return "f" + this.id + "p" + atlasId;
}
String getAtlasResourceName(int atlasId) {
return "minecraft:angelica_c" + getAtlasFilename(atlasId);
}
String getAtlasFullPath(int atlasId) {
return getFontDir() + getAtlasFilename(atlasId) + ".png";
}
@Value
private class GlyphData {
float uStart;
@@ -125,9 +97,8 @@ public final class FontProviderCustom implements FontProvider {
}
private class FontAtlas {
GlyphData[] glyphData = new GlyphData[ATLAS_SIZE];
private ResourceLocation texture;
private int texture;
private final int id;
FontAtlas(int id) {
@@ -214,13 +185,46 @@ public final class FontProviderCustom implements FontProvider {
tileX++;
}
g2d.dispose();
try {
Files.createDirectories(Paths.get(getFontDir()));
ImageIO.write(image, "png", new File(getAtlasFullPath(this.id)));
} catch (IOException e) {
e.printStackTrace();
int id = GLStateManager.glGenTextures();
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);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);
final int width = image.getWidth();
final int height = image.getHeight();
final IntBuffer pixelBuffer = memAllocInt(width * height);
long ptr = memAddress0(pixelBuffer);
if (image.getRaster().getDataBuffer() instanceof DataBufferInt dataBufferInt) {
final int[] pixelValues = dataBufferInt.getData();
for (int i : pixelValues) {
memPutInt(ptr, i);
ptr += 4;
}
pixelBuffer.limit(pixelValues.length);
} else {
Raster raster = image.getRaster();
ColorModel colorModel = image.getColorModel();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
Object pixel = raster.getDataElements(x, y, null);
memPutInt(ptr, colorModel.getRGB(pixel));
ptr += 4;
}
}
pixelBuffer.limit(image.getHeight() * image.getWidth());
}
this.texture = new ResourceLocation(getAtlasResourceName(this.id));
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL12.GL_BGRA, image.getWidth(), image.getHeight(), 0, GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, pixelBuffer);
memFree(pixelBuffer);
this.texture = id;
}
}
@@ -303,7 +307,7 @@ public final class FontProviderCustom implements FontProvider {
}
@Override
public ResourceLocation getTexture(char chr) {
public int getTexture(char chr) {
return getAtlas(chr).texture;
}
@@ -3,6 +3,10 @@ package com.gtnewhorizons.angelica.client.font;
import com.gtnewhorizons.angelica.config.FontConfig;
import it.unimi.dsi.fastutil.chars.Char2ShortOpenHashMap;
import jss.util.RandomXoshiro256StarStar;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.ITextureObject;
import net.minecraft.client.renderer.texture.SimpleTexture;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.util.ResourceLocation;
public final class FontProviderMC implements FontProvider {
@@ -109,8 +113,15 @@ public final class FontProviderMC implements FontProvider {
}
@Override
public ResourceLocation getTexture(char chr) {
return locationFontTexture;
public int getTexture(char chr) {
final TextureManager tm = Minecraft.getMinecraft().getTextureManager();
ITextureObject tex = tm.getTexture(locationFontTexture);
if (tex == null) {
tex = new SimpleTexture(locationFontTexture);
tm.loadTexture(locationFontTexture, tex);
tex = tm.getTexture(locationFontTexture);
}
return tex.getGlTextureId();
}
@Override
@@ -3,6 +3,9 @@ package com.gtnewhorizons.angelica.client.font;
import com.gtnewhorizons.angelica.config.FontConfig;
import jss.util.RandomXoshiro256StarStar;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.ITextureObject;
import net.minecraft.client.renderer.texture.SimpleTexture;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.client.resources.IReloadableResourceManager;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.client.resources.IResourceManagerReloadListener;
@@ -79,12 +82,12 @@ public final class FontProviderUnicode implements FontProvider, IResourceManager
@Override
public float getUStart(char chr) {
final float startColumnF = (float)((this.glyphWidth[chr] >>> 4) & 15);
return ((float) (chr % 16 * 16) + startColumnF + 0.21f) / 256.0f;
return ((float) (chr % 16 * 16) + startColumnF + 0.01f) / 256.0f;
}
@Override
public float getVStart(char chr) {
return ((float) ((chr & 255) / 16 * 16) + 0.21f) / 256.0f;
return ((float) ((chr & 255) / 16 * 16) + 0.01f) / 256.0f;
}
@Override
@@ -113,12 +116,12 @@ public final class FontProviderUnicode implements FontProvider, IResourceManager
final float startColumnF = (float) startColumn;
final float endColumnF = (float) (endColumn + 1);
final float chrWidth = endColumnF - startColumnF - 0.02F;
return (chrWidth - 0.42f) / 256.0f;
return (chrWidth - 0.02f) / 256.0f;
}
@Override
public float getVSize(char chr) {
return (16.0f - 0.42f) / 256.0f;
return (16.0f - 0.02f) / 256.0f;
}
@Override
@@ -127,9 +130,17 @@ public final class FontProviderUnicode implements FontProvider, IResourceManager
}
@Override
public ResourceLocation getTexture(char chr) {
public int getTexture(char chr) {
final int uniPage = chr / 256;
return getUnicodePageLocation(uniPage);
ResourceLocation rl = getUnicodePageLocation(uniPage);
final TextureManager tm = Minecraft.getMinecraft().getTextureManager();
ITextureObject tex = tm.getTexture(rl);
if (tex == null) {
tex = new SimpleTexture(rl);
tm.loadTexture(rl, tex);
tex = tm.getTexture(rl);
}
return tex.getGlTextureId();
}
@Override
@@ -2,14 +2,11 @@ package com.gtnewhorizons.angelica.client.font;
import com.google.common.collect.HashMultiset;
import com.gtnewhorizons.angelica.config.FontConfig;
import com.gtnewhorizons.angelica.mixins.interfaces.ResourceAccessor;
import cpw.mods.fml.client.SplashProgress;
import cpw.mods.fml.common.versioning.DefaultArtifactVersion;
import lombok.Getter;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.resources.DefaultResourcePack;
import net.minecraft.client.resources.SimpleReloadableResourceManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -25,12 +22,15 @@ import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class FontStrategist {
@Getter
private static final Font[] availableFonts;
private static final Map<Font,String> fontNameMap;
public static final Logger LOGGER = LogManager.getLogger("Angelica");
private static final boolean isSafeToUseAwtEnvironmentData = checkIsSafeToUseAwtEnvironmentData();
@@ -74,20 +74,13 @@ public class FontStrategist {
loadBundledFonts(fontSet);
availableFonts = fontSet.values().stream().sorted(Comparator.comparing(Font::getFontName)).toArray(Font[]::new);
fontNameMap = fontSet.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
availableFonts = fontSet.values().stream().sorted(Comparator.comparing(fontNameMap::get)).toArray(Font[]::new);
}
// create and add the resource pack that provides fonts
HashMap<String, File> packMap = new HashMap<>();
for (int i = 0; i < FontProviderCustom.ATLAS_COUNT; i++) {
packMap.put(FontProviderCustom.getPrimary().getAtlasResourceName(i), new File(FontProviderCustom.getPrimary().getAtlasFullPath(i)));
packMap.put(FontProviderCustom.getFallback().getAtlasResourceName(i), new File(FontProviderCustom.getFallback().getAtlasFullPath(i)));
}
Minecraft mc = Minecraft.getMinecraft();
DefaultResourcePack fontResourcePack = new DefaultResourcePack(packMap);
((ResourceAccessor) mc).angelica$getDefaultResourcePacks().add(fontResourcePack);
((SimpleReloadableResourceManager) mc.getResourceManager()).reloadResourcePack(fontResourcePack);
// reduces allocations
public static String getFontName(Font font) {
return fontNameMap.get(font);
}
private static boolean checkIsSafeToUseAwtEnvironmentData() {
@@ -181,10 +174,11 @@ public class FontStrategist {
FontProviderCustom.getPrimary().setFont(null);
FontProviderCustom.getFallback().setFont(null);
for (int i = 0; i < availableFonts.length; i++) {
if (Objects.equals(FontConfig.customFontNamePrimary, availableFonts[i].getFontName())) {
String fontName = getFontName(availableFonts[i]);
if (Objects.equals(FontConfig.customFontNamePrimary, fontName)) {
FontProviderCustom.getPrimary().reloadFont(i);
}
if (Objects.equals(FontConfig.customFontNameFallback, availableFonts[i].getFontName())) {
if (Objects.equals(FontConfig.customFontNameFallback, fontName)) {
FontProviderCustom.getFallback().reloadFont(i);
}
}
@@ -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);
}
}
}
@@ -21,6 +21,8 @@ import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static com.gtnewhorizons.angelica.client.font.FontStrategist.getFontName;
public class FontConfigScreen extends GuiScreen {
private static final Font[] availableFonts = FontStrategist.getAvailableFonts();
@@ -52,11 +54,12 @@ public class FontConfigScreen extends GuiScreen {
SliderClone.Option optShadowCopies = new SliderClone.Option(1, 8, 1);
SliderClone.Option optBoldCopies = new SliderClone.Option(1, 8, 1);
SliderClone.Option optGlyphAspect = new SliderClone.Option(-1, 1, 0.05f);
SliderClone.Option optGlyphScale = new SliderClone.Option(0.1f, 3, 0.05f);
SliderClone.Option optGlyphScale = new SliderClone.Option(0.25f, 2, 0.01f);
SliderClone.Option optWhitespaceScale = new SliderClone.Option(0.1f, 3, 0.05f);
SliderClone.Option optGlyphSpacing = new SliderClone.Option(-2f, 2f, 0.05f);
SliderClone.Option optFontAAMode = new SliderClone.Option(0, 2, 1);
SliderClone.Option optFontAAStrength = new SliderClone.Option(1, 24, 1);
SliderClone.Option optFontBrightness = new SliderClone.Option(0, 1, 0.05f);
SliderClone.Option optCustomFontScale = new SliderClone.Option(0.1f, 3, 0.05f);
SliderClone.Option optShadowOffsetUC = new SliderClone.Option(0, 2, 0.05f);
@@ -69,10 +72,11 @@ public class FontConfigScreen extends GuiScreen {
this.currentFallbackFontName = FontConfig.customFontNameFallback;
this.displayedFonts = new ArrayList<>(Arrays.asList(availableFonts));
for (int i = 0; i < availableFonts.length; i++) {
if (Objects.equals(this.currentPrimaryFontName, availableFonts[i].getFontName())) {
String fontName = getFontName(availableFonts[i]);
if (Objects.equals(this.currentPrimaryFontName, fontName)) {
selectedPrimaryFontListPos = i;
}
if (Objects.equals(this.currentFallbackFontName, availableFonts[i].getFontName())) {
if (Objects.equals(this.currentFallbackFontName, fontName)) {
selectedFallbackFontListPos = i;
}
}
@@ -217,6 +221,16 @@ public class FontConfigScreen extends GuiScreen {
.formatString("%.0f")
.build()
);
sliders.add(new SliderClone.SliderCloneBuilder()
.width(sliderWidth)
.height(sliderHeight)
.option(optFontBrightness)
.initialValue(FontConfig.fontBrightness)
.setter(value -> FontConfig.fontBrightness = value)
.langKey("options.angelica.fontconfig.font_brightness")
.formatString("%3.2f")
.build()
);
sliders.add(new SliderClone.SliderCloneBuilder()
.width(sliderWidth)
.height(sliderHeight)
@@ -278,11 +292,11 @@ public class FontConfigScreen extends GuiScreen {
int pos;
pos = selectedPrimaryFontListPos;
if (pos >= 0 && pos < displayedFonts.size()) {
FontConfig.customFontNamePrimary = displayedFonts.get(pos).getFontName();
FontConfig.customFontNamePrimary = getFontName(displayedFonts.get(pos));
}
pos = selectedFallbackFontListPos;
if (pos >= 0 && pos < displayedFonts.size()) {
FontConfig.customFontNameFallback = displayedFonts.get(pos).getFontName();
FontConfig.customFontNameFallback = getFontName(displayedFonts.get(pos));
}
FontStrategist.reloadCustomFontProviders();
@@ -338,9 +352,9 @@ public class FontConfigScreen extends GuiScreen {
};
}
private float lastMouseX = 0;
private float lastMouseY = 0;
private long lastStillTime = 0;
private int lastMouseX = 0;
private int lastMouseY = 0;
private long lastMovedTime = -1;
@Override
public void drawScreen(int mouseX, int mouseY, float delta) {
drawBackground(0);
@@ -366,8 +380,9 @@ public class FontConfigScreen extends GuiScreen {
this.searchBox.yPosition + this.searchBox.height / 2 - 4, 0xFFFFFF);
}
drawCenteredString(this.fontRendererObj, this.title, (int) (this.width * 0.5), 8, 0xFFFFFF);
drawCenteredString(this.fontRendererObj, I18n.format("options.angelica.fontconfig.currentfonts",
FontConfig.customFontNamePrimary, FontConfig.customFontNameFallback), (int) (this.width * 0.5), this.height - 92, 0xFFFFFF);
final int fontInfoTextY = 92;
String currentFonts = I18n.format("options.angelica.fontconfig.currentfonts", FontConfig.customFontNamePrimary, FontConfig.customFontNameFallback);
drawCenteredString(this.fontRendererObj, currentFonts, (int) (this.width * 0.5), this.height - fontInfoTextY, 0xFFFFFF);
if (this.testAreaInfo == null) {
this.testAreaInfo = this.fontRendererObj.listFormattedStringToWidth(
@@ -398,6 +413,18 @@ public class FontConfigScreen extends GuiScreen {
super.drawScreen(mouseX, mouseY, delta);
// tooltip code, can return
long timeMillis = System.currentTimeMillis();
if (mouseX != lastMouseX || mouseY != lastMouseY) {
lastMovedTime = timeMillis;
lastMouseX = mouseX;
lastMouseY = mouseY;
}
if (lastMovedTime + 500L > timeMillis) { return; }
for (GuiButton guiButton : buttonList) {
if (!(guiButton instanceof SliderClone slider)) { continue; }
final int top = slider.yPosition;
@@ -405,20 +432,18 @@ public class FontConfigScreen extends GuiScreen {
final int left = slider.xPosition;
final int right = slider.xPosition + slider.width;
if (mouseY < top || mouseY >= bot || mouseX < left || mouseX >= right) { continue; }
if (mouseX == lastMouseX && mouseY == lastMouseY) {
if (lastStillTime == 0) {
lastStillTime = System.currentTimeMillis();
}
if (lastStillTime + 500L < System.currentTimeMillis()) {
displayTooltip(mouseX, mouseY, slider.tooltipKey);
}
} else {
lastStillTime = 0;
}
break;
displayTooltip(mouseX, mouseY, slider.tooltipKey);
return;
}
int fontInfoTextWidth = this.fontRendererObj.getStringWidth(currentFonts);
int top = this.height - fontInfoTextY - 8;
int bot = this.height - fontInfoTextY + 16;
int left = this.width / 2 - fontInfoTextWidth / 2 - 8;
int right = this.width / 2 + fontInfoTextWidth / 2 + 8;
if (top <= mouseY && mouseY < bot && left <= mouseX && mouseX < right) {
displayTooltip(mouseX, mouseY, "options.angelica.fontconfig.font_selection_info");
}
lastMouseX = mouseX;
lastMouseY = mouseY;
}
private void displayTooltip(int x, int y, String langKey) {
@@ -477,16 +502,16 @@ public class FontConfigScreen extends GuiScreen {
if (search == null || search.isEmpty()) {
results = new ArrayList<>(Arrays.asList(availableFonts));
} else {
results = Arrays.stream(availableFonts).filter((font -> font.getFontName().toLowerCase().contains(search))).collect(Collectors.toCollection(ArrayList::new));
results = Arrays.stream(availableFonts).filter((font -> getFontName(font).toLowerCase().contains(search))).collect(Collectors.toCollection(ArrayList::new));
}
selectedPrimaryFontListPos = -1;
selectedFallbackFontListPos = -1;
for (int i = 0; i < results.size(); i++) {
if (Objects.equals(currentPrimaryFontName, results.get(i).getFontName())) {
if (Objects.equals(currentPrimaryFontName, getFontName(results.get(i)))) {
selectedPrimaryFontListPos = i;
}
if (Objects.equals(currentFallbackFontName, results.get(i).getFontName())) {
if (Objects.equals(currentFallbackFontName, getFontName(results.get(i)))) {
selectedFallbackFontListPos = i;
}
}
@@ -520,10 +545,10 @@ public class FontConfigScreen extends GuiScreen {
protected void onElemClicked(int index, boolean rightClick) {
if (!rightClick) {
selectedPrimaryFontListPos = index;
currentPrimaryFontName = displayedFonts.get(index).getFontName();
currentPrimaryFontName = getFontName(displayedFonts.get(index));
} else {
selectedFallbackFontListPos = index;
currentFallbackFontName = displayedFonts.get(index).getFontName();
currentFallbackFontName = getFontName(displayedFonts.get(index));
}
applyChanges(false);
}
@@ -566,7 +591,7 @@ public class FontConfigScreen extends GuiScreen {
if (index == selectedFallbackFontListPos) {
color &= 0x55ffff;
}
drawCenteredString(fontRendererObj, displayedFonts.get(index).getFontName(), this.width / 2, y + 1, color);
drawCenteredString(fontRendererObj, getFontName(displayedFonts.get(index)), this.width / 2, y + 1, color);
}
}
}
@@ -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;
@@ -64,10 +64,15 @@ public class FontConfig {
public static int fontAAMode;
@Config.Comment("Affects font antialiasing sample spacing. Higher values increase blur.")
@Config.DefaultInt(7)
@Config.DefaultInt(6)
@Config.RangeInt(min = 1, max = 24)
public static int fontAAStrength;
@Config.Comment("Affects the thickness of antialiased fonts. Can improve visibility of thin text.")
@Config.DefaultFloat(0.6f)
@Config.RangeFloat(min = 0, max = 1)
public static float fontBrightness;
@Config.Comment("Custom font scale multiplier, for bugfixes.")
@Config.DefaultFloat(1.5F)
@Config.RangeFloat(min = 0.1F, max = 3F)
@@ -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
@@ -0,0 +1,32 @@
package com.gtnewhorizons.angelica.loading.fml.transformers;
import com.gtnewhorizons.angelica.loading.AngelicaClientTweaker;
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
import com.gtnewhorizons.angelica.loading.shared.transformers.DarkModeFontTransform;
import net.minecraft.launchwrapper.IClassTransformer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
public class DarkModeFontTransformer implements IClassTransformer {
private final DarkModeFontTransform inner = new DarkModeFontTransform();
@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
if (DarkModeFontTransform.classesToTransform.containsKey(transformedName)) {
final ClassReader cr = new ClassReader(basicClass);
final ClassNode cn = new ClassNode();
cr.accept(cn, 0);
if (!inner.transformClassNode(cn, transformedName, AngelicaClientTweaker.isObfEnv())) {
return basicClass;
}
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cn.accept(cw);
final byte[] bytes = cw.toByteArray();
AngelicaClassDump.dumpClass(transformedName, basicClass, bytes, this);
return bytes;
}
return basicClass;
}
}
@@ -64,9 +64,14 @@ public class AngelicaLateTweaker implements ITweaker {
FMLRelaunchLog.warning("[Angelica] Failed to remove EarlyRedirectorTransformer: %s", e.getMessage());
}
final String transformer = "com.gtnewhorizons.angelica.loading.fml.transformers.AngelicaRedirectorTransformer";
FMLRelaunchLog.finer("Registering transformer %s", transformer);
Launch.classLoader.registerTransformer(transformer);
final String[] transformers = {
"com.gtnewhorizons.angelica.loading.fml.transformers.AngelicaRedirectorTransformer",
"com.gtnewhorizons.angelica.loading.fml.transformers.DarkModeFontTransformer"
};
for (String transformer : transformers) {
FMLRelaunchLog.finer("Registering transformer %s", transformer);
Launch.classLoader.registerTransformer(transformer);
}
return new String[0];
}
}
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.loading.rfb;
import com.gtnewhorizons.angelica.loading.rfb.transformers.RFBAngelicaRedirector;
import com.gtnewhorizons.angelica.loading.rfb.transformers.RFBCeleritasBlockTransformer;
import com.gtnewhorizons.angelica.loading.rfb.transformers.RFBDarkModeFontTransformer;
import com.gtnewhorizons.angelica.loading.rfb.transformers.RFBIsbrhTessellatorAbuseTransformer;
import com.gtnewhorizons.retrofuturabootstrap.api.PluginContext;
import com.gtnewhorizons.retrofuturabootstrap.api.RetroFuturaBootstrap;
@@ -30,7 +31,8 @@ public class AngelicaRfbPlugin implements RfbPlugin {
return new RfbClassTransformer[] {
new RFBAngelicaRedirector(),
new RFBCeleritasBlockTransformer(isObf),
new RFBIsbrhTessellatorAbuseTransformer(isObf)
new RFBIsbrhTessellatorAbuseTransformer(isObf),
new RFBDarkModeFontTransformer(isObf)
};
}
}
@@ -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
@@ -0,0 +1,61 @@
package com.gtnewhorizons.angelica.loading.rfb.transformers;
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
import com.gtnewhorizons.angelica.loading.shared.transformers.DarkModeFontTransform;
import com.gtnewhorizons.retrofuturabootstrap.api.ClassNodeHandle;
import com.gtnewhorizons.retrofuturabootstrap.api.ExtensibleClassLoader;
import com.gtnewhorizons.retrofuturabootstrap.api.RfbClassTransformer;
import org.intellij.lang.annotations.Pattern;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.jar.Manifest;
public class RFBDarkModeFontTransformer implements RfbClassTransformer {
private final DarkModeFontTransform inner;
private final boolean isObf;
public RFBDarkModeFontTransformer(boolean isObf) {
this.inner = new DarkModeFontTransform();
this.isObf = isObf;
}
@Pattern("[a-z0-9-]+")
@Override
public @NotNull String id() {
return "dark-mode-font";
}
@Override
public @NotNull String @Nullable [] sortAfter() {
return new String[] {"*", "mixin:mixin"};
}
@Override
public boolean shouldTransformClass(@NotNull ExtensibleClassLoader classLoader, @NotNull RfbClassTransformer.Context context,
@Nullable Manifest manifest, @NotNull String className, @NotNull ClassNodeHandle classNode) {
if (!classNode.isPresent()) {
return false;
}
return DarkModeFontTransform.classesToTransform.containsKey(className);
}
@Override
public boolean transformClassIfNeeded(@NotNull ExtensibleClassLoader classLoader, @NotNull RfbClassTransformer.Context context,
@Nullable Manifest manifest, @NotNull String className, @NotNull ClassNodeHandle classNode) {
final boolean changed = inner.transformClassNode(classNode.getNode(), className, this.isObf);
if (changed) {
classNode.computeFrames();
AngelicaClassDump.dumpRFBClass(className, classNode, this);
}
return changed;
}
@Override
public void transformClass(@NotNull ExtensibleClassLoader classLoader, @NotNull RfbClassTransformer.Context context,
@Nullable Manifest manifest, @NotNull String className, @NotNull ClassNodeHandle classNode) {
transformClassIfNeeded(classLoader, context, manifest, className, classNode);
}
}
@@ -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.
@@ -0,0 +1,443 @@
package com.gtnewhorizons.angelica.loading.shared.transformers;
import com.gtnewhorizons.angelica.client.font.BatchingFontRenderer;
import com.gtnewhorizons.angelica.client.font.DarkModeUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DarkModeFontTransform {
private static final Logger LOGGER = LogManager.getLogger("DarkModeFontTransformer");
private static final String BATCHINGFONTRENDERER = "com/gtnewhorizons/angelica/client/font/BatchingFontRenderer";
public static class MethodInfo {
public final String className;
public final String classNameSlash;
private final String name;
private final String obfName;
public final String desc;
// Accepts an AT entry, examples:
// net.minecraft.client.gui.inventory.GuiContainer func_73863_a(IIF)V # drawScreen
// vswe.stevescarts.Interfaces.GuiNEIKiller drawScreen(IIF)V
public MethodInfo(@NotNull String atEntry) {
int firstSpace = atEntry.indexOf(' ');
int descStart = atEntry.indexOf('(');
int hashIndex = atEntry.indexOf('#');
String className = atEntry.substring(0, firstSpace);
String identifierBeforeDesc = atEntry.substring(firstSpace, descStart).trim();
String name, obfName, desc;
if (hashIndex != -1) {
obfName = identifierBeforeDesc;
desc = atEntry.substring(descStart, hashIndex).trim();
name = atEntry.substring(hashIndex + 1).trim();
} else {
name = identifierBeforeDesc;
desc = atEntry.substring(descStart);
obfName = null;
}
this(className, name, obfName, desc);
}
public MethodInfo(@NotNull String className, @NotNull String name, @Nullable String obfName, @NotNull String desc) {
this.className = className;
this.classNameSlash = className.replace('.', '/');
this.name = name;
this.obfName = obfName;
this.desc = desc;
}
public String getName(boolean isObf) {
if (obfName == null) { return name; }
if (isObf) { return obfName; }
return name;
}
@Override
public String toString() {
return className + "#" + name + desc;
}
}
/**
* Data used to tell the transformer that it should use some calls
* ({@link BatchingFontRenderer#enterRecolorSection(boolean)}, {@link BatchingFontRenderer#exitRecolorSection(boolean)})
* to mark areas where text should be recolored according to the rules in {@link DarkModeUtils}.
* Can either apply to an entire method (placing markers in it) or a specific method call in some method (wrapping the call).
* A button target wraps the same way, but with a button section that also carries the button's three text colors.
*/
public record RecolorTarget(@NotNull MethodInfo method, @Nullable MethodInfo calledMethod, boolean recolorEnabled, @Nullable ButtonColors buttonColors) {
public RecolorTarget(MethodInfo method, MethodInfo calledMethod, boolean recolorEnabled) { this(method, calledMethod, recolorEnabled, null); }
public static RecolorTarget includeMethod(MethodInfo method) { return new RecolorTarget(method, null, true); }
public static RecolorTarget excludeMethod(MethodInfo method) { return new RecolorTarget(method, null, false); }
public static RecolorTarget includeMethodCall(MethodInfo callingMethod, MethodInfo calledMethod) { return new RecolorTarget(callingMethod, calledMethod, true); }
public static RecolorTarget excludeMethodCall(MethodInfo callingMethod, MethodInfo calledMethod) { return new RecolorTarget(callingMethod, calledMethod, false); }
public static RecolorTarget includeButtonMethod(MethodInfo method, int enabledColor, int hoveredColor, int disabledColor) {
return new RecolorTarget(method, null, true, new ButtonColors(enabledColor, hoveredColor, disabledColor));
}
public static RecolorTarget includeButtonMethodCall(MethodInfo callingMethod, MethodInfo calledMethod, int enabledColor, int hoveredColor, int disabledColor) {
return new RecolorTarget(callingMethod, calledMethod, true, new ButtonColors(enabledColor, hoveredColor, disabledColor));
}
public static RecolorTarget includeButtonMethod(MethodInfo method) {
return new RecolorTarget(method, null, true, ButtonColors.VANILLA);
}
public static RecolorTarget includeButtonMethodCall(MethodInfo callingMethod, MethodInfo calledMethod) {
return new RecolorTarget(callingMethod, calledMethod, true, ButtonColors.VANILLA);
}
public boolean targetsCall() { return calledMethod != null; }
}
/** The colors a button draws its text in for each of its three states. VANILLA is what vanilla buttons use. */
public record ButtonColors(int enabled, int hovered, int disabled) {
public static final ButtonColors VANILLA = new ButtonColors(0xE0E0E0, 0xFFFFA0, 0xA0A0A0);
}
// A target method must contain the obf name if it's
// a) a vanilla method
// or b) a modded method belonging to a class that extends (or is) a vanilla class that declares said method (so, an override).
// Notably, "GuiNEIKiller" does not override drawGuiContainerForegroundLayer because it doesn't inherit GuiContainer.
public static final RecolorTarget[] recolorTargets = {
//region Include Methods
// Vanilla methods. Many mods use these; several draw text in the "background" layer...
RecolorTarget.includeMethodCall(
new MethodInfo("net.minecraft.client.gui.inventory.GuiContainer func_73863_a(IIF)V # drawScreen"),
new MethodInfo("net.minecraft.client.gui.inventory.GuiContainer func_146979_b(II)V # drawGuiContainerForegroundLayer")
),
RecolorTarget.includeMethodCall(
new MethodInfo("net.minecraft.client.gui.inventory.GuiContainer func_73863_a(IIF)V # drawScreen"),
new MethodInfo("net.minecraft.client.gui.inventory.GuiContainer func_146976_a(FII)V # drawGuiContainerBackgroundLayer")
),
RecolorTarget.excludeMethod(
new MethodInfo("net.minecraft.client.renderer.entity.RenderItem func_94148_a(Lnet/minecraft/client/gui/FontRenderer;Lnet/minecraft/client/renderer/texture/TextureManager;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V # renderItemOverlayIntoGUI")
),
// Modded stuff. Some mods copy-paste vanilla code.
// Avaritia Addons
RecolorTarget.includeMethodCall(
new MethodInfo("wanion.avaritiaddons.block.chest.infinity.GuiInfinityChest func_73863_a(IIF)V # drawScreen"),
new MethodInfo("wanion.avaritiaddons.block.chest.infinity.GuiInfinityChest func_146979_b(II)V # drawGuiContainerForegroundLayer")
),
// Better Achievements
RecolorTarget.includeMethodCall(
new MethodInfo("betterachievements.gui.GuiBetterAchievements func_73863_a(IIF)V # drawScreen"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_78276_b(Ljava/lang/String;III)I # drawString")
),
// ModularUI 1
RecolorTarget.includeMethodCall(
new MethodInfo("com.gtnewhorizons.modularui.common.internal.wrapper.ModularGui func_73863_a(IIF)V # drawScreen"),
new MethodInfo("com.gtnewhorizons.modularui.common.internal.wrapper.ModularGui func_146976_a(FII)V # drawGuiContainerBackgroundLayer")
),
// Nutrition
RecolorTarget.includeMethod(
new MethodInfo("ca.wescook.nutrition.gui.GuiLabelNutrition drawLabel(Lnet/minecraft/client/Minecraft;II)V")
),
// Railcraft
RecolorTarget.includeMethodCall(
new MethodInfo("mods.railcraft.client.gui.GuiBasic func_73863_a(IIF)V # drawScreen"),
new MethodInfo("mods.railcraft.client.gui.GuiTools drawCenteredString(Lnet/minecraft/client/gui/FontRenderer;Ljava/lang/String;II)V")
),
RecolorTarget.includeMethodCall(
new MethodInfo("mods.railcraft.client.gui.GuiBasic func_73863_a(IIF)V # drawScreen"),
new MethodInfo("mods.railcraft.client.gui.GuiBasic drawExtras(IIF)V")
),
// Galacticraft
RecolorTarget.includeMethodCall(
new MethodInfo("micdoodle8.mods.galacticraft.core.client.gui.element.GuiElementCheckbox func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_85187_a(Ljava/lang/String;IIIZ)I # drawString")
),
// Steve's Factory Manager
RecolorTarget.includeMethodCall(
new MethodInfo("vswe.stevesfactory.interfaces.GuiBase drawString(Ljava/lang/String;IIFI)V"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_78276_b(Ljava/lang/String;III)I # drawString")
),
// Steve's Carts 2
RecolorTarget.includeMethodCall(
new MethodInfo("vswe.stevescarts.Interfaces.GuiNEIKiller func_73863_a(IIF)V # drawScreen"),
new MethodInfo("vswe.stevescarts.Interfaces.GuiNEIKiller drawGuiContainerForegroundLayer(II)V")
),
// Binnie
RecolorTarget.includeMethodCall(
new MethodInfo("binnie.core.craftgui.minecraft.GuiCraftGUI func_73863_a(IIF)V # drawScreen"),
new MethodInfo("binnie.core.craftgui.minecraft.Window render()V")
),
// Malisis Core (Malisis' Doors)
RecolorTarget.includeMethodCall(
new MethodInfo("net.malisis.core.client.gui.MalisisGui func_73863_a(IIF)V # drawScreen"),
new MethodInfo("net.malisis.core.client.gui.GuiRenderer drawScreen(Lnet/malisis/core/client/gui/component/container/UIContainer;IIF)V")
),
// Nuclear Control
RecolorTarget.includeMethod(
new MethodInfo("shedar.mods.ic2.nuclearcontrol.gui.controls.GuiHowlerAlarmSlider func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton")
),
RecolorTarget.includeMethod(
new MethodInfo("shedar.mods.ic2.nuclearcontrol.gui.controls.GuiHowlerAlarmListBox func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton")
),
RecolorTarget.includeMethod(
new MethodInfo("shedar.mods.ic2.nuclearcontrol.gui.GuiScreenColor func_73863_a(IIF)V # drawScreen")
),
// Draconic Evolution
RecolorTarget.includeMethodCall(
new MethodInfo("com.brandon3055.draconicevolution.client.gui.GUIFlowGate func_73863_a(IIF)V # drawScreen"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_78276_b(Ljava/lang/String;III)I # drawString")
),
RecolorTarget.includeMethodCall(
new MethodInfo("com.brandon3055.draconicevolution.client.gui.GUIParticleGenerator func_73863_a(IIF)V # drawScreen"),
new MethodInfo("com.brandon3055.draconicevolution.client.gui.GUIParticleGenerator$IProperty drawLabel(Lnet/minecraft/client/gui/FontRenderer;II)V")
),
RecolorTarget.includeMethodCall(
new MethodInfo("com.brandon3055.draconicevolution.client.gui.GUIParticleGenerator func_73863_a(IIF)V # drawScreen"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_78279_b(Ljava/lang/String;IIII)V # drawSplitString")
),
// Logistics Pipes
RecolorTarget.includeMethodCall(
new MethodInfo("logisticspipes.utils.gui.SubGuiScreen func_73863_a(IIF)V # drawScreen"),
new MethodInfo("logisticspipes.utils.gui.SubGuiScreen drawGuiContainerForegroundLayer(II)V")
),
// Minecraft-Backpack-Mod
RecolorTarget.includeMethod(
new MethodInfo("de.eydamos.guiadvanced.form.Label draw(Lnet/minecraft/client/Minecraft;IIF)V")
),
//endregion
//region Exclude Methods
// Applied Energistics 2
RecolorTarget.excludeMethod(
new MethodInfo("appeng.client.render.StackSizeRenderer drawStackSize(IILjava/lang/String;Lnet/minecraft/client/gui/FontRenderer;Lappeng/api/config/TerminalFontSize;)V")
),
// GregTech
RecolorTarget.excludeMethodCall(
new MethodInfo("gregtech.common.render.FluidDisplayStackRenderer renderItem(Lnet/minecraftforge/client/IItemRenderer$ItemRenderType;Lnet/minecraft/item/ItemStack;[Ljava/lang/Object;)V"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_85187_a(Ljava/lang/String;IIIZ)I # drawString")
),
RecolorTarget.excludeMethodCall(
new MethodInfo("gregtech.api.recipe.RecipeMapFrontend drawNEIOverlayText(Ljava/lang/String;Lcodechicken/nei/PositionedStack;IFZLcom/gtnewhorizons/modularui/api/math/Alignment;)V"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_85187_a(Ljava/lang/String;IIIZ)I # drawString")
),
// GT NEI Ore Plugin
RecolorTarget.excludeMethodCall(
new MethodInfo("gtneioreplugin.plugin.renderer.ItemDimensionDisplayRenderer renderItem(Lnet/minecraftforge/client/IItemRenderer$ItemRenderType;Lnet/minecraft/item/ItemStack;[Ljava/lang/Object;)V"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_85187_a(Ljava/lang/String;IIIZ)I # drawString")
),
// NotEnoughItems
RecolorTarget.excludeMethod(
new MethodInfo("codechicken.nei.guihook.GuiContainerManager drawBigStackSize(IILjava/lang/String;F)V")
),
// NEI Custom Diagram
RecolorTarget.excludeMethod(
new MethodInfo("com.github.dcysteine.neicustomdiagram.api.draw.Draw drawStackSize(ILcom/github/dcysteine/neicustomdiagram/api/draw/Point;)V")
),
// BiblioCraft
RecolorTarget.excludeMethodCall(
new MethodInfo("jds.bibliocraft.gui.GuiFancySign func_146976_a(FII)V # drawGuiContainerBackgroundLayer"),
new MethodInfo("jds.bibliocraft.gui.GuiBiblioTextField drawTextBox()V")
),
// Harvestcraft
RecolorTarget.excludeMethodCall(
new MethodInfo("com.pam.harvestcraft.TrapRecipeHandler drawExtras(I)V"),
new MethodInfo("codechicken.lib.gui.GuiDraw drawString(Ljava/lang/String;III)V")
),
// Thaumcraft
RecolorTarget.excludeMethod(
new MethodInfo("thaumcraft.client.gui.GuiResearchTable drawAspects(II)V")
),
// Steve's Carts 2
RecolorTarget.excludeMethod(
new MethodInfo("vswe.stevescarts.Interfaces.GuiBase drawMouseOver(Ljava/lang/String;II)V")
),
//endregion
//region Buttons
// Buttons. The three colors are the button's own: enabled, hovered, disabled.
// Vanilla
RecolorTarget.includeButtonMethodCall(
new MethodInfo("net.minecraft.client.gui.GuiButton func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton"),
new MethodInfo("net.minecraft.client.gui.GuiButton func_73732_a(Lnet/minecraft/client/gui/FontRenderer;Ljava/lang/String;III)V # drawCenteredString")
),
// CodeChickenCore
RecolorTarget.includeButtonMethod(
new MethodInfo("codechicken.core.gui.GuiCCButton drawText(II)V")
),
// NotEnoughItems
RecolorTarget.includeButtonMethodCall(
new MethodInfo("codechicken.nei.LayoutStyleMinecraft drawButton(Lcodechicken/nei/Button;II)V"),
new MethodInfo("codechicken.lib.gui.GuiDraw drawStringC(Ljava/lang/String;III)V"),
0xE0E0E0, 0xFFFFA0, 0x601010
),
RecolorTarget.includeButtonMethod(
new MethodInfo("codechicken.nei.GuiNEIButton drawContent(Lnet/minecraft/client/Minecraft;IIZ)V")
),
RecolorTarget.includeButtonMethodCall(
new MethodInfo("codechicken.nei.config.OptionButton drawButton(II)V"),
new MethodInfo("codechicken.lib.gui.GuiDraw drawStringC(Ljava/lang/String;IIIII)V")
),
RecolorTarget.includeButtonMethodCall(
new MethodInfo("codechicken.nei.config.DataDumper drawButton(IILcodechicken/lib/vec/Rectangle4i;Ljava/lang/String;)V"),
new MethodInfo("codechicken.lib.gui.GuiDraw drawStringC(Ljava/lang/String;IIIII)V")
),
// Malisis Core (Malisis' Doors)
RecolorTarget.includeButtonMethodCall(
new MethodInfo("net.malisis.core.client.gui.component.interaction.UIButton drawForeground(Lnet/malisis/core/client/gui/GuiRenderer;IIF)V"),
new MethodInfo("net.malisis.core.client.gui.GuiRenderer drawText(Lnet/malisis/core/renderer/font/MalisisFont;Ljava/lang/String;FFFLnet/malisis/core/renderer/font/FontRenderOptions;)V"),
0xFFFFFF, 0xFFFFA0, 0xA0A0A0
),
// MrTJPCore (ProjectRed)
RecolorTarget.includeButtonMethodCall(
new MethodInfo("mrtjp.core.gui.TButtonText$class drawButton(Lmrtjp/core/gui/TButtonText;Z)V"),
new MethodInfo("codechicken.lib.gui.GuiDraw drawStringC(Ljava/lang/String;III)V")
),
// LittleTiles (CreativeCore)
RecolorTarget.includeButtonMethodCall(
new MethodInfo("com.creativemd.creativecore.common.gui.controls.GuiButton drawControl(Lnet/minecraft/client/gui/FontRenderer;)V"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_78261_a(Ljava/lang/String;III)I # drawStringWithShadow")
),
// Logistics Pipes
RecolorTarget.includeButtonMethod(
new MethodInfo("logisticspipes.utils.gui.SmallGuiButton func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton")
),
// Nuclear Control
RecolorTarget.includeButtonMethodCall(
new MethodInfo("shedar.mods.ic2.nuclearcontrol.gui.controls.CompactButton func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton"),
new MethodInfo("net.minecraft.client.gui.FontRenderer func_78276_b(Ljava/lang/String;III)I # drawString"),
0x303030, 0x303030, 0x303030
),
// Forge
RecolorTarget.includeButtonMethod(
new MethodInfo("cpw.mods.fml.client.config.GuiButtonExt func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton")
),
RecolorTarget.includeButtonMethod(
new MethodInfo("cpw.mods.fml.client.config.GuiUnicodeGlyphButton func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton")
),
// Tinkers Mechworks
RecolorTarget.includeButtonMethod(
new MethodInfo("tmechworks.client.gui.AdvDrawbridgeButton func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton")
),
// RemoteIO
RecolorTarget.includeButtonMethod(
new MethodInfo("remoteio.client.gui.button.GuiBetterButton func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton")
),
// Railcraft
RecolorTarget.includeButtonMethod(
new MethodInfo("mods.railcraft.client.gui.buttons.GuiBetterButton func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton"),
0xE0E0E0, 0xFFFFA0, 0x777777
),
RecolorTarget.includeButtonMethod(
new MethodInfo("mods.railcraft.client.gui.buttons.GuiMultiButton func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton")
),
// BuildCraft
RecolorTarget.includeButtonMethod(
new MethodInfo("buildcraft.core.lib.gui.buttons.GuiBetterButton func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton")
),
// Minecraft-Backpack-Mod
RecolorTarget.includeButtonMethodCall(
new MethodInfo("de.eydamos.guiadvanced.form.Button func_146112_a(Lnet/minecraft/client/Minecraft;II)V # drawButton"),
new MethodInfo("de.eydamos.guiadvanced.form.Button func_73732_a(Lnet/minecraft/client/gui/FontRenderer;Ljava/lang/String;III)V # drawCenteredString")
),
//endregion
};
public static final Map<String, List<RecolorTarget>> classesToTransform = new HashMap<>();
static {
for (RecolorTarget target : recolorTargets) {
classesToTransform.computeIfAbsent(target.method.className, _ -> new ArrayList<>()).add(target);
}
}
public boolean transformClassNode(ClassNode cn, String className, boolean isObf) {
boolean changed = false;
List<RecolorTarget> targets = classesToTransform.get(className);
if (targets == null) { return false; }
for (RecolorTarget target : targets) {
for (MethodNode mn : cn.methods) {
if (!mn.name.equals(target.method.getName(isObf))) { continue; }
if (!mn.desc.equals(target.method.desc)) { continue; }
if (target.targetsCall()) {
changed = markRecolorCallScoped(mn, target.calledMethod, isObf, target.recolorEnabled, target.buttonColors) || changed;
} else {
changed = markRecolorMethodScoped(mn, target.method, target.recolorEnabled, target.buttonColors) || changed;
}
}
}
return changed;
}
private static void insertEnterSection(InsnList insnList, AbstractInsnNode before, boolean recolorEnabled, @Nullable ButtonColors buttonColors) {
if (buttonColors == null) {
insnList.insertBefore(before, new InsnNode(recolorEnabled ? Opcodes.ICONST_1 : Opcodes.ICONST_0));
insnList.insertBefore(before, new MethodInsnNode(Opcodes.INVOKESTATIC, BATCHINGFONTRENDERER, "enterRecolorSection", "(Z)Z", false));
} else {
insnList.insertBefore(before, new LdcInsnNode(buttonColors.enabled()));
insnList.insertBefore(before, new LdcInsnNode(buttonColors.hovered()));
insnList.insertBefore(before, new LdcInsnNode(buttonColors.disabled()));
insnList.insertBefore(before, new MethodInsnNode(Opcodes.INVOKESTATIC, BATCHINGFONTRENDERER, "enterButtonSection", "(III)Z", false));
}
}
private static MethodInsnNode exitSectionCall(@Nullable ButtonColors buttonColors) {
return new MethodInsnNode(Opcodes.INVOKESTATIC, BATCHINGFONTRENDERER, buttonColors == null ? "exitRecolorSection" : "exitButtonSection", "(Z)V", false);
}
private boolean markRecolorCallScoped(MethodNode mn, MethodInfo calledMethod, boolean isObf, boolean recolorEnabled, @Nullable ButtonColors buttonColors) {
boolean changed = false;
InsnList insnList = mn.instructions;
for (AbstractInsnNode insn = insnList.getFirst(); insn != null; insn = insn.getNext()) {
if (insn instanceof MethodInsnNode min) {
if (!min.owner.equals(calledMethod.classNameSlash)) { continue; }
if (!min.name.equals(calledMethod.getName(isObf))) { continue; }
if (!min.desc.equals(calledMethod.desc)) { continue; }
int maxLocals = mn.maxLocals;
insertEnterSection(insnList, min, recolorEnabled, buttonColors);
insnList.insertBefore(min, new VarInsnNode(Opcodes.ISTORE, maxLocals));
insnList.insert(min, exitSectionCall(buttonColors));
insnList.insert(min, new VarInsnNode(Opcodes.ILOAD, maxLocals));
LOGGER.info("Added {}-recolor flags at call site of {}", recolorEnabled ? "enable" : "disable", calledMethod.toString());
changed = true;
}
}
return changed;
}
private boolean markRecolorMethodScoped(MethodNode mn, MethodInfo method, boolean recolorEnabled, @Nullable ButtonColors buttonColors) {
InsnList insnList = mn.instructions;
if (insnList.size() == 0) { return false; }
AbstractInsnNode firstInsn = insnList.getFirst();
int maxLocals = mn.maxLocals;
insertEnterSection(insnList, firstInsn, recolorEnabled, buttonColors);
insnList.insertBefore(firstInsn, new VarInsnNode(Opcodes.ISTORE, maxLocals));
for (AbstractInsnNode insn = firstInsn; insn != null; insn = insn.getNext()) {
int opcode = insn.getOpcode();
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
insnList.insertBefore(insn, new VarInsnNode(Opcodes.ILOAD, maxLocals));
insnList.insertBefore(insn, exitSectionCall(buttonColors));
}
}
LOGGER.info("Added {}-recolor flags in {}", recolorEnabled ? "enable" : "disable", method.toString());
return true;
}
}
@@ -150,7 +150,6 @@ public enum Mixins implements IMixins {
.addClientMixins(
"angelica.fontrenderer.MixinGuiIngameForge"
, "angelica.fontrenderer.MixinFontRenderer"
, "angelica.fontrenderer.MixinMCResourceAccessor"
)
),
@@ -215,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)
@@ -1,7 +0,0 @@
package com.gtnewhorizons.angelica.mixins.interfaces;
import java.util.List;
public interface ResourceAccessor {
List angelica$getDefaultResourcePacks();
}
@@ -37,6 +37,7 @@ import biomesoplenty.api.content.BOPCBlocks;
import com.google.common.base.Objects;
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager;
import com.gtnewhorizons.angelica.client.font.DarkModeUtils;
import com.gtnewhorizons.angelica.commands.AngelicaCommand;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.common.BlockError;
@@ -154,6 +155,7 @@ public final class ClientProxy extends CommonProxy {
HUDCaching.init();
}
CeleritasSetup.ensureInitialized();
DarkModeUtils.init();
final SodiumGameOptions opts = options();
if (opts.advanced.vsyncMode == null) {
opts.advanced.vsyncMode = FrameRateOptions.defaultMode();
@@ -57,8 +57,8 @@ public class EmissiveTextureAutoloader {
final IResourceManager resMan = Minecraft.getMinecraft().getResourceManager();
if (resMan instanceof SimpleReloadableResourceManager simple) {
FallbackResourceManager fallback = simple.domainResourceManagers.get(full.getResourceDomain());
if (fallback != null) {
final Map<String, ?> domainManagers = simple.domainResourceManagers;
if (domainManagers.get(full.getResourceDomain()) instanceof FallbackResourceManager fallback) {
for (IResourcePack rp : fallback.resourcePacks) {
if (rp != null && rp.resourceExists(full)) {
return true;
@@ -17,4 +17,8 @@ public interface SpriteExtension {
int angelica$getGutterWidth();
void angelica$setGutterWidth(int gutter);
void angelica$setUploaded(boolean uploaded);
boolean angelica$takeUploaded();
}
@@ -59,6 +59,30 @@ public final class BakedTransformCapture extends DirectTessellator {
}
}
// While capturing, Tessellator.instance is this tessellator, so vanilla code (e.g. RenderBlocks.renderBlockAsItem)
// writes vertices directly instead of going through interceptDraw; bake the transform in here as well.
@Override
public void addVertex(double x, double y, double z) {
final Matrix4fc direct = transform == null ? delta.deltaOrNull(deltaMatrix) : null;
if (direct == null) {
super.addVertex(x, y, z);
return;
}
scratch.set((float) (x + xOffset), (float) (y + yOffset), (float) (z + zOffset)).mulPosition(direct);
super.addVertex(scratch.x - xOffset, scratch.y - yOffset, scratch.z - zOffset);
}
@Override
public void setNormal(float nx, float ny, float nz) {
final Matrix4fc direct = transform == null ? delta.deltaOrNull(deltaMatrix) : null;
if (direct == null) {
super.setNormal(nx, ny, nz);
return;
}
scratch.set(nx, ny, nz).mulDirection(direct).normalize();
super.setNormal(scratch.x, scratch.y, scratch.z);
}
@Override
protected long writeVertexData(VertexFormat format, int[] rawBuffer, int rawBufferIndex) {
if (transform != null) {
@@ -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,251 @@
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<>();
final Map<String, ?> domainManagers = manager.domainResourceManagers;
for (Map.Entry<String, ?> entry : domainManagers.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, Object domainManager, Trust trust,
Map<String, List<String>> untrustedDomains) {
if (!(domainManager instanceof FallbackResourceManager domain) || 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);
}
}
}
@@ -43,6 +43,10 @@ public class Zoom {
return zoomEnabled;
}
public static boolean isCinematicCameraEnabled(){
return (boolean)Settings.ZOOM_CINEMATIC.option.getStore();
}
public static void modifyZoom(int eventDWheel) {
if (eventDWheel == 0) return;
zoom = MathHelper.clamp_float((float) (zoom * Math.pow(ZOOM_STEP, Integer.signum(eventDWheel))), ZOOM_MIN, ZOOM_MAX);
@@ -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 {
@@ -52,12 +52,10 @@ public class TexturePackAPI {
List<IResourcePack> resourcePacks = new ArrayList<>();
IResourceManager resourceManager = Minecraft.getMinecraft().getResourceManager();
if (resourceManager instanceof SimpleReloadableResourceManager) {
@SuppressWarnings("unchecked")
Set<Map.Entry<String, FallbackResourceManager>> entrySet = ((SimpleReloadableResourceManager) resourceManager).domainResourceManagers
.entrySet();
for (Map.Entry<String, FallbackResourceManager> entry : entrySet) {
if (namespace == null || namespace.equals(entry.getKey())) {
List<IResourcePack> packs = entry.getValue().resourcePacks;
final Map<String, ?> domainManagers = ((SimpleReloadableResourceManager) resourceManager).domainResourceManagers;
for (Map.Entry<String, ?> entry : domainManagers.entrySet()) {
if ((namespace == null || namespace.equals(entry.getKey())) && entry.getValue() instanceof FallbackResourceManager fallback) {
List<IResourcePack> packs = fallback.resourcePacks;
if (packs != null) {
resourcePacks.removeAll(packs);
resourcePacks.addAll(packs);
+2 -1
View File
@@ -176,7 +176,8 @@ public enum Settings {
},
VOID_FOG(new NotFineOptionTickBox(false, OptionImpact.LOW)),
ZOOM_SMOOTH(new NotFineOptionTickBox(true, OptionImpact.LOW)),
ZOOM_SMOOTH_SPEED(new NotFineOptionSlider(20, 1, 30, 1, OptionImpact.LOW));
ZOOM_SMOOTH_SPEED(new NotFineOptionSlider(20, 1, 30, 1, OptionImpact.LOW)),
ZOOM_CINEMATIC(new NotFineOptionTickBox(true, OptionImpact.LOW));
public final NotFineOption<?> option;
@@ -168,6 +168,7 @@ public class NotFineGameOptionPages {
groups.add(OptionGroup.createBuilder()
.add(Settings.ZOOM_SMOOTH.option)
.add(Settings.ZOOM_SMOOTH_SPEED.option)
.add(Settings.ZOOM_CINEMATIC.option)
.build());
}
@@ -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)
@@ -792,6 +830,7 @@ public class SodiumGameOptionPages {
groups.add(OptionGroup.createBuilder()
.add(Settings.ZOOM_SMOOTH.option)
.add(Settings.ZOOM_SMOOTH_SPEED.option)
.add(Settings.ZOOM_CINEMATIC.option)
.build());
}
+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);
}
}
@@ -612,8 +612,8 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
this.shadowRenderer = new ShadowRenderer(programs.getShadow().orElse(null),
programs.getPackDirectives(), shadowRenderTargets, shadowCompositeRenderer,
() -> usesSampler(IrisSamplers.USAGE_SHADOWTEX1));
Program shadowProgram = table.match(RenderCondition.SHADOW, InputAvailability.of(true, true)).getProgram();
Program shadowWaterProgram = table.match(RenderCondition.SHADOW_TRANSLUCENT, InputAvailability.of(true, true)).getProgram();
Program shadowProgram = table.match(RenderCondition.SHADOW, SHADOW_INPUTS).getProgram();
Program shadowWaterProgram = table.match(RenderCondition.SHADOW_TRANSLUCENT, SHADOW_INPUTS).getProgram();
shadowRenderer.setUsesImages((shadowProgram != null && shadowProgram.getActiveImages() > 0) || (shadowWaterProgram != null && shadowWaterProgram.getActiveImages() > 0));
shadowRenderer.setPlayerReflectionCaptureEnabled((shadowProgram != null && GLStateManager.glGetUniformLocation(shadowProgram.getProgramId(), "playerAtlas_img") != -1) || (shadowWaterProgram != null && GLStateManager.glGetUniformLocation(shadowWaterProgram.getProgramId(), "playerAtlas_img") != -1));
} else {
@@ -1031,9 +1031,25 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
private void matchPass(RenderCondition condition) {
if (DisplayListManager.getRecordMode() == DisplayListManager.RecordMode.COMPILE) return;
currentCondition = condition;
beginPass(table.match(condition, inputs));
beginPass(table.match(condition, inputsFor(condition)));
}
/**
* Shadow passes always get the lightmap attribute: every vertex format they submit carries the lightmap
* element, while the live {@code inputs} flag tracks whether unit 1 has GL_TEXTURE_2D enabled - which the
* shadow pass never does. Keep this consistent with the shadow program lookups in the constructor.
*/
private InputAvailability inputsFor(RenderCondition condition) {
if (condition == RenderCondition.SHADOW || condition == RenderCondition.SHADOW_TRANSLUCENT) {
return SHADOW_INPUTS;
}
return inputs;
}
/** Availability used for every shadow pass draw and for the shadow program lookups at load time. */
private static final InputAvailability SHADOW_INPUTS = InputAvailability.of(true, true);
@Override
public void onEntityRenderBoundary() {
GLSMHooks.resolvePendingProgram();
@@ -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
@@ -240,6 +240,7 @@ options.angelica.fontconfig.searchprompt=§7§oSearch fonts...
options.angelica.fontconfig.testareaprompt=§7§oTest area...
options.angelica.fontconfig.testareainfo=Enter text below to see how it renders. Use & for formatting.
options.angelica.fontconfig.testareaexample=&d&lHerobrine&r was &7&m&oNOT&r &nhere
options.angelica.fontconfig.font_selection_info=Left-click on a list item to set the primary font. Right-click for the fallback font. Rendering priority: primary, fallback, Unicode font.
options.angelica.fontconfig.disable_custom_font=Disable Custom Font
options.angelica.fontconfig.enable_custom_font=Enable Custom Font
options.angelica.fontconfig.reset_values=Reset Values
@@ -256,6 +257,7 @@ options.angelica.fontconfig.font_aa_strength=Font AA Strength: %s
options.angelica.fontconfig.aamode.aa_none=Font AA: OFF
options.angelica.fontconfig.aamode.aa_4x=Font AA: 4x MSAA
options.angelica.fontconfig.aamode.aa_16x=Font AA: 16x MSAA
options.angelica.fontconfig.font_brightness=Font Brightness: %s
options.angelica.fontconfig.custom_font_scale=Custom Font Scale: %s
options.angelica.fontconfig.shadow_offset_unicode=Unicode Shadow Offset: %s
options.angelica.fontconfig.font_quality.tooltip=The quality at which custom fonts are rendered, making them less pixelated at the expense of some performance and memory.
@@ -268,6 +270,7 @@ options.angelica.fontconfig.whitespace_scale.tooltip=The scaling factor of blank
options.angelica.fontconfig.glyph_spacing.tooltip=The spacing between each individual glyph.
options.angelica.fontconfig.font_aa_strength.tooltip=The spacing of samples used in font antialiasing. High values may cause fonts to look blurry, low values may cause aliasing. See what works best with your settings.
options.angelica.fontconfig.aamode.tooltip=The font antialiasing mode. Can significantly improve the appearance and readability of text that's being scaled or viewed at oblique angles, at the expense of some performance.
options.angelica.fontconfig.font_brightness.tooltip=Affects the thickness of antialiased fonts. Can improve visibility of some text. A value of 0.5 leaves the alpha untouched. Extreme values might make text look jagged.
options.angelica.fontconfig.custom_font_scale.tooltip=A multiplier that affects the scale of custom fonts. Adjust this if text in your font of choice looks under-/over-sized relative to text that does not use custom fonts.
options.angelica.fontconfig.shadow_offset_unicode.tooltip=A shadow offset multiplier applied only to the Unicode font. Set this to 1.0x if, for example, you use a texture pack that replaces portions of the Unicode font with MC lookalikes.
options.angelica.hudCaching=HUD Caching
@@ -286,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
@@ -3,6 +3,7 @@
uniform sampler2D sampler;
uniform int aaMode;
uniform float strength;
uniform float fontBrightness;
uniform float alphaTestRef;
in vec4 color;
@@ -12,22 +13,17 @@ in vec2 texCoord;
out vec4 fragColor;
/*
Hacky MSAA and anisotropic filtering. Cursed beyond belief. There _may_ have been simpler means of
achieving the produced effects, but this appears to work without noticeable performance losses.
Hacky multisampling and fake anisotropic filtering. Cursed beyond belief. There _may_ have been simpler
means of achieving the produced effects, but this appears to work without noticeable performance losses.
*/
float totalWt;
float txSample(vec2 uv, float du, float dv, float factorU, float factorV) {
float distSquared = sqrt(du * du + dv * dv);
float weight = exp(-distSquared / 6.0);
totalWt += weight;
float finalU = uv.x + factorU * du;
float finalV = uv.y + factorV * dv;
if (finalU < tB.x || finalU > tB.y || finalV < tB.z || finalV > tB.w) {
return 0.0;
}
return weight * texture(sampler, vec2(finalU, finalV)).a;
return texture(sampler, vec2(finalU, finalV)).a;
}
void main() {
@@ -39,15 +35,15 @@ void main() {
col.a = original_alpha * texture(sampler, texCoord).a;
} else {
vec2 texScaled = texCoord * strength;
float res = 0.0;
float fu = abs(dFdx(texScaled.x)) + abs(dFdy(texScaled.x));
float fv = abs(dFdx(texScaled.y)) + abs(dFdy(texScaled.y));
totalWt = 0.0;
float res = 0.0;
if (aaMode == 1) {
res += txSample(texCoord, 2.0, 6.0, fu, fv);
res += txSample(texCoord, 6.0, -2.0, fu, fv);
res += txSample(texCoord, -2.0, -6.0, fu, fv);
res += txSample(texCoord, -6.0, 2.0, fu, fv);
res /= 4.0;
} else {
res += txSample(texCoord, 1.0, 1.0, fu, fv);
res += txSample(texCoord, -1.0, -3.0, fu, fv);
@@ -65,8 +61,10 @@ void main() {
res += txSample(texCoord, 7.0, -4.0, fu, fv);
res += txSample(texCoord, 6.0, 7.0, fu, fv);
res += txSample(texCoord, -7.0, -8.0, fu, fv);
res /= 16.0;
}
res /= totalWt;
float b = fontBrightness;
res = (b * res) / (b * res + (1 - b) * (1 - res));
col.a = original_alpha * res;
}
}
@@ -82,4 +82,6 @@ options.background.random=Random
options.zoom_smooth=Smooth Zoom
options.zoom_smooth.tooltip=Smoothly interpolates the camera toward the zoom level instead of snapping instantly.
options.zoom_smooth_speed=Smooth Zoom Speed
options.zoom_smooth_speed.tooltip=How fast smooth zoom converges to the target zoom. Higher is snappier, lower is floatier. No effect when Smooth Zoom is off.
options.zoom_smooth_speed.tooltip=How fast smooth zoom converges to the target zoom. Higher is snappier, lower is floatier. No effect when Smooth Zoom is off.
options.zoom_cinematic=Cinematic Camera Zoom
options.zoom_cinematic.tooltip=Whether or not to use the Cinematic Camera smooth movement. Off is standard Minecraft mouse camera movement where camera snaps to mouse position. On is floating uninterrupted movement after initial mouse movement, where the camera continues to shift after halting mouse movement.
@@ -1,19 +0,0 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.fontrenderer;
import com.gtnewhorizons.angelica.mixins.interfaces.ResourceAccessor;
import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import java.util.List;
@Mixin(Minecraft.class)
public class MixinMCResourceAccessor implements ResourceAccessor {
@Shadow
private List defaultResourcePacks;
@Override
public List angelica$getDefaultResourcePacks() {
return defaultResourcePacks;
}
}
@@ -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();
}
}
}
@@ -35,7 +35,12 @@ public class MixinEntityRenderer_Zoom {
target = "Lnet/minecraft/client/settings/GameSettings;smoothCamera:Z",
value = "FIELD"))
private boolean angelica$checkZoomEnablement(GameSettings settings) {
return settings.smoothCamera || Zoom.isZoomedIn();
if (Zoom.isCinematicCameraEnabled()){
return settings.smoothCamera || Zoom.isZoomedIn();
}
else{
return settings.smoothCamera;
}
}
}
@@ -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;
}
/**
@@ -138,7 +138,7 @@ public class MixinRenderGlobal implements IRenderGlobalExt {
} else {
pipeline = Iris.getPipelineManager().getPipelineNullable();
if (pass == 0) {
pipeline.setPhase(WorldRenderingPhase.TERRAIN_CUTOUT);
pipeline.setPhase(WorldRenderingPhase.TERRAIN_SOLID);
} else if (pass == 1) {
if (!ShadowRenderingState.areShadowsCurrentlyBeingRendered()) {
iris$beginTranslucents(pipeline, Camera.INSTANCE);
@@ -173,8 +173,10 @@ public class MixinRenderGlobal implements IRenderGlobalExt {
try {
if (pass == 0) {
mc.mcProfiler.endStartSection(shadow ? SECTION_SHADOW_SOLID : SECTION_SOLID);
iris$setTerrainPhase(pipeline, WorldRenderingPhase.TERRAIN_SOLID);
this.celeritas$renderer.drawChunkLayer(BlockRenderLayer.SOLID, camX, camY, camZ);
mc.mcProfiler.endStartSection(shadow ? SECTION_SHADOW_CUTOUT_MIPPED : SECTION_CUTOUT_MIPPED);
iris$setTerrainPhase(pipeline, WorldRenderingPhase.TERRAIN_CUTOUT_MIPPED);
this.celeritas$renderer.drawChunkLayer(BlockRenderLayer.CUTOUT_MIPPED, camX, camY, camZ);
} else {
mc.mcProfiler.endStartSection(shadow ? SECTION_SHADOW_TRANSLUCENT : SECTION_TRANSLUCENT);
@@ -323,6 +325,13 @@ public class MixinRenderGlobal implements IRenderGlobalExt {
return true;
}
@Unique
private static void iris$setTerrainPhase(WorldRenderingPipeline pipeline, WorldRenderingPhase phase) {
if (pipeline != null) {
pipeline.setPhase(phase);
}
}
@Unique
private void iris$beginTranslucents(WorldRenderingPipeline pipeline, Camera camera) {
pipeline.beginHand();
@@ -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,260 @@
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.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.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());
}
private static final class ForeignDomainManager implements IResourceManager {
@Override
public Set<String> getResourceDomains() {
return Collections.emptySet();
}
@Override
public IResource getResource(ResourceLocation location) throws IOException {
throw new IOException("not used");
}
@Override
public List<IResource> getAllResources(ResourceLocation location) throws IOException {
throw new IOException("not used");
}
}
@Test
@SuppressWarnings({ "unchecked", "rawtypes" })
void eligibleDomainsSkipsForeignDomainManager() {
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("domainD", domain);
((Map) manager.domainResourceManagers).put("sound_x", new ForeignDomainManager());
final Map<String, List<IResourcePack>> result = AtlasPackPolicy.eligibleDomains(manager);
assertTrue(result.containsKey("domainD"));
assertFalse(result.containsKey("sound_x"));
}
@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();
}
}
}