Skip to content

Add opt-in rendering for loaded chunk edges#3713

Open
GigaZelensky wants to merge 2 commits into
CaffeineMC:1.21.11/stablefrom
GigaZelensky:edgechunkfix/loaded-edge-sections
Open

Add opt-in rendering for loaded chunk edges#3713
GigaZelensky wants to merge 2 commits into
CaffeineMC:1.21.11/stablefrom
GigaZelensky:edgechunkfix/loaded-edge-sections

Conversation

@GigaZelensky

@GigaZelensky GigaZelensky commented Jun 8, 2026

Copy link
Copy Markdown

Summary

This adds an opt-in Render Loaded Chunk Edges performance option with three modes:

  • Off: keeps Sodium's current full-neighborhood readiness behavior.
  • Performance: renders eligible loaded edge chunks only after chunk loading has been quiet for a short cooldown.
  • Immediate: renders eligible loaded edge chunks as soon as the center chunk has complete data and all known neighbors are complete.

The goal is to reduce visible holes at loaded chunk borders when the client already has the center chunk's block and light data, while keeping the conservative behavior as the default.

Background

This came out of a small standalone Fabric mod called EdgeChunkFix, which explored the same visual problem in the vanilla renderer. At loaded chunk borders, the client can sometimes select a block and know its state, but terrain rendering still leaves that block invisible because the renderer is stricter than the client-side world data.

That standalone mod is useful for vanilla-style rendering, but it is not compatible with Sodium because both alter terrain renderer behavior around LevelRenderer.cullTerrain. Integrating a Sodium-specific version here avoids that mod compatibility trap and makes the improvement available directly to Sodium users who choose to enable it.

This PR intentionally does not port the vanilla lighting workaround from that mod. It only adjusts Sodium's chunk readiness rule for the opt-in case.

Before / After

Before, client-known edge terrain can be selectable while the terrain itself is missing:

Before: selectable but invisible edge chunk blocks

After, the already-known edge terrain renders:

After: loaded edge chunks render

Details

Sodium currently merges readiness across the 3x3 chunk neighborhood. If any neighboring chunk is absent, the merged status becomes empty, which can keep an otherwise complete center chunk out of the renderer.

With loaded-edge rendering enabled by mode:

  • the center chunk still requires FLAG_HAS_BLOCK_DATA | FLAG_HAS_LIGHT_DATA
  • known neighboring chunks must also have complete block and light data
  • missing neighbors may be treated as absent context for the provisional edge mesh
  • each ready chunk records a bitmask of which neighbors were missing when it became ready
  • when chunk status changes, the changed chunk and its 3x3 neighborhood are re-evaluated
  • if a ready chunk's missing-neighbor bitmask changes, the tracker queues a remove/add so the chunk is rebuilt with the new context

This means a mesh built while a neighbor was missing is not treated as final. Once the neighboring data arrives, the stored mask changes and the affected render chunk is rebuilt using the new neighbor data.

refreshReadyChunks() is the expensive path because it scans the tracked chunk map. It is used for global state changes such as option changes, view/load-distance context changes, and Performance mode entering or leaving its delayed edge-rendering state. Normal chunk status updates use local 3x3 updates unless Performance mode was actively rendering provisional edges and needs to globally disable them again after new chunk activity resumes.

Disabling the option restores the previous full-neighborhood readiness behavior.

Validation

  • Built with ./gradlew :fabric:build --no-daemon on Minecraft 1.21.11/Fabric.
  • Manually checked loaded/unloaded chunk border scenarios with the generated Fabric jar.

@GigaZelensky

Copy link
Copy Markdown
Author

Just to make the effect clearer, here is the same area with the option disabled and enabled:

Before

image

After

image

The second screenshot is not requesting farther terrain from the server. It is rendering chunk data that the client had already received, but that Sodium previously kept hidden because a neighboring chunk was missing. In practice, this exposes an extra visible border of already-available chunks without increasing server load or requesting additional chunk data.

@GigaZelensky

Copy link
Copy Markdown
Author

By the way, I just noticed that some other PRs got marked as AI generated recently, and I realized that my Summary/Background/Details style at the top most likely matches ChatGPT exactly, ha.

Just leaving a quick note in case anyone thinks this was AI-generated. I'm just rather finicky about how I format my PRs so that they're legible, heh. Anyway, the actual code for the logic in here is all manually written based off my standalone EdgeChunkFix mod. I figured that forcing a standalone mixin into the LevelRenderer's cullTerrain method would have made this incompatible with Sodium anyway, so doing this directly was definitely easier.

Feel free to let me know if you need any changes!

@douira

douira commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

How does this handle chunk meshing since it requires the data of neighboring chunks to correctly function? We copy a margin around each section since that context is needed for meshing, so if we don't have that data, I don't see how meshing could be deterministic or even possible to do correctly in the first place. Does this end up meshing chunks on the borders of the world twice?

@GigaZelensky GigaZelensky force-pushed the edgechunkfix/loaded-edge-sections branch from 27f88ca to 32e6062 Compare June 8, 2026 23:23
@GigaZelensky GigaZelensky changed the title Render complete chunks at missing neighbor edges Add opt-in rendering for loaded chunk edges Jun 8, 2026

@douira douira left a comment

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.

How does this address the fact that chunks that have been meshed without neighboring chunks having complete data are incorrect once we receive the neighboring chunks' data that was not used on the first time the chunk was meshed?

this.refreshReadyChunks();
}

public void refreshReadyChunks() {

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.

what is the cost of this method and how often is it called?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

pushed a rework for this now. it’s not just a boolean anymore, it’s Off / Performance / Immediate.

for the correctness part, chunks meshed with missing neighbors are treated as temporary. the tracker stores which neighbors were missing when the chunk became ready. when chunk status changes, the changed chunk + its 3x3 around it are checked again. if a missing neighbor arrives, or the missing-neighbor mask changes, that chunk gets queued unload+load so it gets rebuilt with the real neighbor data. so it shouldn’t keep using the first “missing neighbor = air” mesh forever.

for refreshReadyChunks, yeah that’s the expensive path since it scans the tracked chunk map. normal chunk status updates use local 3x3 updates. the full refresh is for bigger state changes like option changes, view/load-distance changes, Performance mode turning edge rendering off when chunks start changing again, and turning it back on after the quiet period.

the modes are:
Off = current behavior
Performance = waits until chunk loading has been quiet for a bit before rendering these edge chunks
Immediate = renders them as soon as the center chunk is complete, so it can do more rebuilds while chunks are still loading

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