Fix texture corruption when GL_UNPACK_ROW_LENGTH is left at its default of 0#872
Open
Chaos02 wants to merge 1 commit into
Open
Fix texture corruption when GL_UNPACK_ROW_LENGTH is left at its default of 0#872Chaos02 wants to merge 1 commit into
Chaos02 wants to merge 1 commit into
Conversation
In GL, UNPACK_ROW_LENGTH defaults to 0, which means "rows are tightly packed", i.e. the row stride equals the width of the transferred region. VulkanImage took the 0 literally and used it as an actual stride. With unpackRowLength == 0 the upload size collapsed to 0, so StagingBuffer.copyBuffer staged nothing and left the staging offset unchanged. The srcOffset handed to copyBufferToImageCmd therefore still pointed at whatever previous upload occupied the shared staging buffer, and bufferRowLength was passed as 0, which in Vulkan means "tightly packed to imageExtent.width". The GPU then copied width * height * formatSize bytes of stale staging memory into the target image, filling the texture with unrelated data from earlier uploads. VulkanMod's own callers never hit this because VkCommandEncoder issues _pixelStore(3314, width) before each upload, so a nonzero row length is always supplied. Mods that leave UNPACK_ROW_LENGTH at the GL default are silently corrupted instead. Normalize the row length to width when it is 0, and use the normalized value for the source skip advance and as bufferRowLength. Passing rowLength explicitly is valid since rowLength == width satisfies the bufferRowLength == 0 || >= imageExtent.width rule. Also correct the upload size. (rowLength * height - unpackSkipPixels) mixed a row stride term with a pixel skip term, and because srcPtr has already been advanced past the skip, it over-read the source by up to unpackSkipRows * rowLength * formatSize bytes. The region actually read spans rowLength * (height - 1) + width pixels.
Author
|
Related PRs from the same effort (getting a VulkanMod based 1.21.1 pack working):
Worth noting for context: this is the PR that actually fixes Xaero's map rendering. On #870 I had initially assumed the cause was an FBO deferred attach issue and said so there. I built that fix first and it did not help, and tracing the FBO lifecycle in game showed it was healthy. The row length handling here is what it turned out to be. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The GL rule
GL_UNPACK_ROW_LENGTHdefaults to0, and0does not mean "zero length". It means "rows are tightly packed", i.e. the row stride is the width of the region being transferred.VulkanImage.uploadSubTextureAsync(the terminal(IIIIIIIIIJ)Voverload) takes the0literally and uses it as a real stride.Failure chain
With
unpackRowLength == 0:uploadSize = (unpackRowLength * height - unpackSkipPixels) * formatSizecollapses to0.stagingBuffer.copyBuffer(0, srcPtr)stages zero bytes, and because nothing is written it does not advance the staging offset.srcOffset = stagingBuffer.getOffset()therefore still points at whatever earlier upload currently occupies the shared staging buffer.ImageUtil.copyBufferToImageCmd(..., srcOffset, unpackRowLength /* 0 */, height)setsbufferRowLength(0), which in Vulkan means "tightly packed toimageExtent.width".width * height * formatSizebytes of stale staging memory into the target image.The result is not noise. The texture ends up filled with recognizable but unrelated data from earlier uploads (block atlas, GUI, mob textures) at the wrong effective resolution.
Why VulkanMod itself never trips over this
VkCommandEncodercalls_pixelStore(3314, width)before every upload, so VulkanMod's own call paths always pass a nonzero row length. The bug is invisible from inside the mod. Any mod that uploads withUNPACK_ROW_LENGTHleft at the GL default is silently corrupted instead, and has no way to tell that it is expected to set a value GL says is optional.Reproduction and runtime evidence
Reproduced on MC 1.21.1 with VulkanMod 0.6.7 using Xaero's Minimap, which uploads its map tiles without touching
UNPACK_ROW_LENGTH. Logging the terminal overload's arguments shows every Minecraft-originated upload passingunpackRowLength == width(1x1, 5x8, 16x16, 128x128), and only the minimap's 64x64 tiles passing0, which is exactly the set of uploads that staged nothing:With the normalization applied, the minimap renders correctly.
The fix
Normalize the row length once at the top of the overload:
and use it for the source skip advance and as
bufferRowLength. PassingrowLengthexplicitly rather than forwarding the raw0is valid:rowLength == widthsatisfies Vulkan'sbufferRowLength == 0 || bufferRowLength >= imageExtent.widthrule.The upload size was also wrong
(rowLength * height - unpackSkipPixels)mixes a row stride term with a pixel skip term, which does not describe any region. It also over-reads:srcPtrhas already been advanced pastunpackSkipRowsfull rows, so countingrowLength * heightfrom the advanced pointer reads up tounpackSkipRows * rowLength * formatSizebytes past the end of the source. The region actually read spansrowLength * (height - 1) + widthpixels, which is what this PR uses. This was masked before because the over-read only bit whenunpackSkipRowswas nonzero.Observed but deliberately not addressed
Two nearby issues turned up while tracing this. They are out of scope here to keep the change reviewable, and I am happy to follow up on either:
VkGlTexture.getByteBufferalready advances the pointer by the unpack skip, andVulkanImagethen advancessrcPtrby the same skip again, so the non-PBOlong pixelspath double-skips.VkGlTexture.pixelStoreIsilently ignoresGL_UNPACK_ALIGNMENT(3317), handling only 3314, 3315 and 3316.Based on
dev.