Skip to content
BonsUnleashed edited this page Oct 7, 2026 · 10 revisions

Oculus

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.

Twelve client-side controls for Oculus 1.8.0 (the Forge port of Iris). They remove repeated work inside the shader pipeline: vertex conversion, uniform and sampler bookkeeping, shadow-frustum construction and the batched-entity-rendering order manager. None of them changes what a shader receives or draws; every one was verified against real GL state or bit-for-bit output comparisons.

Key Patched Since Result
oculus_entity_vertex_reuse ModelToEntityVertexSerializer.serialize 1.0.4 −31…34 % per quad batch
oculus_program_traversal ProgramUniforms, ProgramSamplers 1.0.4 −7…9 % per program update
oculus_shadow_edge_vectors AdvancedShadowCullingFrustum.addEdgePlane 1.0.6 cold-path allocation only
oculus_empty_sampler_fast_return IrisRenderSystem.unbindAllSamplers 1.0.8 35.04 → 1.39 ns when empty
oculus_primitive_uniform_locations CustomUniforms.push 1.0.12 −36.8 % with 24 uniforms
oculus_empty_transparency_graphs GraphTranslucencyRenderOrderManager.getRenderOrder 1.0.12 −79.9 % with empty graphs
oculus_primitive_buffer_affinities FullyBufferedMultiBufferSource affinities 1.0.14 −51…55 % per lookup
oculus_reuse_empty_graphs GraphTranslucencyRenderOrderManager.reset 1.0.14 166.37 → 16.38 ns, 808 → 0 B
oculus_phase_labels IrisRenderingPipeline.setPhase 1.0.21 32 → 1.4 ns per phase change
oculus_dh_instance_lookup LodRendererEvents.getInstance 1.0.23 no Optional chain per DH render event
oculus_shadow_search_replay MixinAdvancedShadowCullingFrustum, MixinBoxCullingFrustum 1.0.30 shadow pass 60-68 % less time per still-view search
oculus_shadow_outline_discard ShadowRenderer, MixinRenderBuffers 1.0.30 outline buffer stays at its first-pass size (2 MB → 4 MB after 60 passes before)

All are CLIENT only; a dedicated server never loads these classes. Every switch fingerprints the Oculus 1.8.0 methods it touches; another Oculus build is left untouched with one WARN.


oculus_entity_vertex_reuse

Since: 1.0.4 · Script (1.0.19 and earlier): furious4-ModelToEntityVertexSerializer.js Patched: net.irisshaders.iris.compat.sodium.impl.vertex_format.ModelToEntityVertexSerializer.serialize Mixin (since 1.0.20): EntityVertexSerializerMixin

What upstream did. When Embeddium's model vertices are converted to Oculus' entity vertex format, the serializer re-read the UVs, the shader ids and the runtime stride for every vertex of a quad, and copied each vertex through a generic memCopy.

What the patch does. Reads those values once per quad and reuses them for its four vertices, and copies the fixed 36-byte vertex directly. The original memCopy is kept for overlapping ranges.

What stays the same. Float addition order, tangent math, the 56-byte runtime stride, every attribute and every padding byte. Random raw inputs, partial counts and overlapping buffers produced equal output.

Measured. vertex-4 60.30 → 41.76 ns (−30.7 %), vertex-64 969.72 → 648.79 ns (−33.1 %), vertex-256 3,920.09 → 2,599.90 ns (−33.7 %), with zero allocation both ways.

Since 1.0.30. Works together with Accelerated Rendering, which moves three of these offsets: its change now applies on top of this patch. Before, the two crashed the game when entities were drawn with shaders on.


oculus_program_traversal

Since: 1.0.4 · Scripts (1.0.19 and earlier): furious4-ProgramUniforms.js, furious4-ProgramSamplers.js Patched: net.irisshaders.iris.gl.program.ProgramUniforms.updateStage / removeListeners and ProgramSamplers.update / removeListeners Mixins (since 1.0.20): ProgramSamplersTraversalMixin, ProgramUniformsTraversalMixin

