From 5876b4bfb169fb9c785a4994528fecb91913b699 Mon Sep 17 00:00:00 2001 From: DaPiCu Date: Wed, 21 Oct 2026 01:31:38 +0200 Subject: [PATCH] fix(26.2): crash-safe framebuffer readback for loupe and eyedropper glReadPixels in MagnifierRenderer and ColorPickerGradients trusted the current pixel-pack state. If another mod (or vanilla) leaves GL_PACK_ROW_LENGTH / GL_PACK_SKIP_* or a bound GL_PIXEL_PACK_BUFFER behind, the driver writes past the end of the destination buffer, which hard-crashes the JVM (EXCEPTION_ACCESS_VIOLATION in atio6axx.dll on AMD). Route both reads through a new SafePixelReader that: - clamps the region to the framebuffer - unbinds GL_PIXEL_PACK_BUFFER and forces a known pack state, restoring it after - uses bounds-checked glReadnPixels (GL 4.5 / KHR / ARB robustness) when available - adds slack to the destination buffer as a last line of defence --- .../client/ui/MagnifierRenderer.java | 20 +--- .../client/ui/SafePixelReader.java | 101 ++++++++++++++++++ .../ui/screens/ColorPickerGradients.java | 15 +-- 3 files changed, 107 insertions(+), 29 deletions(-) create mode 100644 multiloader-26.2/common/src/main/java/io/github/marcsanzdev/chestseparators/client/ui/SafePixelReader.java 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: + *

+ */ +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); } }