Skip to content

Commit ed94043

Browse files
committed
fixes and cleanup
1 parent 6691c7c commit ed94043

4 files changed

Lines changed: 312 additions & 59 deletions

File tree

jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java

Lines changed: 104 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2810,62 +2810,79 @@ private void updateTexImageData(Image img, Texture.Type type, int unit, boolean
28102810
bindTextureAndUnit(target, img, unit);
28112811

28122812
int imageSamples = img.getMultiSamples();
2813-
boolean needsMipmaps = !img.hasMipmaps() && img.isGeneratedMipmapsRequired();
2813+
boolean sourceMipmapsUsable = img.hasMipmaps() && !scaleToPot;
2814+
boolean needsMipmaps = !sourceMipmapsUsable && img.isGeneratedMipmapsRequired();
28142815
boolean hwMipmapSupported = needsMipmaps && isMipmapGenerationSupported(img.getFormat(),
28152816
linearizeSrgbImages ? img.getColorSpace() : ColorSpace.Linear);
2816-
Image cpuMipmapImage = null;
2817+
Image imageForUpload = img;
2818+
boolean cpuMipmapsGenerated = false;
28172819
if (imageSamples <= 1) {
28182820
boolean cpuMipmapFallbackFailed = false;
2819-
if (needsMipmaps && !hwMipmapSupported
2820-
&& allowCpuMipmapFallback
2821-
&& textureWasUnuploaded
2822-
&& !scaleToPot
2823-
&& MipMapGenerator.canGenerateMipmaps(img)) {
2824-
try {
2825-
cpuMipmapImage = createCpuMipmapImage(img);
2826-
MipMapGenerator.generateMipMaps(cpuMipmapImage, linearizeSrgbImages,
2827-
img.getColorSpace() == ColorSpace.sRGB);
2828-
img.setMipmapsGenerated(true);
2829-
} catch (RuntimeException exception) {
2830-
cpuMipmapImage = null;
2831-
cpuMipmapFallbackFailed = true;
2832-
logger.log(Level.WARNING,
2833-
"Texture " + img + " requires mipmaps, but hardware mipmap generation is not supported"
2834-
+ " and CPU mipmap generation failed. Mipmaps will not be generated.",
2835-
exception);
2821+
if (needsMipmaps) {
2822+
/*
2823+
* Some formats cannot use glGenerateMipmap because they are not both
2824+
* renderable and filterable. On a first upload, fall back to a CPU-built
2825+
* mip chain when the image data is suitable. If NPOT scaling is also
2826+
* required, build the CPU mips from the resized upload image.
2827+
*/
2828+
boolean needsCpuMipmapFallback = !hwMipmapSupported
2829+
&& allowCpuMipmapFallback
2830+
&& textureWasUnuploaded
2831+
&& MipMapGenerator.canGenerateMipmaps(img);
2832+
if (needsCpuMipmapFallback) {
2833+
try {
2834+
Image cpuMipmapUploadImage = cloneImageForUpload(img, scaleToPot);
2835+
if (cpuMipmapUploadImage != null) {
2836+
MipMapGenerator.generateMipMaps(cpuMipmapUploadImage, linearizeSrgbImages,
2837+
img.getColorSpace() == ColorSpace.sRGB);
2838+
imageForUpload = cpuMipmapUploadImage;
2839+
cpuMipmapsGenerated = true;
2840+
scaleToPot = false;
2841+
img.setMipmapsGenerated(true);
2842+
}
2843+
} catch (RuntimeException exception) {
2844+
cpuMipmapFallbackFailed = true;
2845+
logger.log(Level.WARNING,
2846+
"Texture " + img + " requires mipmaps, but hardware mipmap generation is not supported"
2847+
+ " and CPU mipmap generation failed. Mipmaps will not be generated.",
2848+
exception);
2849+
}
28362850
}
2837-
}
2838-
2839-
if (needsMipmaps && hwMipmapSupported) {
2840-
// Image does not have mipmaps, but they are required.
2841-
// Generate from base level.
28422851

2843-
if (!caps.contains(Caps.FrameBuffer) && gl2 != null) {
2852+
/*
2853+
* Old desktop GL without FBO support can auto-generate mipmaps during
2854+
* texture upload. Newer paths generate explicitly after upload below.
2855+
*/
2856+
if (hwMipmapSupported && !caps.contains(Caps.FrameBuffer) && gl2 != null) {
28442857
gl2.glTexParameteri(target, GL2.GL_GENERATE_MIPMAP, GL.GL_TRUE);
28452858
img.setMipmapsGenerated(true);
2846-
} else {
2847-
// For OpenGL3 and up.
2848-
// We'll generate mipmaps via glGenerateMipmapEXT (see below)
28492859
}
2850-
} else if (caps.contains(Caps.OpenGL20) || caps.contains(Caps.OpenGLES30)) {
2851-
if (img.hasMipmaps() || cpuMipmapImage != null) {
2852-
int mipCount = cpuMipmapImage != null
2853-
? cpuMipmapImage.getMipMapSizes().length
2854-
: img.getMipMapSizes().length;
2855-
// Image already has mipmaps, set the max level based on the
2856-
// number of mipmaps we have.
2857-
gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, mipCount - 1);
2858-
} else {
2859-
// Image does not have mipmaps, and they are not required.
2860-
// Specify that the texture has no mipmaps.
2861-
gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, 0);
2862-
}
2863-
}
2864-
if (needsMipmaps && !hwMipmapSupported) {
2865-
if (!img.hasMipmaps() && cpuMipmapImage == null && !cpuMipmapFallbackFailed) {
2860+
2861+
if (!hwMipmapSupported
2862+
&& !sourceMipmapsUsable
2863+
&& !cpuMipmapsGenerated
2864+
&& !cpuMipmapFallbackFailed) {
28662865
logger.log(Level.WARNING, "Texture " + img + " requires mipmaps, but hardware mipmaps generation is not supported. Mipmaps will not be generated.");
28672866
}
28682867
}
2868+
2869+
/*
2870+
* Clamp the mip range to the levels actually uploaded. This is still
2871+
* needed when mipmaps are not requested, otherwise GL may sample
2872+
* missing levels left from a previous texture state. When hardware
2873+
* mipmap generation is pending, reopen the full generated range in
2874+
* case an earlier upload clamped this texture to the base level.
2875+
*/
2876+
boolean canSetTextureMaxLevel = caps.contains(Caps.OpenGL20) || caps.contains(Caps.OpenGLES30);
2877+
boolean hasUploadMipmaps = sourceMipmapsUsable || cpuMipmapsGenerated;
2878+
int uploadWidth = scaleToPot ? FastMath.nearestPowerOfTwo(img.getWidth()) : imageForUpload.getWidth();
2879+
int uploadHeight = scaleToPot ? FastMath.nearestPowerOfTwo(img.getHeight()) : imageForUpload.getHeight();
2880+
int maxLevel = textureMaxLevelForUpload(canSetTextureMaxLevel, needsMipmaps, hwMipmapSupported,
2881+
hasUploadMipmaps, cpuMipmapsGenerated ? imageForUpload.getMipMapSizes() : img.getMipMapSizes(),
2882+
generatedMipMaxLevel(uploadWidth, uploadHeight, imageForUpload.getDepth()));
2883+
if (maxLevel >= 0) {
2884+
gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, maxLevel);
2885+
}
28692886
} else {
28702887
// Check if graphics card doesn't support multisample textures
28712888
if (!caps.contains(Caps.TextureMultisample)) {
@@ -2906,13 +2923,8 @@ private void updateTexImageData(Image img, Texture.Type type, int unit, boolean
29062923
}
29072924
}
29082925

2909-
Image imageForUpload;
2910-
if (cpuMipmapImage != null) {
2911-
imageForUpload = cpuMipmapImage;
2912-
} else if (scaleToPot) {
2926+
if (scaleToPot) {
29132927
imageForUpload = MipMapGenerator.resizeToPowerOf2(img);
2914-
} else {
2915-
imageForUpload = img;
29162928
}
29172929
if (target == GL.GL_TEXTURE_CUBE_MAP) {
29182930
List<ByteBuffer> data = imageForUpload.getData();
@@ -2947,14 +2959,12 @@ private void updateTexImageData(Image img, Texture.Type type, int unit, boolean
29472959
img.setMultiSamples(imageSamples);
29482960
}
29492961

2950-
if (caps.contains(Caps.FrameBuffer) || gl2 == null) {
2951-
if (needsMipmaps && img.getData(0) != null
2952-
&& !img.isMipmapsGenerated()) {
2953-
if (hwMipmapSupported) {
2954-
glfbo.glGenerateMipmapEXT(target);
2955-
img.setMipmapsGenerated(true);
2956-
}
2957-
}
2962+
if (needsMipmaps && hwMipmapSupported
2963+
&& (caps.contains(Caps.FrameBuffer) || gl2 == null)
2964+
&& img.getData(0) != null
2965+
&& !img.isMipmapsGenerated()) {
2966+
glfbo.glGenerateMipmapEXT(target);
2967+
img.setMipmapsGenerated(true);
29582968
}
29592969

29602970
img.clearUpdateNeeded();
@@ -2965,9 +2975,44 @@ private boolean isMipmapGenerationSupported(Image.Format format, ColorSpace colo
29652975
return gf != null && gf.colorRenderable && gf.filterable;
29662976
}
29672977

2968-
private Image createCpuMipmapImage(Image image) {
2978+
static int textureMaxLevelForUpload(boolean canSetTextureMaxLevel,
2979+
boolean needsMipmaps,
2980+
boolean hwMipmapSupported,
2981+
boolean hasUploadMipmaps,
2982+
int[] uploadMipMapSizes,
2983+
int generatedMipMaxLevel) {
2984+
if (!canSetTextureMaxLevel) {
2985+
return -1;
2986+
}
2987+
if (needsMipmaps && hwMipmapSupported) {
2988+
return generatedMipMaxLevel;
2989+
}
2990+
if (!hasUploadMipmaps) {
2991+
return 0;
2992+
}
2993+
return uploadMipMapSizes.length - 1;
2994+
}
2995+
2996+
static int generatedMipMaxLevel(int width, int height, int depth) {
2997+
int maxDimension = Math.max(Math.max(width, height), Math.max(1, depth));
2998+
int maxLevel = 0;
2999+
while (maxDimension > 1) {
3000+
maxDimension >>= 1;
3001+
maxLevel++;
3002+
}
3003+
return maxLevel;
3004+
}
3005+
3006+
private Image cloneImageForUpload(Image image, boolean scaleToPot) {
3007+
if (scaleToPot) {
3008+
return MipMapGenerator.resizeToPowerOf2(image);
3009+
}
3010+
29693011
ArrayList<ByteBuffer> data = new ArrayList<>(image.getData().size());
29703012
for (ByteBuffer buffer : image.getData()) {
3013+
if (buffer == null) {
3014+
return null;
3015+
}
29713016
data.add(buffer.duplicate());
29723017
}
29733018
return new Image(image.getFormat(), image.getWidth(), image.getHeight(), image.getDepth(),

jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,22 @@ private static void validateImage(Image image) {
452452
+ " bits per pixel"
453453
);
454454
}
455+
456+
int baseLevelSize = levelSize(image.getFormat(), image.getWidth(), image.getHeight());
457+
for (int dataIndex = 0; dataIndex < image.getData().size(); dataIndex++) {
458+
ByteBuffer data = image.getData(dataIndex);
459+
if (data == null) {
460+
throw new IllegalArgumentException("Image data buffer " + dataIndex + " is null");
461+
}
462+
if (data.capacity() < baseLevelSize) {
463+
throw new IllegalArgumentException(
464+
"Image data buffer " + dataIndex + " is smaller than expected base level size. Data capacity="
465+
+ data.capacity()
466+
+ ", expected="
467+
+ baseLevelSize
468+
);
469+
}
470+
}
455471
}
456472

457473
private static int levelSize(Image.Format format, int width, int height) {
@@ -491,6 +507,9 @@ private static int levelSize(Image.Format format, int width, int height) {
491507
* For rebuilding mipmaps, we only want the base level.
492508
*/
493509
private static ByteBuffer copyBaseLevel(ByteBuffer source, int baseLevelSize) {
510+
if (source == null) {
511+
throw new IllegalArgumentException("Image data buffer is null");
512+
}
494513
if (source.capacity() < baseLevelSize) {
495514
throw new IllegalArgumentException(
496515
"Image data is smaller than expected base level size. Data capacity="
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (c) 2009-2026 jMonkeyEngine
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are
7+
* met:
8+
*
9+
* * Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
*
12+
* * Redistributions in binary form must reproduce the above copyright
13+
* notice, this list of conditions and the following disclaimer in the
14+
* documentation and/or other materials provided with the distribution.
15+
*
16+
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17+
* may be used to endorse or promote products derived from this software
18+
* without specific prior written permission.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22+
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23+
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24+
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25+
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26+
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27+
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28+
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31+
*/
32+
package com.jme3.renderer.opengl;
33+
34+
import org.junit.jupiter.api.Test;
35+
36+
import static org.junit.jupiter.api.Assertions.assertEquals;
37+
38+
public class GLRendererMipmapPolicyTest {
39+
40+
@Test
41+
public void testDoesNotClampWhenHardwareMipmapGenerationIsPending() {
42+
int maxLevel = GLRenderer.textureMaxLevelForUpload(
43+
true,
44+
true,
45+
true,
46+
false,
47+
null,
48+
7);
49+
50+
assertEquals(7, maxLevel);
51+
}
52+
53+
@Test
54+
public void testClampsToBaseLevelWhenNoMipmapsAreUploaded() {
55+
int maxLevel = GLRenderer.textureMaxLevelForUpload(
56+
true,
57+
false,
58+
false,
59+
false,
60+
null,
61+
4);
62+
63+
assertEquals(0, maxLevel);
64+
}
65+
66+
@Test
67+
public void testUsesUploadedMipCountForExistingOrCpuMipmaps() {
68+
int maxLevel = GLRenderer.textureMaxLevelForUpload(
69+
true,
70+
true,
71+
false,
72+
true,
73+
new int[] {64, 16, 4, 1},
74+
7);
75+
76+
assertEquals(3, maxLevel);
77+
}
78+
79+
@Test
80+
public void testSkipsMaxLevelWhenCapabilityIsUnavailable() {
81+
int maxLevel = GLRenderer.textureMaxLevelForUpload(
82+
false,
83+
false,
84+
false,
85+
true,
86+
new int[] {64, 16},
87+
7);
88+
89+
assertEquals(-1, maxLevel);
90+
}
91+
92+
@Test
93+
public void testClampsToBaseLevelWhenRequestedMipmapsCannotBeGeneratedOrUploaded() {
94+
int maxLevel = GLRenderer.textureMaxLevelForUpload(
95+
true,
96+
true,
97+
false,
98+
false,
99+
null,
100+
7);
101+
102+
assertEquals(0, maxLevel);
103+
}
104+
105+
@Test
106+
public void testGeneratedMipMaxLevelUsesLargestUploadDimension() {
107+
assertEquals(0, GLRenderer.generatedMipMaxLevel(1, 1, 1));
108+
assertEquals(2, GLRenderer.generatedMipMaxLevel(3, 5, 1));
109+
assertEquals(3, GLRenderer.generatedMipMaxLevel(4, 8, 1));
110+
assertEquals(4, GLRenderer.generatedMipMaxLevel(4, 8, 16));
111+
}
112+
}

0 commit comments

Comments
 (0)