Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions jme3-core/src/main/java/com/jme3/renderer/Renderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture;
import com.jme3.texture.TextureArray;
import com.jme3.texture.TextureImage;
import com.jme3.util.NativeObject;
import java.nio.ByteBuffer;
Expand Down Expand Up @@ -311,6 +312,38 @@ public void setTexture(int unit, Texture tex)
*/
public void modifyTexture(Texture tex, Image pixels, int x, int y);

/**
* Copies a complete 2D source image into a rectangle of one texture-array
* layer on the render thread. Only level-zero, uncompressed color images
* without multisampling or mipmaps are supported. Source and destination
* must have identical formats and color spaces, and the destination must
* use a non-mipmapped minification filter. Source pixels are tightly packed
* from byte zero of a sufficiently sized direct buffer; its position,
* limit, mark, byte order and contents are preserved.
*
* <p>This updates GPU storage only. It neither changes the destination's CPU
* image data nor marks the image dirty. Callers must retain authoritative
* CPU data or replay updates after texture/context recreation or any full
* image upload. An unuploaded or dirty destination is first fully uploaded
* and requires complete CPU layer buffers. A resident, clean destination
* requires only one layer-region upload; other texels are unchanged. Its CPU
* layer buffers may be null, but its image data list must retain one entry
* per layer for bounds validation.
*
* @param dest the destination texture array (not null)
* @param pixels the single-buffer 2D source image (not null)
* @param layer the destination layer index, starting at zero
* @param x the first destination column
* @param y the first destination row
* @throws IllegalArgumentException for invalid dimensions, bounds, buffers,
* or mismatched image formats/color spaces
* @throws UnsupportedOperationException if array updates or the specified
* image/mipmap policy are unsupported
*/
default void modifyTexture(TextureArray dest, Image pixels, int layer, int x, int y) {
throw new UnsupportedOperationException("Texture-array updates are not supported by this renderer");
}

/**
* Deletes a texture from the GPU.
*
Expand Down
27 changes: 27 additions & 0 deletions jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.texture.TextureArray;
import com.jme3.texture.Texture.ShadowCompareMode;
import com.jme3.texture.Texture.WrapAxis;
import com.jme3.texture.TextureImage;
Expand Down Expand Up @@ -3535,6 +3536,32 @@ public void modifyTexture(Texture2D dest, Image src, int destX, int destY,
srcX, srcY, areaW, areaH, linearizeSrgbImages, dest.getImage().getColorSpace());
}

@Override
public void modifyTexture(TextureArray dest, Image pixels, int layer, int x, int y) {
if (!caps.contains(Caps.TextureArray) || gl2 == null) {
throw new UnsupportedOperationException("Texture arrays are not supported by this renderer");
}
if (dest == null || dest.getImage() == null || pixels == null) {
throw new IllegalArgumentException("Source and destination images must not be null");
}
if (dest.getType() != Texture.Type.TwoDimensionalArray
|| dest.getMinFilter().usesMipMapLevels()) {
throw new UnsupportedOperationException("Only non-mipmapped texture arrays can be updated");
}
// Validation precedes binding: setTexture can allocate/reupload the entire array.
GLImageFormat format = texUtil.validateArrayLayerUpdate(dest.getImage(), pixels,
layer, x, y, linearizeSrgbImages);
try {
setTexture(0, dest);
} catch (TextureUnitException exception) {
throw new IllegalStateException("Renderer has no texture unit zero", exception);
}
// setTexture may reuse unit zero's cached binding without making that
// unit active. Subimage writes must target the actual active unit.
bindTextureAndUnit(GLExt.GL_TEXTURE_2D_ARRAY_EXT, dest.getImage(), 0);
texUtil.uploadArrayLayer(pixels, format, layer, x, y);
}

@Override
public void deleteImage(Image image) {
int texId = image.getId();
Expand Down
77 changes: 77 additions & 0 deletions jme3-core/src/main/java/com/jme3/renderer/opengl/TextureUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -563,4 +563,81 @@ void uploadSubTexture(int target, Image src, int index, int targetX, int targetY

}

/** Validates a base-level array update without binding or issuing GL commands. */
GLImageFormat validateArrayLayerUpdate(Image dest, Image src, int layer, int x, int y,
boolean linearizeSrgb) {
if (src.getFormat() == null || dest.getFormat() == null
|| dest.getFormat() != src.getFormat() || dest.getColorSpace() != src.getColorSpace()) {
throw new IllegalArgumentException("Array update format and color space must match "
+ "the destination");
}
if (src.getFormat().isCompressed() || src.getFormat().isDepthFormat()
|| src.getDepth() > 1 || dest.getDepth() > 1
|| src.getMultiSamples() > 1 || dest.getMultiSamples() > 1
|| src.getMipMapSizes() != null || dest.getMipMapSizes() != null
|| src.isGeneratedMipmapsRequired() || dest.isGeneratedMipmapsRequired()) {
throw new UnsupportedOperationException("Only single-sample, non-mipmapped "
+ "2D color layers can be updated");
}
if (src.getWidth() <= 0 || src.getHeight() <= 0
|| dest.getWidth() <= 0 || dest.getHeight() <= 0
|| x < 0 || y < 0
|| x > dest.getWidth() - src.getWidth()
|| y > dest.getHeight() - src.getHeight()) {
throw new IllegalArgumentException("The array update rectangle is outside the destination");
}
if (dest.getData() == null || layer < 0 || layer >= dest.getData().size()
|| src.getData() == null || src.getData().size() != 1) {
throw new IllegalArgumentException("Invalid destination layer or source image buffer count");
}
validateLayerBuffer(src.getData(0), src);
if (dest.isUpdateNeeded() || dest.getId() == -1) {
// Initial/full uploads already visit every layer. Resident updates stay O(1).
for (ByteBuffer data : dest.getData()) {
validateLayerBuffer(data, dest);
}
}
boolean srgb = dest.getColorSpace() == ColorSpace.sRGB && linearizeSrgb;
GLImageFormat format = getImageFormatWithError(src.getFormat(), srgb);
// Bound integer scratch calculations before setTexture can mutate native state.
if (requiresPackedExpansion(src.getFormat(), format)) {
long components = src.getFormat() == Format.RGB565 ? 3 : 4;
if ((long) src.getWidth() * src.getHeight() * components > Integer.MAX_VALUE
|| ((dest.isUpdateNeeded() || dest.getId() == -1)
&& (long) dest.getWidth() * dest.getHeight() * components > Integer.MAX_VALUE)) {
throw new IllegalArgumentException("Expanded array update or backing layer is too large");
}
}
return format;
}

