Skip to content

Distant Horizons

BonsUnleashed edited this page Oct 7, 2026 · 5 revisions

Distant Horizons

Minecraft 1.20.1 / Forge: this page documents that build and its measurements. For the separate 170-control Minecraft 1.21.1 port, see Minecraft 1.21.1 NeoForge.

Seventeen controls for Distant Horizons 3.3.2 (mod id distanthorizons). They cover cloud culling and cloud groups, the rough-surface generator, LOD building and loading (biome memo, pooled names, lock-free reads, sorted faces, air flags), biome blending, the update queue, direct chunk reads next to C2ME, and two fixes: one empties its biome caches when a world is left, the other asks a server again for a reply that was lost while joining. The fixes that other mods need inside Distant Horizons' generation regions are explained on Generation context fixes. Every switch checks the code it would change against the tested build; another build is left untouched with one WARN line (see How the patches are applied).

Key Target Side Since Kind
distanthorizons_adjacent_face_skip Distant Horizons 3.3.2 CLIENT 1.0.29 opt
distanthorizons_biome_blend_memo Distant Horizons 3.3.2 CLIENT 1.0.28 opt
distanthorizons_c2me_direct_reads Distant Horizons 3.3.2 with C2ME 0.2.0+alpha.12 BOTH 1.0.25 opt
distanthorizons_cloud_pass_invariants Distant Horizons 3.3.2 CLIENT 1.0.23 opt
distanthorizons_cloud_scalars Distant Horizons 3.3.2 CLIENT 1.0.4 opt
distanthorizons_ignored_dimension_match Distant Horizons 3.3.2 BOTH 1.0.6 opt
distanthorizons_join_config_resend Distant Horizons 3.3.2 CLIENT 1.0.34 fix
distanthorizons_lod_biome_memo Distant Horizons 3.3.2 BOTH 1.0.28 opt
distanthorizons_pooled_string_index Distant Horizons 3.3.2 BOTH 1.0.29 opt
distanthorizons_quad_sort_keys Distant Horizons 3.3.2 CLIENT 1.0.29 opt
distanthorizons_render_param_inverse_reuse Distant Horizons 3.3.2 CLIENT 1.0.21 opt
distanthorizons_rough_surface_xz_cache Distant Horizons 3.3.2 BOTH 1.0.23 opt
distanthorizons_sql_script_lookup_index Distant Horizons 3.3.2 BOTH 1.0.30 opt
distanthorizons_unlocked_byte_stream Distant Horizons 3.3.2 BOTH 1.0.29 opt
distanthorizons_update_queue_wait Distant Horizons 3.3.2 BOTH 1.0.26 opt
distanthorizons_world_change_biome_reset Distant Horizons 3.3.2 BOTH 1.0.30 fix
distanthorizons_wrapper_air_flag Distant Horizons 3.3.2 with Forge 47.4.16 BOTH 1.0.29 opt

distanthorizons_cloud_scalars

Since: 1.0.4 · Script (1.0.19 and earlier): furious4-CloudRenderHandler.js Patched: com.seibel.distanthorizons.core.render.renderer.CloudRenderHandler.shouldCloudBeCulled Mixins (since 1.0.20): CloudParamsAccessor, CloudRenderHandlerMixin

What upstream did. Distant Horizons' cloud culling built private temporary vectors for each cloud-corner test.

What the patch does. Performs the same operations on scalar locals through two private helpers, keeping the original arithmetic, the camera and look callbacks, the subclass callbacks, normalisation, the distance and visibility thresholds, corner order and the near-cloud exemption.

What stays the same. Every culling decision. The class is client rendering code; a dedicated server never loads it, which is why the switch is listed as BOTH without any server effect.

Measured. Warmed cloud-cull 38.92 → 37.41 ns (−3.9 %, 24 B both); cold (interpreted) 906.65 → 470.17 ns (−48.1 %) and 480 → 64 B. Warmed HotSpot already removes most of these temporaries, so the gain is mainly on the cold path (the first frames after a shader or renderer reload).


distanthorizons_ignored_dimension_match

Since: 1.0.6 · Script (1.0.19 and earlier): furious6-IgnoredDimensionCsvHandler.js Patched: com.seibel.distanthorizons.core.config.eventHandlers.IgnoredDimensionCsvHandler.dimensionNameShouldBeIgnored Mixin (since 1.0.20): IgnoredDimensionCsvHandlerMixin

What upstream did. Every ignored-dimension check copied the part of the dimension name after the first @ into a new String before comparing it with each configured entry.

What the patch does. Compares the suffix in place with an exact-length, case-insensitive regionMatches.

What stays the same. Null and empty lists, immediate configuration updates, and the render-cancellation and fog/fade callbacks that consume the answer; the configured ignored dimensions are untouched.

Measured. Prefixed ignored-dimension hit 27.99 → 21.15 ns and non-match 23.30 → 10.80 ns; 72 → 0 B per check.


distanthorizons_render_param_inverse_reuse

Since: 1.0.21 · Side: CLIENT · Mixin: RenderParamInverseMixin · Helper: bons.furious.patch.distanthorizons.MatrixMemo Patched: com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam.update (the ten-argument form)

What upstream did. For every LOD buffer it draws, Distant Horizons updates one shared DhApiBeforeBufferRenderEvent parameter object. Each update inverts two 4×4 matrices and multiplies two more, although the inputs are the same for every buffer of a render pass.

What the patch does. Redirects the two invert() calls and the multiply() call inside update into a small memo per parameter object that remembers the last inputs (exact float bits) and result of each operation, and copies the result when the inputs are identical. invert and multiply are deterministic, so every field of the parameter object gets exactly the value the original update computes, including Distant Horizons' own result for singular matrices.

What stays the same. Every field of the parameter object, the order of the event listeners and what they see. Randomized inputs with singular, NaN, infinite and negative-zero matrices, parents changed between buffers and listeners writing into the output matrices gave bit-identical fields. One thing worth knowing: for the same inputs, a NaN produced by JIT-compiled code can carry a different payload than one from the interpreter; that is the JVM, not the patch, and both are NaN.

Measured. 76 → 51 ns per buffer update on the transformed class in a plain JVM (−33 %); the review model measured 83 → 38 ns. In two client profiles the inversions were 0.47 % / 0.46 % of the render thread. In the 1.0.21 client run, 9,146 of 338,378 live buffer-render events were sampled and compared with a recomputation: all identical.


distanthorizons_rough_surface_xz_cache

Since: 1.0.23 · Side: BOTH (the client's generator plan uses this generator; a dedicated server's chunks-only plan does not) · Mixin: RoughSurfaceDensityMixin · Helpers: bons.furious.patch.distanthorizons.RoughSurfaceDensity, bons.pure.terrain.XzCache Patched: DH's DhRoughSurfaceGenerator$GenParams constructor (the density field is replaced with our cached copy) and isNoiseSolidAtBlockPos

What upstream did. DH's rough-surface generator finds each LOD column's surface with about 35 density probes at different heights, and each probe evaluates the whole terrain density, including the parts that depend only on x and z (continents, erosion, ridges and the splines over them).

What the patch does. When DH prepares a level, the density is copied once with a per-thread cache on every part that reads only x and z: the same value at the same x and z, without recomputing it at each height. The copy is only used when the density contains only vanilla function types (Tectonic's reviewed Invert counts as vanilla; see DensityAudit) and when 256 probes of the copy and the original returned the same bits; otherwise DH keeps the original and logs why.

What stays the same. The densities: 21,000 in-game densities bit-identical; DH's own surface search is unchanged.

Measured. In game the overworld density (Tectonic terrain) keeps 36,205 two-dimensional parts cached; 494 µs instead of 3,351 µs per LOD column. The Nether caches 304 parts, other dimensions 18-3,972. DH world generation spent 82 % of its time in these probes in review 8.

Since 1.0.30. The copy is made the first time Distant Horizons probes a level's surface instead of when it sets up each level: the reference pack's dedicated server holds 8.7 MB less, and 100,000 densities were bit-identical between the original, the copy made on first use and the copy made up front.


distanthorizons_cloud_pass_invariants

Since: 1.0.23 · Side: CLIENT · Mixins: CloudPassInvariantsMixin, GenericRenderPassMixin · Helper: bons.furious.patch.distanthorizons.CloudPass Patched: DH's CloudRenderHandler.preRender (two lookups redirected), marked per pass in GlGenericObjectRenderer.render; ClientLevelWrapper.getCloudColor fingerprinted

What upstream did. DH draws its clouds as 363 box groups, and every group's per-frame preparation asked for the client level wrapper (a synchronized map lookup) and computed the cloud colour as a new Color.

What the patch does. Both answers are the same for every group of a frame, so within one generic-object render pass the first answers are reused for the other groups (the same immutable Color object for all of them). Outside that pass, or for another level or partial tick, every call goes through to DH's own lookups.

What stays the same. The wrapper and the colour: 2,000 passes × 363 groups identical offline; in game the remembered wrapper and colour were used in every one of 1,994 passes.

Measured. The two lookups were 2.7-3.3 % of the render thread and 5.6 MB/s of allocation in review 8.


distanthorizons_c2me_direct_reads

Since: 1.0.25 · Target: Distant Horizons 3.3.2 with C2ME 0.2.0+alpha.12 · Side: BOTH · Kind: opt

Mixins: DistantHorizonsDirectReadsMixin

For its distant terrain, Distant Horizons reads chunks that are already saved, normally itself, on its own worker threads. Whenever C2ME is installed it switches every level ("C2ME detected" in the log) to queueing those reads on Minecraft's single chunk IO thread, and the chunk parsing that follows then runs on that thread too, in the same queue as the game's own chunk loads and saves. The reason is C2ME's replacement chunk IO; c2me.toml in this pack switches that off, and C2ME's async chunk IO and reduced-allocation serializer as well. While those three C2ME modules are off (read from C2ME's own enabled flags), Distant Horizons keeps its direct reads, exactly as without C2ME. Nothing changes when any of them is on, when another C2ME version is installed, or when C2ME is not installed.

Measured: in a client copy of a saved world (C2ME installed as here), all 625 saved chunks around the player read on Distant Horizons' direct path were identical to Minecraft's IO-thread reads; on 8 threads they took 48-165 ms against 236-455 ms queued on the IO thread (warm files, 5 runs), and Distant Horizons' chunk parsing no longer runs on that thread, which the game's own chunk loads and saves share.


distanthorizons_update_queue_wait

Since: 1.0.26 · Target: Distant Horizons 3.3.2 · Side: BOTH · Kind: opt

Mixins: UpdateQueueWaitMixin

Distant Horizons holds every chunk update back for 250 ms. Its update-queue thread only slept when both of its queues were empty, so while the closest update was still inside that window the thread popped it, put it back and went straight round again: busy work on a full core for as long as updates were waiting (during loading, pregeneration and after every burst of block changes). A round that does no work now parks the thread for about a millisecond. Same updates, same order, same LOD data; a due update leaves the queue at most about a millisecond (plus the OS timer tick) later, against the 250 ms DH waits anyway.

Measured: offline on Distant Horizons' own queue and loop thread, 600 updates left in the same order, each exactly once; while they waited out the debounce the queue thread used 203 ms of CPU, now less than one 16 ms timer tick, and a due update left 1.9 ms (mean) instead of 3.9 ms after its debounce. In game, with the switch alternating every 2 s while the world loaded and the player travelled, the queue thread used 0-2% of a core instead of 10-12% (two runs).


distanthorizons_lod_biome_memo

Since: 1.0.28 · Target: Distant Horizons 3.3.2 · Side: BOTH · Kind: opt

Mixin: LodBiomeMemoMixin

Distant Horizons LOD building reuses the biome of the quart. When Distant Horizons turns a chunk into LOD data it asks the biome of every block of every column, about 100,000 to 150,000 times per chunk, while the biome only changes every four blocks. Within one chunk conversion the previous answer is handed back when the next block is in the same column and the same quart, which is the very call that produced it. Only Distant Horizons' own Forge chunk wrapper is shortened; any other wrapper is asked every time.

Measured: 22.7 -> 11.5 ns per biome lookup on real chunk palettes (3.4 -> 1.7 ms per LOD chunk); 9,615,910 checks identical.


distanthorizons_biome_blend_memo

Since: 1.0.28 · Target: Distant Horizons 3.3.2 · Side: CLIENT · Kind: opt

Mixin: BiomeBlendMemoMixin

Distant Horizons biome blending reuses a repeated colour. For the nearest LOD rings Distant Horizons blends each point's grass, foliage and water tint over its neighbours (49 samples at the default radius) and looks up each sample's colour through three concurrent maps, although neighbours mostly share one biome. Within one point the block and the colour resolver are fixed, so the previous sample's colour is reused while the biome object is the same; "no colour" is never reused.

Measured: 733 -> 186 ns per tinted LOD point (49 samples, 4.5 biome changes per point).


distanthorizons_pooled_string_index

Since: 1.0.29 · Target: Distant Horizons 3.3.2 · Side: BOTH · Kind: opt

Mixins: PooledStringIndexMixin, StringPoolClearMixin

Distant Horizons: the biome and block names of saved LOD data are found in a table instead of letter by letter. Each time Distant Horizons reads LOD data back from its database, it turns every stored biome and block name into one shared text through a tree with a step per letter (about 40 steps per name, each a locked lookup). A table keyed by the whole name now returns the text that tree already made; a name it has not seen yet still goes through the tree once. Every caller gets the very same text as before.

Measured: offline on Distant Horizons' own classes with the 840 LOD sections of the test world and synthetic data, 8 threads included: the same text in every case; reading a stored block entry 1.3-1.5 -> 0.85-1.0 us (one name 0.71 -> 0.15 us). In the test flight the letter-by-letter lookup took about 11% of the LOD loader threads.


distanthorizons_wrapper_air_flag

Since: 1.0.29 · Target: Distant Horizons 3.3.2 with Forge 47.4.16 · Side: BOTH · Kind: opt

Mixin: WrapperAirFlagMixin

Distant Horizons: each block-state wrapper remembers whether it is air. When Distant Horizons turns LOD data into render data it asks the block of every data point whether it is air. With Forge that question goes from the block state to its block and back, two slow memory reads when they are not cached, while every other answer it needs is already stored in its wrapper. The wrapper now stores the air answer as well, for every block that uses Forge's standard air check (a fixed property of the state). Blocks with their own air check (Ars Nouveau's intangible air, Immersive Engineering's fake light) are still asked every time.

Measured: offline on Distant Horizons' own wrapper class: the same answer in every case, including a block whose answer changes; per data point 6.3 -> 2.3 ns with everything cached and 59-73 -> 9-10 ns from cold memory. In the test flight the air check took about 7% of the LOD loader threads.


distanthorizons_adjacent_face_skip

Since: 1.0.29 · Target: Distant Horizons 3.3.2 · Side: CLIENT · Kind: opt

Mixin: AdjacentFaceSkipMixin

Distant Horizons: the side faces of LOD columns skip neighbour slots that cannot change them. For each side face of each LOD column, Distant Horizons walks every slot of the neighbouring column to work out the face's lighting: all 16 slots at the finest level although real columns hold one or two points, and every point below the face, which only copies the light list unchanged. The walk now stops after the neighbour's last point and passes over points that lie wholly below the face. The faces, their light and their order stay the same.

Measured: offline on Distant Horizons' own classes: identical render data for 30,000 single faces (broken and unsorted columns included) and for 111 LOD sections of the test world; building the render data of a section 5.1-6.0 -> 4.0-4.5 ms at the finest levels and 4.5-6.1 -> 3.9-5.4 ms further out with this switch alone, 3.1-4.4 ms together with the next one. In the test flight these faces took about 23% of the LOD loader threads.


distanthorizons_quad_sort_keys

Since: 1.0.29 · Target: Distant Horizons 3.3.2 · Side: CLIENT · Kind: opt

Mixin: QuadSortKeysMixin

Distant Horizons: LOD faces are sorted by their position numbers before merging. Before merging neighbouring LOD faces, Distant Horizons sorts each direction's faces with a comparison that opens both face objects every time. Every face already carries its position as one number, so the faces are now sorted by those numbers, with their place in the list as tie-breaker, which keeps equal faces in exactly the order the original sort leaves them. Lists of more than 65,536 faces or anything unusual still use the original sort.

Measured: offline on Distant Horizons' own classes: identical order and merges for 900 lists of up to 70,000 faces and for 111 LOD sections of the test world; the merge step 0.74-1.19 -> 0.55-0.68 ms per section, the render data of a section further out 4.5-6.1 -> 3.8-5.2 ms with this switch alone. In the test flight this sort took about 8% of the LOD loader threads.


distanthorizons_unlocked_byte_stream

Since: 1.0.29 · Target: Distant Horizons 3.3.2 · Side: BOTH · Kind: opt

Mixin: UnlockedByteStreamMixin

Distant Horizons: saved LOD data is read from memory without locking for every single byte. Distant Horizons reads LOD data back from its database one byte at a time (numbers, block and biome names) through a Java in-memory stream whose every read takes a lock, although only the reading thread ever sees that stream. That stream is now a version of the same Java class that reads the same bytes in the same way without the lock.

Measured: offline on Distant Horizons' own classes: the 840 LOD sections of the test world read back identically, and 160,000 random reads (end of data included) gave the same results; reading a saved section 0.69 -> 0.50 ms, a stored block entry 0.88 -> 0.61 us (together with the name table above 1.05 -> 0.60 us).


distanthorizons_sql_script_lookup_index

Since: 1.0.30 · Target: Distant Horizons 3.3.2 · Side: BOTH · Kind: opt

Mixin: DhSqlScriptLookupMixin

Distant Horizons SQL scripts found without searching every jar. Every Distant Horizons database (four per dimension) reads DH's 13 update-script files when it opens. Forge looks such files up by asking each of the ~530 jars in turn, so loading a world here spent about 7 s of the server thread (our own recording) on those lookups. Now the folder is listed once in every jar and later lookups read that list. The same jar entry is found and read every time; jars cannot change while the game runs, and anything the list cannot answer exactly (a mod folder on disk, a file a multi-release jar redirects) is still asked directly.

Idea: lazyyyyy (faster resource lookups for named mods), idea text only

Measured: offline on Forge's own class loader over this pack's 506 jars, 66,640 checks identical (31,856 names incl. every root file, jar order, multi-release redirects, a live folder); 13 script lookups 10.5-16.3 -> 0.7-0.9 ms per database (one-time listing set-up about 50-70 ms).

Since 1.0.33. In 1.0.30 this control switched itself off at every start. The lookup index it shares with two other controls first checks the versions of two Forge libraries, and it could not read the version of one of them, so it logged game-layer resource index off and left the original lookups in place. It reads that version correctly now and the control runs.


distanthorizons_world_change_biome_reset

Since: 1.0.30 · Target: Distant Horizons 3.3.2 · Side: BOTH · Kind: fix

Mixins: SharedApiWorldChangeMixin, BiomeWrapperCachesReadyMixin, BlockBiomePairCachesReadyMixin, TintCachesReadyMixin

Distant Horizons: biome caches are emptied when a world is left, so the next world's distant terrain uses its own biomes. Distant Horizons keeps five static biome caches (biome wrappers by biome and by name, block/biome pairs, and the client's biome-by-name and tint-colour tables) that it never clears. After leaving a world or a server, the next world's distant terrain was tinted with the previous world's biomes (grass, foliage and water colours and coldness come out wrong whenever the next world or server defines a biome differently), and the previous world's whole biome registry stayed in memory until the game was closed. The caches are now emptied where Distant Horizons already resets its other world state: right after it clears its block textures when a world is unloaded, and again just before a new world's worker threads start. They refill on demand from the current world, exactly as after a fresh start; within one world nothing changes. Since 1.0.34 the reset never loads a Distant Horizons class (on the first world of a session it did, just as the server's first Distant Horizons message arrived, and that message was lost: no distant terrain from the server), and the caches keyed by name are emptied only before the next world starts.

Measured: offline on Distant Horizons' own classes through its real world change: before the fix all five caches kept the old world's entries, the next world's plains resolved to the old world's biome and that biome was never freed; with the fix every cache was empty after the unload, the next world got its own entries and the old biome was garbage collected.


distanthorizons_join_config_resend

Since: 1.0.34 · Target: Distant Horizons 3.3.2 · Side: CLIENT · Kind: fix

Mixin: SharedApiJoinConfigResendMixin

Distant Horizons: a server reply lost while joining is asked for again, so distant terrain streams in from the server. When joining a server, Distant Horizons sends its settings to the server while it creates its client world, and only afterwards starts the threads that handle the server's messages. A reply that arrives in between is dropped (a bare "warn" line in the log). That reply is the only one that tells Distant Horizons the server can send distant terrain, so without it only the chunks along the player's own path get distant terrain. Once those threads exist, the settings are sent again if no reply was handled yet; the server answers every time, and that answer is handled. When the first reply was only late, the same settings simply arrive twice. Singleplayer is not affected.

Measured: a dedicated server with a large pack, three joins per case with Distant Horizons 3.3.2. With the client held 3 s at login, so that the server registers it first (the order that loses the reply): switch off, the reply was lost in 2 of 3 joins, and in both Distant Horizons asked the server for no distant terrain; switch on, it was lost in 3 of 3 joins and all 3 handled the answer to the second sending 8-9 ms after Distant Horizons started. Without the hold the first reply came 0.7-1.6 s after the start, so the settings were sent and answered twice in all 3 joins, and all 3 got full support from the server.

Bons and Furious

Minecraft 1.20.1 / Forge 1.0.34

Compatibility

Controls by mod

Links

Clone this wiki locally