diff --git a/multiloader-26.2/common/src/main/java/io/github/marcsanzdev/chestseparators/client/ui/MagnifierRenderer.java b/multiloader-26.2/common/src/main/java/io/github/marcsanzdev/chestseparators/client/ui/MagnifierRenderer.java
index 3dda6d3..1e83742 100644
--- a/multiloader-26.2/common/src/main/java/io/github/marcsanzdev/chestseparators/client/ui/MagnifierRenderer.java
+++ b/multiloader-26.2/common/src/main/java/io/github/marcsanzdev/chestseparators/client/ui/MagnifierRenderer.java
@@ -1,10 +1,7 @@
package io.github.marcsanzdev.chestseparators.client.ui;
-import java.nio.ByteBuffer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
-import org.lwjgl.opengl.GL11;
-import org.lwjgl.system.MemoryUtil;
/**
* Cursor zoom loupe. It samples a small block of the real framebuffer under the cursor and draws it
@@ -44,20 +41,9 @@ public static void render(GuiGraphicsExtractor context, int cursorX, int cursorY
int fbX = clamp((int) Math.round((cursorX - srcHalf) * scale), 0, fbW - blockFb);
int fbYBottom = clamp(fbH - (int) Math.round((cursorY + srcHalf) * scale), 0, fbH - blockFb);
- int[] pixels = new int[blockFb * blockFb];
- // Heap-allocated native buffer: a stack buffer (MemoryStack) overflows for large loupe sizes.
- ByteBuffer buf = MemoryUtil.memAlloc(blockFb * blockFb * 4);
- try {
- GL11.glReadPixels(fbX, fbYBottom, blockFb, blockFb, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buf);
- for (int i = 0; i < pixels.length; i++) {
- int r = buf.get(i * 4) & 0xFF;
- int g = buf.get(i * 4 + 1) & 0xFF;
- int b = buf.get(i * 4 + 2) & 0xFF;
- pixels[i] = 0xFF000000 | (r << 16) | (g << 8) | b;
- }
- } finally {
- MemoryUtil.memFree(buf);
- }
+ // Crash-safe readback (forces a sane pixel-pack state, bounds-checked read). See SafePixelReader.
+ int[] pixels = SafePixelReader.readArgb(fbX, fbYBottom, blockFb, blockFb);
+ if (pixels == null) return;
// Place the loupe up-right of the cursor, flipping to stay on screen.
int sw = client.getWindow().getGuiScaledWidth();
diff --git a/multiloader-26.2/common/src/main/java/io/github/marcsanzdev/chestseparators/client/ui/SafePixelReader.java b/multiloader-26.2/common/src/main/java/io/github/marcsanzdev/chestseparators/client/ui/SafePixelReader.java
new file mode 100644
index 0000000..ed7a863
--- /dev/null
+++ b/multiloader-26.2/common/src/main/java/io/github/marcsanzdev/chestseparators/client/ui/SafePixelReader.java
@@ -0,0 +1,101 @@
+package io.github.marcsanzdev.chestseparators.client.ui;
+
+import java.nio.ByteBuffer;
+import net.minecraft.client.Minecraft;
+import org.lwjgl.opengl.ARBRobustness;
+import org.lwjgl.opengl.GL;
+import org.lwjgl.opengl.GL11C;
+import org.lwjgl.opengl.GL15C;
+import org.lwjgl.opengl.GL21C;
+import org.lwjgl.opengl.GL45C;
+import org.lwjgl.opengl.GLCapabilities;
+import org.lwjgl.system.MemoryUtil;
+
+/**
+ * Crash-safe framebuffer readback for the loupe and the eyedropper.
+ *
+ *
Plain {@code glReadPixels} trusts the current pixel-pack state. Other mods (or vanilla) may leave
+ * {@code GL_PACK_ROW_LENGTH}, {@code GL_PACK_SKIP_*} or a bound {@code GL_PIXEL_PACK_BUFFER} behind, which
+ * makes the driver write more bytes than the destination buffer holds (hard JVM crash on AMD,
+ * EXCEPTION_ACCESS_VIOLATION in atio6axx.dll). This helper:
+ *
+ * - clamps the region to the framebuffer,
+ * - forces a known pack state (no PBO, alignment 4, row length 0, no skips) and restores it after,
+ * - uses the bounds-checked {@code glReadnPixels} (GL 4.5 / KHR / ARB robustness) when available,
+ * - adds slack to the destination buffer as a last line of defence.
+ *
+ */
+public final class SafePixelReader {
+
+ private SafePixelReader() {}
+
+ private static final int SLACK = 64 * 1024;
+
+ /**
+ * Reads a w*h block at framebuffer coords (x, yBottom) as opaque ARGB ints, row 0 = bottom row.
+ * Returns {@code null} if nothing could be read; the caller should then skip drawing.
+ */
+ public static int[] readArgb(int x, int yBottom, int w, int h) {
+ Minecraft client = Minecraft.getInstance();
+ if (client == null || client.getWindow() == null) return null;
+ int fbW = client.getWindow().getWidth();
+ int fbH = client.getWindow().getHeight();
+ if (w <= 0 || h <= 0 || fbW <= 0 || fbH <= 0 || w > fbW || h > fbH) return null;
+ x = Math.max(0, Math.min(x, fbW - w));
+ yBottom = Math.max(0, Math.min(yBottom, fbH - h));
+
+ GLCapabilities caps;
+ try {
+ caps = GL.getCapabilities();
+ } catch (Throwable t) {
+ return null; // no GL context on this thread
+ }
+
+ int bytes = w * h * 4;
+ ByteBuffer buf = MemoryUtil.memCalloc(bytes + SLACK);
+ buf.limit(bytes); // glReadnPixels uses remaining() as the hard size limit
+
+ int prevPbo = GL11C.glGetInteger(GL21C.GL_PIXEL_PACK_BUFFER_BINDING);
+ int prevAlign = GL11C.glGetInteger(GL11C.GL_PACK_ALIGNMENT);
+ int prevRowLen = GL11C.glGetInteger(GL11C.GL_PACK_ROW_LENGTH);
+ int prevSkipRows = GL11C.glGetInteger(GL11C.GL_PACK_SKIP_ROWS);
+ int prevSkipPixels = GL11C.glGetInteger(GL11C.GL_PACK_SKIP_PIXELS);
+ int prevSwap = GL11C.glGetInteger(GL11C.GL_PACK_SWAP_BYTES);
+ int prevLsb = GL11C.glGetInteger(GL11C.GL_PACK_LSB_FIRST);
+ try {
+ if (prevPbo != 0) GL15C.glBindBuffer(GL21C.GL_PIXEL_PACK_BUFFER, 0);
+ GL11C.glPixelStorei(GL11C.GL_PACK_ALIGNMENT, 4);
+ GL11C.glPixelStorei(GL11C.GL_PACK_ROW_LENGTH, 0);
+ GL11C.glPixelStorei(GL11C.GL_PACK_SKIP_ROWS, 0);
+ GL11C.glPixelStorei(GL11C.GL_PACK_SKIP_PIXELS, 0);
+ GL11C.glPixelStorei(GL11C.GL_PACK_SWAP_BYTES, 0);
+ GL11C.glPixelStorei(GL11C.GL_PACK_LSB_FIRST, 0);
+
+ if (caps.OpenGL45 || caps.GL_KHR_robustness) {
+ GL45C.glReadnPixels(x, yBottom, w, h, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, buf);
+ } else if (caps.GL_ARB_robustness) {
+ ARBRobustness.glReadnPixelsARB(x, yBottom, w, h, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, buf);
+ } else {
+ GL11C.glReadPixels(x, yBottom, w, h, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, buf);
+ }
+
+ int[] out = new int[w * h];
+ for (int i = 0; i < out.length; i++) {
+ int r = buf.get(i * 4) & 0xFF;
+ int g = buf.get(i * 4 + 1) & 0xFF;
+ int b = buf.get(i * 4 + 2) & 0xFF;
+ out[i] = 0xFF000000 | (r << 16) | (g << 8) | b;
+ }
+ return out;
+ } finally {
+ GL11C.glPixelStorei(GL11C.GL_PACK_ALIGNMENT, prevAlign);
+ GL11C.glPixelStorei(GL11C.GL_PACK_ROW_LENGTH, prevRowLen);
+ GL11C.glPixelStorei(GL11C.GL_PACK_SKIP_ROWS, prevSkipRows);
+ GL11C.glPixelStorei(GL11C.GL_PACK_SKIP_PIXELS, prevSkipPixels);
+ GL11C.glPixelStorei(GL11C.GL_PACK_SWAP_BYTES, prevSwap);
+ GL11C.glPixelStorei(GL11C.GL_PACK_LSB_FIRST, prevLsb);
+ if (prevPbo != 0) GL15C.glBindBuffer(GL21C.GL_PIXEL_PACK_BUFFER, prevPbo);
+ MemoryUtil.memFree(buf);
+ }
+ }
+}
diff --git a/multiloader-26.2/common/src/main/java/io/github/marcsanzdev/chestseparators/client/ui/screens/ColorPickerGradients.java b/multiloader-26.2/common/src/main/java/io/github/marcsanzdev/chestseparators/client/ui/screens/ColorPickerGradients.java
index 567cd79..4a1817c 100644
--- a/multiloader-26.2/common/src/main/java/io/github/marcsanzdev/chestseparators/client/ui/screens/ColorPickerGradients.java
+++ b/multiloader-26.2/common/src/main/java/io/github/marcsanzdev/chestseparators/client/ui/screens/ColorPickerGradients.java
@@ -1,11 +1,8 @@
package io.github.marcsanzdev.chestseparators.client.ui.screens;
import java.awt.Color;
-import java.nio.ByteBuffer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
-import org.lwjgl.opengl.GL11;
-import org.lwjgl.system.MemoryStack;
/**
* Stateless drawing helpers for the color picker: the saturation/value gradient box, the hue
@@ -43,14 +40,8 @@ static int readHoveredPixelColor(int mouseX, int mouseY) {
double scale = client.getWindow().getGuiScale();
int fbX = (int) (mouseX * scale);
int fbY = client.getWindow().getHeight() - (int) (mouseY * scale) - 1;
-
- try (MemoryStack stack = MemoryStack.stackPush()) {
- ByteBuffer buffer = stack.malloc(4);
- GL11.glReadPixels(fbX, fbY, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer);
- int r = buffer.get(0) & 0xFF;
- int g = buffer.get(1) & 0xFF;
- int b = buffer.get(2) & 0xFF;
- return (r << 16) | (g << 8) | b;
- }
+ // Crash-safe readback (forces a sane pixel-pack state, bounds-checked read).
+ int[] px = io.github.marcsanzdev.chestseparators.client.ui.SafePixelReader.readArgb(fbX, fbY, 1, 1);
+ return px == null ? 0 : (px[0] & 0x00FFFFFF);
}
}