2025-11-24 22:52:51 +03:00

592 lines
22 KiB
Java

/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* com.mojang.jtracy.MemoryPool
* com.mojang.jtracy.TracyClient
* com.mojang.logging.LogUtils
* org.apache.commons.io.IOUtils
* org.jspecify.annotations.Nullable
* org.lwjgl.stb.STBIWriteCallback
* org.lwjgl.stb.STBImage
* org.lwjgl.stb.STBImageResize
* org.lwjgl.stb.STBImageWrite
* org.lwjgl.system.MemoryStack
* org.lwjgl.system.MemoryUtil
* org.lwjgl.util.freetype.FT_Bitmap
* org.lwjgl.util.freetype.FT_Face
* org.lwjgl.util.freetype.FT_GlyphSlot
* org.lwjgl.util.freetype.FreeType
* org.slf4j.Logger
*/
package com.mojang.blaze3d.platform;
import com.mojang.blaze3d.platform.DebugMemoryUntracker;
import com.mojang.blaze3d.platform.TextureUtil;
import com.mojang.jtracy.MemoryPool;
import com.mojang.jtracy.TracyClient;
import com.mojang.logging.LogUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.util.EnumSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.function.IntUnaryOperator;
import net.minecraft.client.gui.font.providers.FreeTypeUtil;
import net.minecraft.util.ARGB;
import net.minecraft.util.PngInfo;
import org.apache.commons.io.IOUtils;
import org.jspecify.annotations.Nullable;
import org.lwjgl.stb.STBIWriteCallback;
import org.lwjgl.stb.STBImage;
import org.lwjgl.stb.STBImageResize;
import org.lwjgl.stb.STBImageWrite;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.util.freetype.FT_Bitmap;
import org.lwjgl.util.freetype.FT_Face;
import org.lwjgl.util.freetype.FT_GlyphSlot;
import org.lwjgl.util.freetype.FreeType;
import org.slf4j.Logger;
public final class NativeImage
implements AutoCloseable {
private static final Logger LOGGER = LogUtils.getLogger();
private static final MemoryPool MEMORY_POOL = TracyClient.createMemoryPool((String)"NativeImage");
private static final Set<StandardOpenOption> OPEN_OPTIONS = EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
private final Format format;
private final int width;
private final int height;
private final boolean useStbFree;
private long pixels;
private final long size;
public NativeImage(int width, int height, boolean zero) {
this(Format.RGBA, width, height, zero);
}
public NativeImage(Format format, int width, int height, boolean zero) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Invalid texture size: " + width + "x" + height);
}
this.format = format;
this.width = width;
this.height = height;
this.size = (long)width * (long)height * (long)format.components();
this.useStbFree = false;
this.pixels = zero ? MemoryUtil.nmemCalloc((long)1L, (long)this.size) : MemoryUtil.nmemAlloc((long)this.size);
MEMORY_POOL.malloc(this.pixels, (int)this.size);
if (this.pixels == 0L) {
throw new IllegalStateException("Unable to allocate texture of size " + width + "x" + height + " (" + format.components() + " channels)");
}
}
public NativeImage(Format format, int width, int height, boolean useStbFree, long pixels) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Invalid texture size: " + width + "x" + height);
}
this.format = format;
this.width = width;
this.height = height;
this.useStbFree = useStbFree;
this.pixels = pixels;
this.size = (long)width * (long)height * (long)format.components();
}
public String toString() {
return "NativeImage[" + String.valueOf((Object)this.format) + " " + this.width + "x" + this.height + "@" + this.pixels + (this.useStbFree ? "S" : "N") + "]";
}
private boolean isOutsideBounds(int x, int y) {
return x < 0 || x >= this.width || y < 0 || y >= this.height;
}
public static NativeImage read(InputStream inputStream) throws IOException {
return NativeImage.read(Format.RGBA, inputStream);
}
/*
* WARNING - Removed try catching itself - possible behaviour change.
*/
public static NativeImage read(@Nullable Format format, InputStream inputStream) throws IOException {
ByteBuffer file = null;
try {
file = TextureUtil.readResource(inputStream);
NativeImage nativeImage = NativeImage.read(format, file);
return nativeImage;
}
finally {
MemoryUtil.memFree((Buffer)file);
IOUtils.closeQuietly((InputStream)inputStream);
}
}
public static NativeImage read(ByteBuffer bytes) throws IOException {
return NativeImage.read(Format.RGBA, bytes);
}
/*
* WARNING - Removed try catching itself - possible behaviour change.
*/
public static NativeImage read(byte[] bytes) throws IOException {
MemoryStack memoryStack = MemoryStack.stackGet();
int bytesAvailable = memoryStack.getPointer();
if (bytesAvailable < bytes.length) {
ByteBuffer buffer = MemoryUtil.memAlloc((int)bytes.length);
try {
NativeImage nativeImage = NativeImage.putAndRead(buffer, bytes);
return nativeImage;
}
finally {
MemoryUtil.memFree((Buffer)buffer);
}
}
try (MemoryStack stack = MemoryStack.stackPush();){
ByteBuffer buffer = stack.malloc(bytes.length);
NativeImage nativeImage = NativeImage.putAndRead(buffer, bytes);
return nativeImage;
}
}
private static NativeImage putAndRead(ByteBuffer nativeBuffer, byte[] bytes) throws IOException {
nativeBuffer.put(bytes);
nativeBuffer.rewind();
return NativeImage.read(nativeBuffer);
}
public static NativeImage read(@Nullable Format format, ByteBuffer bytes) throws IOException {
if (format != null && !format.supportedByStb()) {
throw new UnsupportedOperationException("Don't know how to read format " + String.valueOf((Object)format));
}
if (MemoryUtil.memAddress((ByteBuffer)bytes) == 0L) {
throw new IllegalArgumentException("Invalid buffer");
}
PngInfo.validateHeader(bytes);
try (MemoryStack stack = MemoryStack.stackPush();){
IntBuffer w = stack.mallocInt(1);
IntBuffer h = stack.mallocInt(1);
IntBuffer comp = stack.mallocInt(1);
ByteBuffer pixels = STBImage.stbi_load_from_memory((ByteBuffer)bytes, (IntBuffer)w, (IntBuffer)h, (IntBuffer)comp, (int)(format == null ? 0 : format.components));
if (pixels == null) {
throw new IOException("Could not load image: " + STBImage.stbi_failure_reason());
}
long address = MemoryUtil.memAddress((ByteBuffer)pixels);
MEMORY_POOL.malloc(address, pixels.limit());
NativeImage nativeImage = new NativeImage(format == null ? Format.getStbFormat(comp.get(0)) : format, w.get(0), h.get(0), true, address);
return nativeImage;
}
}
private void checkAllocated() {
if (this.pixels == 0L) {
throw new IllegalStateException("Image is not allocated.");
}
}
@Override
public void close() {
if (this.pixels != 0L) {
if (this.useStbFree) {
STBImage.nstbi_image_free((long)this.pixels);
} else {
MemoryUtil.nmemFree((long)this.pixels);
}
MEMORY_POOL.free(this.pixels);
}
this.pixels = 0L;
}
public int getWidth() {
return this.width;
}
public int getHeight() {
return this.height;
}
public Format format() {
return this.format;
}
private int getPixelABGR(int x, int y) {
if (this.format != Format.RGBA) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "getPixelRGBA only works on RGBA images; have %s", new Object[]{this.format}));
}
if (this.isOutsideBounds(x, y)) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "(%s, %s) outside of image bounds (%s, %s)", x, y, this.width, this.height));
}
this.checkAllocated();
long offset = ((long)x + (long)y * (long)this.width) * 4L;
return MemoryUtil.memGetInt((long)(this.pixels + offset));
}
public int getPixel(int x, int y) {
return ARGB.fromABGR(this.getPixelABGR(x, y));
}
public void setPixelABGR(int x, int y, int pixel) {
if (this.format != Format.RGBA) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "setPixelRGBA only works on RGBA images; have %s", new Object[]{this.format}));
}
if (this.isOutsideBounds(x, y)) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "(%s, %s) outside of image bounds (%s, %s)", x, y, this.width, this.height));
}
this.checkAllocated();
long offset = ((long)x + (long)y * (long)this.width) * 4L;
MemoryUtil.memPutInt((long)(this.pixels + offset), (int)pixel);
}
public void setPixel(int x, int y, int pixel) {
this.setPixelABGR(x, y, ARGB.toABGR(pixel));
}
public NativeImage mappedCopy(IntUnaryOperator function) {
if (this.format != Format.RGBA) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "function application only works on RGBA images; have %s", new Object[]{this.format}));
}
this.checkAllocated();
NativeImage result = new NativeImage(this.width, this.height, false);
int pixelCount = this.width * this.height;
IntBuffer sourceBuffer = MemoryUtil.memIntBuffer((long)this.pixels, (int)pixelCount);
IntBuffer targetBuffer = MemoryUtil.memIntBuffer((long)result.pixels, (int)pixelCount);
for (int i = 0; i < pixelCount; ++i) {
int pixel = ARGB.fromABGR(sourceBuffer.get(i));
int modified = function.applyAsInt(pixel);
targetBuffer.put(i, ARGB.toABGR(modified));
}
return result;
}
public int[] getPixelsABGR() {
if (this.format != Format.RGBA) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "getPixels only works on RGBA images; have %s", new Object[]{this.format}));
}
this.checkAllocated();
int[] result = new int[this.width * this.height];
MemoryUtil.memIntBuffer((long)this.pixels, (int)(this.width * this.height)).get(result);
return result;
}
public int[] getPixels() {
int[] result = this.getPixelsABGR();
for (int i = 0; i < result.length; ++i) {
result[i] = ARGB.fromABGR(result[i]);
}
return result;
}
public byte getLuminanceOrAlpha(int x, int y) {
if (!this.format.hasLuminanceOrAlpha()) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "no luminance or alpha in %s", new Object[]{this.format}));
}
if (this.isOutsideBounds(x, y)) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "(%s, %s) outside of image bounds (%s, %s)", x, y, this.width, this.height));
}
int offset = (x + y * this.width) * this.format.components() + this.format.luminanceOrAlphaOffset() / 8;
return MemoryUtil.memGetByte((long)(this.pixels + (long)offset));
}
@Deprecated
public int[] makePixelArray() {
if (this.format != Format.RGBA) {
throw new UnsupportedOperationException("can only call makePixelArray for RGBA images.");
}
this.checkAllocated();
int[] pixels = new int[this.getWidth() * this.getHeight()];
for (int y = 0; y < this.getHeight(); ++y) {
for (int x = 0; x < this.getWidth(); ++x) {
pixels[x + y * this.getWidth()] = this.getPixel(x, y);
}
}
return pixels;
}
public void writeToFile(File file) throws IOException {
this.writeToFile(file.toPath());
}
public boolean copyFromFont(FT_Face face, int index) {
if (this.format.components() != 1) {
throw new IllegalArgumentException("Can only write fonts into 1-component images.");
}
if (FreeTypeUtil.checkError(FreeType.FT_Load_Glyph((FT_Face)face, (int)index, (int)4), "Loading glyph")) {
return false;
}
FT_GlyphSlot glyph = Objects.requireNonNull(face.glyph(), "Glyph not initialized");
FT_Bitmap bitmap = glyph.bitmap();
if (bitmap.pixel_mode() != 2) {
throw new IllegalStateException("Rendered glyph was not 8-bit grayscale");
}
if (bitmap.width() != this.getWidth() || bitmap.rows() != this.getHeight()) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "Glyph bitmap of size %sx%s does not match image of size: %sx%s", bitmap.width(), bitmap.rows(), this.getWidth(), this.getHeight()));
}
int bufferSize = bitmap.width() * bitmap.rows();
ByteBuffer buffer = Objects.requireNonNull(bitmap.buffer(bufferSize), "Glyph has no bitmap");
MemoryUtil.memCopy((long)MemoryUtil.memAddress((ByteBuffer)buffer), (long)this.pixels, (long)bufferSize);
return true;
}
public void writeToFile(Path file) throws IOException {
if (!this.format.supportedByStb()) {
throw new UnsupportedOperationException("Don't know how to write format " + String.valueOf((Object)this.format));
}
this.checkAllocated();
try (SeekableByteChannel out = Files.newByteChannel(file, OPEN_OPTIONS, new FileAttribute[0]);){
if (!this.writeToChannel(out)) {
throw new IOException("Could not write image to the PNG file \"" + String.valueOf(file.toAbsolutePath()) + "\": " + STBImage.stbi_failure_reason());
}
}
}
/*
* WARNING - Removed try catching itself - possible behaviour change.
*/
private boolean writeToChannel(WritableByteChannel output) throws IOException {
WriteCallback writer = new WriteCallback(output);
try {
int height = Math.min(this.getHeight(), Integer.MAX_VALUE / this.getWidth() / this.format.components());
if (height < this.getHeight()) {
LOGGER.warn("Dropping image height from {} to {} to fit the size into 32-bit signed int", (Object)this.getHeight(), (Object)height);
}
if (STBImageWrite.nstbi_write_png_to_func((long)writer.address(), (long)0L, (int)this.getWidth(), (int)height, (int)this.format.components(), (long)this.pixels, (int)0) == 0) {
boolean bl = false;
return bl;
}
writer.throwIfException();
boolean bl = true;
return bl;
}
finally {
writer.free();
}
}
public void copyFrom(NativeImage from) {
if (from.format() != this.format) {
throw new UnsupportedOperationException("Image formats don't match.");
}
int components = this.format.components();
this.checkAllocated();
from.checkAllocated();
if (this.width == from.width) {
MemoryUtil.memCopy((long)from.pixels, (long)this.pixels, (long)Math.min(this.size, from.size));
} else {
int minWidth = Math.min(this.getWidth(), from.getWidth());
int minHeight = Math.min(this.getHeight(), from.getHeight());
for (int y = 0; y < minHeight; ++y) {
int fromOffset = y * from.getWidth() * components;
int toOffset = y * this.getWidth() * components;
MemoryUtil.memCopy((long)(from.pixels + (long)fromOffset), (long)(this.pixels + (long)toOffset), (long)minWidth);
}
}
}
public void fillRect(int xs, int ys, int width, int height, int pixel) {
for (int y = ys; y < ys + height; ++y) {
for (int x = xs; x < xs + width; ++x) {
this.setPixel(x, y, pixel);
}
}
}
public void copyRect(int startX, int startY, int offsetX, int offsetY, int sizeX, int sizeY, boolean swapX, boolean swapY) {
this.copyRect(this, startX, startY, startX + offsetX, startY + offsetY, sizeX, sizeY, swapX, swapY);
}
public void copyRect(NativeImage target, int sourceX, int sourceY, int targetX, int targetY, int sizeX, int sizeY, boolean swapX, boolean swapY) {
for (int y = 0; y < sizeY; ++y) {
for (int x = 0; x < sizeX; ++x) {
int dx = swapX ? sizeX - 1 - x : x;
int dy = swapY ? sizeY - 1 - y : y;
int source = this.getPixelABGR(sourceX + x, sourceY + y);
target.setPixelABGR(targetX + dx, targetY + dy, source);
}
}
}
public void resizeSubRectTo(int sourceX, int sourceY, int sizeX, int sizeY, NativeImage to) {
this.checkAllocated();
if (to.format() != this.format) {
throw new UnsupportedOperationException("resizeSubRectTo only works for images of the same format.");
}
int components = this.format.components();
STBImageResize.nstbir_resize_uint8((long)(this.pixels + (long)((sourceX + sourceY * this.getWidth()) * components)), (int)sizeX, (int)sizeY, (int)(this.getWidth() * components), (long)to.pixels, (int)to.getWidth(), (int)to.getHeight(), (int)0, (int)components);
}
public void untrack() {
DebugMemoryUntracker.untrack(this.pixels);
}
public long getPointer() {
return this.pixels;
}
public static enum Format {
RGBA(4, true, true, true, false, true, 0, 8, 16, 255, 24, true),
RGB(3, true, true, true, false, false, 0, 8, 16, 255, 255, true),
LUMINANCE_ALPHA(2, false, false, false, true, true, 255, 255, 255, 0, 8, true),
LUMINANCE(1, false, false, false, true, false, 0, 0, 0, 0, 255, true);
private final int components;
private final boolean hasRed;
private final boolean hasGreen;
private final boolean hasBlue;
private final boolean hasLuminance;
private final boolean hasAlpha;
private final int redOffset;
private final int greenOffset;
private final int blueOffset;
private final int luminanceOffset;
private final int alphaOffset;
private final boolean supportedByStb;
private Format(int components, boolean hasRed, boolean hasGreen, boolean hasBlue, boolean hasLuminance, boolean hasAlpha, int redOffset, int greenOffset, int blueOffset, int luminanceOffset, int alphaOffset, boolean supportedByStb) {
this.components = components;
this.hasRed = hasRed;
this.hasGreen = hasGreen;
this.hasBlue = hasBlue;
this.hasLuminance = hasLuminance;
this.hasAlpha = hasAlpha;
this.redOffset = redOffset;
this.greenOffset = greenOffset;
this.blueOffset = blueOffset;
this.luminanceOffset = luminanceOffset;
this.alphaOffset = alphaOffset;
this.supportedByStb = supportedByStb;
}
public int components() {
return this.components;
}
public boolean hasRed() {
return this.hasRed;
}
public boolean hasGreen() {
return this.hasGreen;
}
public boolean hasBlue() {
return this.hasBlue;
}
public boolean hasLuminance() {
return this.hasLuminance;
}
public boolean hasAlpha() {
return this.hasAlpha;
}
public int redOffset() {
return this.redOffset;
}
public int greenOffset() {
return this.greenOffset;
}
public int blueOffset() {
return this.blueOffset;
}
public int luminanceOffset() {
return this.luminanceOffset;
}
public int alphaOffset() {
return this.alphaOffset;
}
public boolean hasLuminanceOrRed() {
return this.hasLuminance || this.hasRed;
}
public boolean hasLuminanceOrGreen() {
return this.hasLuminance || this.hasGreen;
}
public boolean hasLuminanceOrBlue() {
return this.hasLuminance || this.hasBlue;
}
public boolean hasLuminanceOrAlpha() {
return this.hasLuminance || this.hasAlpha;
}
public int luminanceOrRedOffset() {
return this.hasLuminance ? this.luminanceOffset : this.redOffset;
}
public int luminanceOrGreenOffset() {
return this.hasLuminance ? this.luminanceOffset : this.greenOffset;
}
public int luminanceOrBlueOffset() {
return this.hasLuminance ? this.luminanceOffset : this.blueOffset;
}
public int luminanceOrAlphaOffset() {
return this.hasLuminance ? this.luminanceOffset : this.alphaOffset;
}
public boolean supportedByStb() {
return this.supportedByStb;
}
private static Format getStbFormat(int i) {
switch (i) {
case 1: {
return LUMINANCE;
}
case 2: {
return LUMINANCE_ALPHA;
}
case 3: {
return RGB;
}
}
return RGBA;
}
}
private static class WriteCallback
extends STBIWriteCallback {
private final WritableByteChannel output;
private @Nullable IOException exception;
private WriteCallback(WritableByteChannel output) {
this.output = output;
}
public void invoke(long context, long data, int size) {
ByteBuffer dataBuf = WriteCallback.getData((long)data, (int)size);
try {
this.output.write(dataBuf);
}
catch (IOException e) {
this.exception = e;
}
}
public void throwIfException() throws IOException {
if (this.exception != null) {
throw this.exception;
}
}
}
}