minecraft-src/com/mojang/blaze3d/platform/NativeImage.java
2025-07-04 03:45:38 +03:00

632 lines
19 KiB
Java

package com.mojang.blaze3d.platform;
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.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.function.IntUnaryOperator;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
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.jetbrains.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;
@Environment(EnvType.CLIENT)
public final class NativeImage implements AutoCloseable {
private static final Logger LOGGER = LogUtils.getLogger();
private static final MemoryPool MEMORY_POOL = TracyClient.createMemoryPool("NativeImage");
private static final Set<StandardOpenOption> OPEN_OPTIONS = EnumSet.of(
StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
);
private final NativeImage.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 useCalloc) {
this(NativeImage.Format.RGBA, width, height, useCalloc);
}
public NativeImage(NativeImage.Format format, int width, int height, boolean useCalloc) {
if (width > 0 && height > 0) {
this.format = format;
this.width = width;
this.height = height;
this.size = (long)width * height * format.components();
this.useStbFree = false;
if (useCalloc) {
this.pixels = MemoryUtil.nmemCalloc(1L, this.size);
} else {
this.pixels = MemoryUtil.nmemAlloc(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)");
}
} else {
throw new IllegalArgumentException("Invalid texture size: " + width + "x" + height);
}
}
public NativeImage(NativeImage.Format format, int width, int height, boolean useStbFree, long pixels) {
if (width > 0 && height > 0) {
this.format = format;
this.width = width;
this.height = height;
this.useStbFree = useStbFree;
this.pixels = pixels;
this.size = (long)width * height * format.components();
} else {
throw new IllegalArgumentException("Invalid texture size: " + width + "x" + height);
}
}
public String toString() {
return "NativeImage[" + 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 textureStream) throws IOException {
return read(NativeImage.Format.RGBA, textureStream);
}
public static NativeImage read(@Nullable NativeImage.Format format, InputStream textureStream) throws IOException {
ByteBuffer byteBuffer = null;
NativeImage var3;
try {
byteBuffer = TextureUtil.readResource(textureStream);
byteBuffer.rewind();
var3 = read(format, byteBuffer);
} finally {
MemoryUtil.memFree(byteBuffer);
IOUtils.closeQuietly(textureStream);
}
return var3;
}
public static NativeImage read(ByteBuffer textureData) throws IOException {
return read(NativeImage.Format.RGBA, textureData);
}
public static NativeImage read(byte[] bytes) throws IOException {
MemoryStack memoryStack = MemoryStack.stackGet();
int i = memoryStack.getPointer();
if (i < bytes.length) {
ByteBuffer byteBuffer = MemoryUtil.memAlloc(bytes.length);
NativeImage var13;
try {
var13 = putAndRead(byteBuffer, bytes);
} finally {
MemoryUtil.memFree(byteBuffer);
}
return var13;
} else {
NativeImage var5;
try (MemoryStack memoryStack2 = MemoryStack.stackPush()) {
ByteBuffer byteBuffer2 = memoryStack2.malloc(bytes.length);
var5 = putAndRead(byteBuffer2, bytes);
}
return var5;
}
}
private static NativeImage putAndRead(ByteBuffer buffer, byte[] bytes) throws IOException {
buffer.put(bytes);
buffer.rewind();
return read(buffer);
}
public static NativeImage read(@Nullable NativeImage.Format format, ByteBuffer textureData) throws IOException {
if (format != null && !format.supportedByStb()) {
throw new UnsupportedOperationException("Don't know how to read format " + format);
} else if (MemoryUtil.memAddress(textureData) == 0L) {
throw new IllegalArgumentException("Invalid buffer");
} else {
PngInfo.validateHeader(textureData);
NativeImage var9;
try (MemoryStack memoryStack = MemoryStack.stackPush()) {
IntBuffer intBuffer = memoryStack.mallocInt(1);
IntBuffer intBuffer2 = memoryStack.mallocInt(1);
IntBuffer intBuffer3 = memoryStack.mallocInt(1);
ByteBuffer byteBuffer = STBImage.stbi_load_from_memory(textureData, intBuffer, intBuffer2, intBuffer3, format == null ? 0 : format.components);
if (byteBuffer == null) {
throw new IOException("Could not load image: " + STBImage.stbi_failure_reason());
}
long l = MemoryUtil.memAddress(byteBuffer);
MEMORY_POOL.malloc(l, byteBuffer.limit());
var9 = new NativeImage(format == null ? NativeImage.Format.getStbFormat(intBuffer3.get(0)) : format, intBuffer.get(0), intBuffer2.get(0), true, l);
}
return var9;
}
}
private void checkAllocated() {
if (this.pixels == 0L) {
throw new IllegalStateException("Image is not allocated.");
}
}
public void close() {
if (this.pixels != 0L) {
if (this.useStbFree) {
STBImage.nstbi_image_free(this.pixels);
} else {
MemoryUtil.nmemFree(this.pixels);
}
MEMORY_POOL.free(this.pixels);
}
this.pixels = 0L;
}
public int getWidth() {
return this.width;
}
public int getHeight() {
return this.height;
}
public NativeImage.Format format() {
return this.format;
}
private int getPixelABGR(int x, int y) {
if (this.format != NativeImage.Format.RGBA) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "getPixelRGBA only works on RGBA images; have %s", this.format));
} else 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));
} else {
this.checkAllocated();
long l = (x + (long)y * this.width) * 4L;
return MemoryUtil.memGetInt(this.pixels + l);
}
}
public int getPixel(int x, int y) {
return ARGB.fromABGR(this.getPixelABGR(x, y));
}
public void setPixelABGR(int x, int y, int color) {
if (this.format != NativeImage.Format.RGBA) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "setPixelRGBA only works on RGBA images; have %s", this.format));
} else 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));
} else {
this.checkAllocated();
long l = (x + (long)y * this.width) * 4L;
MemoryUtil.memPutInt(this.pixels + l, color);
}
}
public void setPixel(int x, int y, int color) {
this.setPixelABGR(x, y, ARGB.toABGR(color));
}
public NativeImage mappedCopy(IntUnaryOperator function) {
if (this.format != NativeImage.Format.RGBA) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "function application only works on RGBA images; have %s", this.format));
} else {
this.checkAllocated();
NativeImage nativeImage = new NativeImage(this.width, this.height, false);
int i = this.width * this.height;
IntBuffer intBuffer = MemoryUtil.memIntBuffer(this.pixels, i);
IntBuffer intBuffer2 = MemoryUtil.memIntBuffer(nativeImage.pixels, i);
for (int j = 0; j < i; j++) {
int k = ARGB.fromABGR(intBuffer.get(j));
int l = function.applyAsInt(k);
intBuffer2.put(j, ARGB.toABGR(l));
}
return nativeImage;
}
}
public int[] getPixelsABGR() {
if (this.format != NativeImage.Format.RGBA) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "getPixels only works on RGBA images; have %s", this.format));
} else {
this.checkAllocated();
int[] is = new int[this.width * this.height];
MemoryUtil.memIntBuffer(this.pixels, this.width * this.height).get(is);
return is;
}
}
public int[] getPixels() {
int[] is = this.getPixelsABGR();
for (int i = 0; i < is.length; i++) {
is[i] = ARGB.fromABGR(is[i]);
}
return is;
}
public byte getLuminanceOrAlpha(int x, int y) {
if (!this.format.hasLuminanceOrAlpha()) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "no luminance or alpha in %s", this.format));
} else 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));
} else {
int i = (x + y * this.width) * this.format.components() + this.format.luminanceOrAlphaOffset() / 8;
return MemoryUtil.memGetByte(this.pixels + i);
}
}
@Deprecated
public int[] makePixelArray() {
if (this.format != NativeImage.Format.RGBA) {
throw new UnsupportedOperationException("can only call makePixelArray for RGBA images.");
} else {
this.checkAllocated();
int[] is = new int[this.getWidth() * this.getHeight()];
for (int i = 0; i < this.getHeight(); i++) {
for (int j = 0; j < this.getWidth(); j++) {
is[j + i * this.getWidth()] = this.getPixel(j, i);
}
}
return is;
}
}
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.");
} else if (FreeTypeUtil.checkError(FreeType.FT_Load_Glyph(face, index, 4), "Loading glyph")) {
return false;
} else {
FT_GlyphSlot fT_GlyphSlot = (FT_GlyphSlot)Objects.requireNonNull(face.glyph(), "Glyph not initialized");
FT_Bitmap fT_Bitmap = fT_GlyphSlot.bitmap();
if (fT_Bitmap.pixel_mode() != 2) {
throw new IllegalStateException("Rendered glyph was not 8-bit grayscale");
} else if (fT_Bitmap.width() == this.getWidth() && fT_Bitmap.rows() == this.getHeight()) {
int i = fT_Bitmap.width() * fT_Bitmap.rows();
ByteBuffer byteBuffer = (ByteBuffer)Objects.requireNonNull(fT_Bitmap.buffer(i), "Glyph has no bitmap");
MemoryUtil.memCopy(MemoryUtil.memAddress(byteBuffer), this.pixels, i);
return true;
} else {
throw new IllegalArgumentException(
String.format(
Locale.ROOT, "Glyph bitmap of size %sx%s does not match image of size: %sx%s", fT_Bitmap.width(), fT_Bitmap.rows(), this.getWidth(), this.getHeight()
)
);
}
}
}
public void writeToFile(Path path) throws IOException {
if (!this.format.supportedByStb()) {
throw new UnsupportedOperationException("Don't know how to write format " + this.format);
} else {
this.checkAllocated();
WritableByteChannel writableByteChannel = Files.newByteChannel(path, OPEN_OPTIONS);
try {
if (!this.writeToChannel(writableByteChannel)) {
throw new IOException("Could not write image to the PNG file \"" + path.toAbsolutePath() + "\": " + STBImage.stbi_failure_reason());
}
} catch (Throwable var6) {
if (writableByteChannel != null) {
try {
writableByteChannel.close();
} catch (Throwable var5) {
var6.addSuppressed(var5);
}
}
throw var6;
}
if (writableByteChannel != null) {
writableByteChannel.close();
}
}
}
private boolean writeToChannel(WritableByteChannel channel) throws IOException {
NativeImage.WriteCallback writeCallback = new NativeImage.WriteCallback(channel);
boolean var4;
try {
int i = Math.min(this.getHeight(), Integer.MAX_VALUE / this.getWidth() / this.format.components());
if (i < this.getHeight()) {
LOGGER.warn("Dropping image height from {} to {} to fit the size into 32-bit signed int", this.getHeight(), i);
}
if (STBImageWrite.nstbi_write_png_to_func(writeCallback.address(), 0L, this.getWidth(), i, this.format.components(), this.pixels, 0) != 0) {
writeCallback.throwIfException();
return true;
}
var4 = false;
} finally {
writeCallback.free();
}
return var4;
}
public void copyFrom(NativeImage other) {
if (other.format() != this.format) {
throw new UnsupportedOperationException("Image formats don't match.");
} else {
int i = this.format.components();
this.checkAllocated();
other.checkAllocated();
if (this.width == other.width) {
MemoryUtil.memCopy(other.pixels, this.pixels, Math.min(this.size, other.size));
} else {
int j = Math.min(this.getWidth(), other.getWidth());
int k = Math.min(this.getHeight(), other.getHeight());
for (int l = 0; l < k; l++) {
int m = l * other.getWidth() * i;
int n = l * this.getWidth() * i;
MemoryUtil.memCopy(other.pixels + m, this.pixels + n, j);
}
}
}
}
public void fillRect(int x, int y, int width, int height, int value) {
for (int i = y; i < y + height; i++) {
for (int j = x; j < x + width; j++) {
this.setPixel(j, i, value);
}
}
}
public void copyRect(int xFrom, int yFrom, int xToDelta, int yToDelta, int width, int height, boolean mirrorX, boolean mirrorY) {
this.copyRect(this, xFrom, yFrom, xFrom + xToDelta, yFrom + yToDelta, width, height, mirrorX, mirrorY);
}
public void copyRect(NativeImage source, int xFrom, int yFrom, int xTo, int yTo, int width, int height, boolean mirrorX, boolean mirrorY) {
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
int k = mirrorX ? width - 1 - j : j;
int l = mirrorY ? height - 1 - i : i;
int m = this.getPixelABGR(xFrom + j, yFrom + i);
source.setPixelABGR(xTo + k, yTo + l, m);
}
}
}
public void resizeSubRectTo(int x, int y, int width, int height, NativeImage image) {
this.checkAllocated();
if (image.format() != this.format) {
throw new UnsupportedOperationException("resizeSubRectTo only works for images of the same format.");
} else {
int i = this.format.components();
STBImageResize.nstbir_resize_uint8(
this.pixels + (x + y * this.getWidth()) * i, width, height, this.getWidth() * i, image.pixels, image.getWidth(), image.getHeight(), 0, i
);
}
}
public void untrack() {
DebugMemoryUntracker.untrack(this.pixels);
}
public long getPointer() {
return this.pixels;
}
@Environment(EnvType.CLIENT)
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);
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(
final int components,
final boolean hasRed,
final boolean hasGreen,
final boolean hasBlue,
final boolean hasLuminance,
final boolean hasAlpha,
final int redOffset,
final int greenOffset,
final int blueOffset,
final int luminanceOffset,
final int alphaOffset,
final 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;
}
static NativeImage.Format getStbFormat(int channels) {
switch (channels) {
case 1:
return LUMINANCE;
case 2:
return LUMINANCE_ALPHA;
case 3:
return RGB;
case 4:
default:
return RGBA;
}
}
}
@Environment(EnvType.CLIENT)
static class WriteCallback extends STBIWriteCallback {
private final WritableByteChannel output;
@Nullable
private IOException exception;
WriteCallback(WritableByteChannel output) {
this.output = output;
}
@Override
public void invoke(long l, long m, int i) {
ByteBuffer byteBuffer = getData(m, i);
try {
this.output.write(byteBuffer);
} catch (IOException var8) {
this.exception = var8;
}
}
public void throwIfException() throws IOException {
if (this.exception != null) {
throw this.exception;
}
}
}
}