Repository navigation
Fix GLES3 packed sRGB texture transfers - #2986
Conversation
| gl.glPixelStorei(GL.GL_UNPACK_ROW_LENGTH, 0); | ||
| } | ||
| data.position(cpos); | ||
|
|
There was a problem hiding this comment.
One thing I want to double-check here: this method used to end with data.position(cpos);, so the caller's Image buffer was handed back at the position it came in with. With that line gone, even the plain non-sRGB paths (desktop / linear destination, where nothing is converted) now leave the source buffer already consumed — GLRenderer.modifyTexture passes in a buffer the caller still holds.
Does the new packed-sRGB helper restore the position, or should we keep the restore at the end of this method so the buffer state contract stays as it was for every caller?
| int target = convertTextureType(tex.getType(), pixels.getMultiSamples(), -1); | ||
| texUtil.uploadSubTexture(target, pixels, 0, x, y, | ||
| 0, 0, pixels.getWidth(), pixels.getHeight(), linearizeSrgbImages); | ||
| 0, 0, pixels.getWidth(), pixels.getHeight(), linearizeSrgbImages, tex.getImage().getColorSpace()); |
There was a problem hiding this comment.
Nullability heads-up for the new argument: Image.getColorSpace() can legitimately return null. Image.read() restores it with a null default (colorSpace = capsule.readEnum("colorSpace", ColorSpace.class, null);), and ColorSpace only has sRGB/Linear.
If the packed-transfer check does anything other than destColorSpace == ColorSpace.sRGB, an image loaded from a .j3f that never wrote a color space would throw an NPE mid-upload. Can you make that comparison null-safe (a null/unknown color space then just means "don't convert")?
jaime-jmebot
left a comment
There was a problem hiding this comment.
Nice, well-scoped fix — expanding the packed pixels on the CPU keeps the SRGB8_ALPHA8 storage instead of silently falling back to linear, and the byte-order handling looks right.
A few things to look at (details inline):
- The
data.position(cpos)restore at the end ofuploadSubTextureis gone; worth confirming the caller's buffer still comes back where it started on the non-sRGB paths. - The new destination
ColorSpaceargument should be null-safe —Image.read()can restore it asnull. - Sanity-check memory on mobile: expanding a full mip level allocates a fresh byte buffer per level/slice. A reused scratch buffer (or converting row-by-row) would keep peak memory down for large textures.
The test coverage looks great — full 65,536-value sweeps per format, both byte-order metadata settings, cropped strides, volumes/array/cubemap slices and the desktop/linear controls are exactly the cases that are easy to regress.
|
Additional offscreen pixel validation of 48ccd63 against baseline e8cf975: The actual pinned Java GLRenderer/TextureUtil classes ran through a test-only JNA bridge to surfaceless EGL and Mesa 25.0.7 llvmpipe (LLVM 19.1.7, OpenGL ES 3.2). A fixture shader sampled the textures into a linear RGBA8 framebuffer for readback.
This is software-driver upload/sampling validation. Production LWJGL/Android integration, hardware GPUs and a forced ES3.0-only context remain untested here. Mip, array and volume paths remain covered by the separate recording-GL tests. No performance or CI result is claimed. |
|
Checked all three points and pushed b2d7a5b:
The scratch retains the largest requested expansion until cleanup; cropped updates still expand the full source image. Release uses the existing BufferUtils allocator. The default ReflectionAllocator cannot explicitly free on this modular JVM, so this is not a hard native-memory bound or a mobile memory/performance measurement. Validation: all 24 focused tests, full core tests (506 passed, one skipped), six effects tests and renderer Checkstyle pass. The same 10-case Mesa llvmpipe ES3.2/JNA smoke was rerun against these renderer sources: all 44 readbacks pass with the previously documented RGB tolerance and exact alpha. |
|
In this PR the scratch buffer grows and never shrinks until cleanup. Cropped subimages uploads also counts the entire source image instead of the region, wasting memory. I think you should allocate on first upload a scratch buffer with a reasonable size (eg. 32 mb) and keep it forever. When it needs a bigger scratch buffer you should allocate a temporary one on-demand and destroy it after use. |
|
@riccardobl Addressed in 9d541c8. The conversion scratch is now allocated lazily at a fixed 32 MiB and retained until renderer cleanup. Larger uploads use an exact-size temporary, released through BufferUtils in finally on success or failure. Cropped subimages now expand only the requested rows and pixels into one contiguous upload, with scratch sizing based on the crop. All 31 focused tests pass, including boundary sizes, crop-only conversion and exception cleanup. Broader local checks: 512 core tests passed, one existing skip; the network-dependent locator test was excluded after DNS failures. All six effects tests pass, and renderer Checkstyle reports no errors or new warnings. Release still depends on the configured BufferUtils allocator; the default ReflectionAllocator on this modular JVM cannot guarantee immediate native deallocation. No GPU or performance validation was rerun for this change. |
Summary
GLES3 accepts only
UNSIGNED_BYTEtransfers forSRGB8andSRGB8_ALPHA8, but RGB565 and RGB5A1 sRGB images were registered with packed unsigned-short types. Uploading those combinations producesGL_INVALID_OPERATION.SRGB8_ALPHA8storageThe transfer restrictions are specified in OpenGL ES 3.0.6 Table 3.2 and §3.8.3. Subimage transfers are checked against the existing destination storage (§3.8.5). This preserves sRGB storage without a silent linear fallback or CPU gamma conversion.
Validation
jme3-core:test: 502 cases, zero failures, one skippedjme3-effects:test: 6 cases, zero failuresThis branch starts directly from
e8cf975beon upstreammasterand contains only this fix and its tests.