private static void validateLayerBuffer(ByteBuffer data, Image image) {
long pixels = (long) image.getWidth() * image.getHeight();
int bytesPerPixel = image.getFormat().getBitsPerPixel() / 8;
if (data == null || !data.isDirect() || pixels <= 0 || bytesPerPixel <= 0
|| pixels > data.capacity() / bytesPerPixel) {
throw new IllegalArgumentException("A complete direct buffer is required for each image layer");
}
}

/** Uploads one validated tightly packed patch. Renderer owns texture binding/unpack state. */
void uploadArrayLayer(Image src, GLImageFormat format, int layer, int x, int y) {
ByteBuffer data = src.getData(0).duplicate();
data.clear();
data.limit(Math.multiplyExact(Math.multiplyExact(src.getWidth(), src.getHeight()),
src.getFormat().getBitsPerPixel() / 8));
ByteBuffer expanded = null;
try {
if (requiresPackedExpansion(src.getFormat(), format)) {
expanded = expandPackedPixels(data, src.getFormat(),
Math.multiplyExact(src.getWidth(), src.getHeight()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This direct upload assumes the current GL_UNPACK_ALIGNMENT happens to match the patch row stride. OpenGL defaults to alignment 4, so tightly packed RGB8 patches with widths whose 3-byte row size is not divisible by 4 will be read with padding between rows (and can overrun the supplied patch data); the result can also depend on whatever unpack state a previous upload left behind. Please establish the correct unpack alignment for this upload (and restore or otherwise consistently manage the prior GL state), as the existing source contract is tightly packed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are settings gl_unpack_alignment 1 in the renderer initialize:
https://github.com/8Keep/jmonkeyengine/blob/308dced919d190d3bc89c4daa1311b7c86b8f059/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java#L951-L957

We should probably change this for perf? But I think for now this should be fine

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing that out—I missed the renderer’s initialization setting. Since GL_UNPACK_ALIGNMENT is set to 1 during initialization, the tightly packed patch rows are handled correctly under the renderer’s normal GL-state assumptions, so my concern about the default alignment doesn’t apply here. I’ll withdraw that finding; thanks for clarifying!

}
gl2.glTexSubImage3D(GLExt.GL_TEXTURE_2D_ARRAY_EXT, 0, x, y, layer,
src.getWidth(), src.getHeight(), 1, format.format, format.dataType,
expanded != null ? expanded : data);
} finally {
releasePackedPixels(expanded);
}
}

}
6 changes: 6 additions & 0 deletions jme3-core/src/main/java/com/jme3/system/NullRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.TextureArray;
import com.jme3.texture.TextureImage;
import com.jme3.util.NativeObject;

Expand Down Expand Up @@ -184,6 +185,11 @@ public void setTextureImage(int unit, TextureImage tex) throws TextureUnitExcept
public void modifyTexture(Texture tex, Image pixels, int x, int y) {
}

@Override
public void modifyTexture(TextureArray dest, Image pixels, int layer, int x, int y) {
// GPU-only operation: there is no storage to update in the null renderer.
}

@Override
public void updateBufferData(VertexBuffer vb) {
}
Expand Down
4 changes: 3 additions & 1 deletion jme3-core/src/main/java/com/jme3/texture/TextureArray.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ public TextureArray(List<Image> images) {
ColorSpace colorSpace = images.get(0).getColorSpace();
int[] mipMapSizes = images.get(0).getMipMapSizes();
Image arrayImage = new Image(format, width, height, null, colorSpace);
arrayImage.setMipMapSizes(mipMapSizes);
if (mipMapSizes != null) {
arrayImage.setMipMapSizes(mipMapSizes);
}

for (Image img : images) {
if (img.getHeight() != height || img.getWidth() != width) {
Expand Down
Loading
Loading