What upstream did. On every shader program change, the uniform and sampler updates iterated Guava ImmutableLists through temporary iterators.

What the patch does. Indexes the same immutable lists in order. Mutable initializer lists keep their iterator traversal, because their contract differs.

What stays the same. Every uniform update, listener reset, timing guard, active-program assignment and texture-state restoration.

Measured. uniforms-0 3.69 → 3.45 ns, uniforms-1 7.39 → 6.52 ns (24 → 0 B), uniforms-16 75.72 → 69.19 ns (−8.6 %), uniforms-64 312.24 → 288.62 ns (−7.6 %). A small, per-program-switch saving.


oculus_shadow_edge_vectors

Since: 1.0.6 · Script (1.0.19 and earlier): furious6-AdvancedShadowCullingFrustum.js Patched: net.irisshaders.iris.shadows.frustum.advanced.AdvancedShadowCullingFrustum.addEdgePlane Mixin (since 1.0.20): ShadowEdgePlaneMixin

What upstream did. Building the advanced shadow frustum allocated two Vector3f cross products per edge plane.

What the patch does. Reuses two private truncated vectors after their original values are no longer needed.

What stays the same. The JOML operations, getter order, plane insertion, every culling decision, and the Distant Horizons and Embeddium adapters that hook this class.

Measured. Interpreted edge probe 323.20 → 297.13 ns and 176 → 128 B; the warmed complete constructor was 222.50 → 218.29 ns with equal allocation (1,456 B). The benefit is limited to the path before JIT compilation; it is kept because it is exact and free once warm.


oculus_empty_sampler_fast_return

Since: 1.0.8 · Script (1.0.19 and earlier): furious8-samplers.js Patched: net.irisshaders.iris.gl.IrisRenderSystem.initRenderer, bindSamplerToUnit, unbindAllSamplers Mixin (since 1.0.20): SamplerCleanupMixin

What upstream did. unbindAllSamplers scanned every texture unit's cached sampler even when the cache was already empty, which is the common case between passes.

What the patch does. A private, conservative flag records that a non-zero sampler may be bound: it is set on a successful cache write, and cleared on renderer initialisation and after a complete cleanup. When the flag is clear a small guard returns immediately; otherwise it calls an exact copy of the original cleanup body. A failed cleanup keeps the flag conservative (set), so nothing can be skipped wrongly.

What stays the same. Every OpenGL call, cache write and error behaviour of the occupied case, and the binding cache's treatment of external GL calls.

Measured. Empty sampler cleanup 35.04 → 1.39 ns wall (thread CPU 34.72 → 2.48 ns); the occupied path (bind one sampler and clear) is unchanged at 353.15 → 355.20 ns. Verified with 3,000 sampler patterns using actual OpenGL binding queries, repeated clears, invalid indices and renderer re-initialisation.


oculus_primitive_uniform_locations

Since: 1.0.12 · Script (1.0.19 and earlier): furious10-uniforms.js Patched: net.irisshaders.iris.uniforms.custom.CustomUniforms.push Mixin (since 1.0.20): CustomUniformsPushMixin

What upstream did. Custom-uniform upload invoked Object2IntMap.forEach(BiConsumer). With the fastutil 8.5.9 that Minecraft bundles, that adapts each primitive location through an Integer box and a callback adapter for every uniform, every push.

What the patch does. Uses Object2IntMaps.fastForEach and Entry.getIntValue() to feed the same CachedUniform.pushIfChanged(int) call. For Oculus' own Object2IntOpenHashMap this is the same fast-entry visitation in the same order; unknown map implementations keep the original call. This is separate from oculus_program_traversal, which covers the built-in uniform lists.

What stays the same. Uniform order, locations, dirty flags and values; real shader uniform values matched in the native fixture, including null keys, collisions, arbitrary integer values and callback failure order.

Measured. uniforms-0 28.30 → 21.92 ns (−22.6 %, 48 → 24 B); uniforms-24 193.05 → 122.00 ns (−36.8 %, 408 → 0 B).


oculus_empty_transparency_graphs

Since: 1.0.12 · Script (1.0.19 and earlier): furious10-graphs.js Patched: net.irisshaders.batchedentityrendering.impl.ordering.GraphTranslucencyRenderOrderManager.getRenderOrder Mixin (since 1.0.20): TransparencyGraphOrderMixin

What upstream did. The batched-entity-rendering order manager keeps one dependency graph per transparency category (six) and topologically sorts each one to decide draw order. It processed all six even when a graph had no vertices, allocating the feedback sets, sort lists and result appends for nothing.

What the patch does. Skips the feedback-set / topological-sort / append block only for zero-vertex graphs. Populated graphs, including cyclic ones, run the unchanged algorithm with cycle removal, in the same category order, producing the same fresh mutable result list.

Measured. graphs-0 (all six empty) 279.94 → 56.27 ns (−79.9 %, 1,184 → 32 B); graphs-6 (all populated) 5,512.18 → 5,142.42 ns (−6.7 %, 20,776 → 20,008 B). Populated-graph timing varied between runs; the reliable saving is the empty work. Tested with Oculus' actual bundled Ithaka graph classes.


oculus_primitive_buffer_affinities

Since: 1.0.14 · Script (1.0.19 and earlier): furious14-affinities.js Patched: net.irisshaders.batchedentityrendering.impl.FullyBufferedMultiBufferSource (the affinities map and its getBuffer path) Mixin (since 1.0.20): BufferAffinitiesMixin

What upstream did. Oculus assigns render types to 32 existing segmented builders through an access-ordered LinkedHashMap<RenderType, Integer>. The map is cleared after preparation and repopulated every frame, allocating linked entries and boxed integers again; full-map eviction also called remove after an iterator had already removed the entry.

What the patch does. Replaces that private bookkeeping map with fastutil's Object2IntLinkedOpenHashMap: getAndMoveToLast for access-ordered hits, a missing-value sentinel outside 0–31, and removeFirstInt for eviction. It preserves object equality (not identity), access order, builder-number assignment, the 32-entry bound and the existing clear point; the actual builders are untouched. Entity Texture Features injects into the surrounding getBuffer; those input and return hooks remain intact and the production method with them passes separate output tests.

Measured (normalised per lookup; raw rows measured batches of 16 or 64): 16 active keys 53.20 → 23.99 ns (−54.9 %); 64-key churn 262.69 → 128.13 ns (−51.2 %, 52 → 0 B). The research model compared 100,000 mixed hits, misses, evictions and clears including null keys, equal-but-distinct keys and deliberate hash collisions; the native fixture verified LRU equality and order.


oculus_reuse_empty_graphs

Since: 1.0.14 · Script (1.0.19 and earlier): a coordinated extension inside furious10-graphs.js (the file checks this switch separately from oculus_empty_transparency_graphs; all four on/off combinations were tested) Patched: GraphTranslucencyRenderOrderManager.reset Mixin (since 1.0.20): TransparencyGraphResetMixin

What upstream did. reset() discarded all six category graphs and constructed six replacements every time, even when none contained a vertex, and cloned TransparencyType.values() on each reset.

What the patch does. Keeps an existing graph when it is empty and constructs a fresh one for every non-empty category, iterating a private fixed enumeration of the six categories. Occupied categories therefore still get fresh graphs; no enlarged graph from a past scene is retained.

What stays the same. begin, group boundaries, edge weights, ordering and currentTypes behaviour; cyclic graphs, repeated resets and subsequent reuse were tested with the bundled graph implementation.

Measured. All-empty graph reset 166.37 → 16.38 ns (−90.2 %), 808 → 0 B.


oculus_phase_labels

Since: 1.0.21 · Mixin: PhaseLabelMixin · Helper: bons.furious.patch.oculus.PhaseLabels Patched: net.irisshaders.iris.pipeline.IrisRenderingPipeline.setPhase

What upstream did. setPhase runs whenever the render state changes phase (entities, block entities, particles, terrain layers, sky: hundreds of times a frame) and rebuilt the phase's GL debug-group label each time with toLowerCase, replace and capitalize.

What the patch does. Builds the 24 labels once, with the same expression, and issues the same popGroup / pushGroup calls in the same order. The method body is otherwise Oculus' own (LGPL-3.0).

What stays the same. All 24 labels (checked in the 1.0.21 client run against the original expression) and the debug-group calls.

Measured. 32 → 1.4 ns per phase change; 0.14 % / 0.06 % of the render thread in two client profiles.


oculus_dh_instance_lookup

Since: 1.0.23 · Mixin: DhInstanceLookupMixin Patched: net.irisshaders.iris.compat.dh.LodRendererEvents.getInstance (with PipelineManager.getPipeline / getPipelineNullable and DHCompat.getInstance fingerprinted)

What upstream did. Every Distant Horizons render event Oculus handles looked up its shader compatibility object through a chain of two or three Optional objects.

What the patch does. The same lookup with plain null checks, returning the same object in every case. The method body is otherwise Oculus' own (LGPL-3.0).

What stays the same. The instance: in game it matched the original lookup with shaders on, off and reloaded.

Measured. No more Optionals on that path, which was 3.3 MB/s of render-thread allocation in review 8.


oculus_shadow_search_replay

Since: 1.0.30 · Target: Oculus 1.8.0 (Minecraft 1.20.1) · Side: CLIENT · Kind: opt

Mixin: ShadowFrustumMarkerMixin

The section search replay also serves Oculus's shadow pass. Lets embeddium_search_replay replay the shadow pass's search too. Oculus's shadow frusta answer the visibility test from their own numbers only, which this switch's fingerprints confirm for Oculus 1.8.0, and the replay keys those numbers bit for bit. The shadow frustum follows the sun, which moves at most once per game tick, so with a still view the shadow search replays between ticks (two to four of every five frames at 60-100 FPS). Nothing in Oculus changes; with another Oculus build the shadow pass searches as before.

Idea: Iris's draft to skip the shadow-pass search, idea text only

Measured: in the embeddium_search_replay harness with all six Oculus shadow frusta, 0 mismatches; shadow pass 60-68% less time per still-view search (hot 65 -> 21 us, cold 193 -> 78 us)


oculus_shadow_outline_discard

Since: 1.0.30 · Target: Oculus 1.8.0 (with or without ImmediatelyFast 1.5.5) · Side: CLIENT · Kind: fix

Mixins: ShadowOutlineDiscardMixin, OutlineSourceAccessor, BufferSourceAccessor

Shader shadow pass outline discard. Oculus renders the shadow pass with its own render buffers, and nothing ever ends their outline batch. Armor renderers that pick the outline buffer themselves (GeckoLib 4.8.4 and AzureLib armor: Ars Nouveau, Mowzie's Mobs, Fantasy Armor, Goblin's Tyranny and more) write a glowing wearer's outline into it on every shadow pass, so it grows without limit (with ImmediatelyFast) or is later drawn into the shadow buffers (without it). The switch ends that pending outline batch at the end of each shadow pass without drawing it: what ending the batch does, minus the draw. The main pass, which draws the real glow outline from its own buffers, is untouched.

Idea: ImmediatelyFast 1.6.9 / 1.8.5 (shadow pass outline buffer clearing), idea text only

Measured: reproduced offline (600 outline quads per pass: 144,000 vertices and 2 MB -> 4 MB after 60 passes, on both the vanilla and the ImmediatelyFast buffer source); with the switch the buffer stays at its first-pass size and the next pass writes the same bytes as a fresh buffer (925 checks, 0 failures)

Bons and Furious

Minecraft 1.20.1 / Forge 1.0.34

Compatibility

Controls by mod

Links

Clone this wiki locally