Skip to content

Fix texture corruption when GL_UNPACK_ROW_LENGTH is left at its default of 0#872

Open
Chaos02 wants to merge 1 commit into
xCollateral:devfrom
Chaos02:fix/gl-unpack-row-length
Open

Fix texture corruption when GL_UNPACK_ROW_LENGTH is left at its default of 0#872
Chaos02 wants to merge 1 commit into
xCollateral:devfrom
Chaos02:fix/gl-unpack-row-length

Conversation

@Chaos02

@Chaos02 Chaos02 commented Jul 16, 2026

Copy link
Copy Markdown

The GL rule

GL_UNPACK_ROW_LENGTH defaults to 0, and 0 does 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)V overload) takes the 0 literally and uses it as a real stride.

Failure chain

With unpackRowLength == 0:

  1. uploadSize = (unpackRowLength * height - unpackSkipPixels) * formatSize collapses to 0.
  2. stagingBuffer.copyBuffer(0, srcPtr) stages zero bytes, and because nothing is written it does not advance the staging offset.
  3. srcOffset = stagingBuffer.getOffset() therefore still points at whatever earlier upload currently occupies the shared staging buffer.
  4. ImageUtil.copyBufferToImageCmd(..., srcOffset, unpackRowLength /* 0 */, height) sets bufferRowLength(0), which in Vulkan means "tightly packed to imageExtent.width".
  5. The GPU dutifully copies width * height * formatSize bytes 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

VkCommandEncoder calls _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 with UNPACK_ROW_LENGTH left 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 passing unpackRowLength == width (1x1, 5x8, 16x16, 128x128), and only the minimap's 64x64 tiles passing 0, which is exactly the set of uploads that staged nothing:

width=64 height=64 unpackRowLength(orig)=0 rowLength(normalized)=64 formatSize=4 uploadSize=16384   <== would have staged 0 bytes

With the normalization applied, the minimap renders correctly.

The fix

Normalize the row length once at the top of the overload:

final int rowLength = unpackRowLength != 0 ? unpackRowLength : width;

and use it for the source skip advance and as bufferRowLength. Passing rowLength explicitly rather than forwarding the raw 0 is valid: rowLength == width satisfies Vulkan's bufferRowLength == 0 || bufferRowLength >= imageExtent.width rule.

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: srcPtr has already been advanced past unpackSkipRows full rows, so counting rowLength * height from the advanced pointer reads up to unpackSkipRows * rowLength * formatSize bytes past the end of the source. The region actually read spans rowLength * (height - 1) + width pixels, which is what this PR uses. This was masked before because the over-read only bit when unpackSkipRows was 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.getByteBuffer already advances the pointer by the unpack skip, and VulkanImage then advances srcPtr by the same skip again, so the non-PBO long pixels path double-skips.
  • VkGlTexture.pixelStoreI silently ignores GL_UNPACK_ALIGNMENT (3317), handling only 3314, 3315 and 3316.

Based on dev.

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.
Copilot AI review requested due to automatic review settings July 16, 2026 12:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Chaos02

Chaos02 commented Jul 16, 2026

Copy link
Copy Markdown
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants