Rendering Performance - AntonIT99/Flans-Mod-Ultimate-2.0 GitHub Wiki
Rendering performance
The experimental GPU model cache is controlled by Rendering -> GPU Model Cache
on the main Flan's options screen, or enableGpuModelCache in the client config.
It is enabled by default. Its purpose is to avoid transforming and uploading
every vertex of detailed rigid models on the CPU every frame. Distance LOD remains
an independent setting.
Execution path and costs
| Operation | When it runs | Optimization |
|---|---|---|
| Build TMT shapes | Model construction | Private vertex/face arrays grow geometrically, then compact on first use; repeated exact-size copying is avoided. Legacy subclass copyTo hooks still run. |
| Flatten texture groups | First use or membership/order change | Unchanged internally owned geometry returns its cached array in O(1). Exact group iteration and polygon order are retained. |
| Validate rigid geometry | Primitive epoch comparison per part; polygon revisions after a mutation hint | No per-face scan in the normal unchanged path. Mutating another part triggers validation but does not unnecessarily discard this part's GPU geometry. |
| Validate exposed geometry | Each render of an exposed part | Conservative scans observe direct field, array and arbitrary list mutations which cannot notify a generation counter. This is a compatibility cost. |
| Compose a leaf pose | Per animated part | Local transforms are rebuilt only when public angles, pivots, offsets, scale or rotation order change. A rebuild sets the offset and pivot translation directly and reuses the rotation of the previous part when the angles are the same, as for every part of a wheel, barrel or leg array under one animation; that rotation repeats the per-axis rotate calls, so results match bit for bit. Vehicle animations that give a whole part array one set of angles (wheel spin and steering, barrel pitch, legs) take a direct path instead: parent * rotation * scale and its culling scale bound are computed once per array, each part adds only its offset and pivot translation and size test, and the parts' own angles are no longer set and restored around each render. Glowing parts, parents and parts without GPU geometry keep the previous path. Baked static parts compose no pose at all: they are drawn and size-tested against their parent pose. Quaternion scratch storage is reused, including animated parts. |
| Projected-size culling | Per eligible part | Squared comparisons avoid four square roots. The threshold factor is calculated once per model culling scope. Baked parts test a bounding sphere moved into their parent's space once, bounding parent and local scale separately; under nonuniform parent scale that bound is tighter but still never below the true size. The culling state is kept with its thread instead of a ThreadLocal lookup per part. A conservative scale bound handles nonuniform scale followed by rotation; near-camera and non-finite cases remain visible. |
| Submit a rigid part | Per part | Consecutive parts with equal transforms, tint, light and overlay share one palette entry when the transform comes from the same pose-stack entry, or when both are animated parts' own composed poses. A new pose is compared in place against the previous entry's uploaded arrays, translation first; distinct entries copy into reusable primitive arrays. No per-frame pose snapshots or draw records. |
| Find a mesh | Per batch | A reusable identity-sequence probe looks up an immutable stored key in a bounded LRU; no stream or temporary key list on hits. Eligible size-culled rigid parts retain their slots so changing visibility does not change the mesh key. |
| Build/upload vertices | Cache miss, subject to upload budget | Existing polygon expansion and normal behavior are reused. Additional large flattened heap meshes are not retained. |
| Draw | Up to 768 geometries sharing at most 96 transform entries | Fully visible batches use one VBO draw; partially visible batches use one glMultiDrawElements call with adjacent visible ranges coalesced. Entirely size-culled batches skip upload and draw. Consecutive GPU batches avoid redundant global buffer flushes. Batches split at the upload size limit. |
| CPU fallback | Per unsupported polygon/vertex | Existing legacy vertex expansion and bone/transform-group behavior are retained. |
For supported fancy tank tracks, the distance LOD also reduces the number of rendered links. Once a source link is at most eight physical screen pixels across by default, two original links share one longer six-face envelope. At four pixels, four share one. This reduces CPU link placement, geometry submission and GPU vertex work by roughly one half or three quarters for those track sections. The real animation state still advances for every link; nearby and locally controlled tracks retain their detailed geometry. This is a workload reduction, not an FPS measurement.
The official Modern Warfare Leopard 2A6 and M1A2 Abrams use the painted first tread tile bank: their supplied skins have transparent second and third banks, including in the 1.12.2 reference pack. Their three frame slots scroll the painted pattern by zero, one and two pixels along each belt segment. Faces split at the UV wrap so they stay inside the painted tile and keep their full geometry and winding. These phases are prepared once when the model is constructed; normal frame selection handles forward, reverse and independent left/right steering. Projected-size detail culling is unchanged.
Tank track travel follows completed horizontal displacement along the vehicle's forward axis, including coasting and reverse motion. Differential belt travel comes from actual hull rotation and the track width derived from wheel positions (falling back to hull width). A stationary hull therefore has stationary tracks, even with residual throttle, steering or network velocity, and a turn blocked by terrain does not move the belts. The predicting driver and observing clients use their completed movement for the same animation. A destroyed belt stays still; a single surviving belt covers the full pivot width. Texture-swap tracks complete four pattern cycles per shared track loop, making their animation four times faster than the previous frame selection. Fancy links use the same motion-derived loop phase; their corner direction fixes also follow actual travel rather than throttle. Walker leg animation retains its existing throttle-based timing.
The successful warmed-up batch path no longer allocates Batch, ArrayList,
Draw, PoseStack, Pose, Matrix4f, float arrays, stream pipelines or temporary
geometry-key lists per submission. Vehicle and gun call sites also avoid capturing
render callbacks. Reusable contexts are indexed by nesting depth; a nested renderer
flushes its parent's pending work before acquiring independent storage. Scopes drop
references to geometry and render buffers on completion. The retained context pool
is bounded to 16 idle nesting levels.
Flan models used by Custom NPCs
On Forge 1.20.1 and NeoForge 1.21.1, NPC Vehicles & Soldiers routes the ordinary body of a Flan-based NPC through the shared world-model renderer. Vehicle and plane models can use GPU mesh batching, cached static transforms, projected-size part culling, distance detail reduction, fancy-track envelopes/grouping and distant impostors. Mechas use the geometry optimizations but keep geometry instead of impostors. The module requires Flan's Mod Ultimate 2.2 or later for this API. AA-gun models use GPU batching and part detail reduction; the driveable impostor atlas does not cover AA guns. The existing client settings control this work, with no separate NPC settings.
The module carries the Custom NPCs render context through a tagged body buffer and
calls FlansModelPreviews.renderWorld. This covers renderer subclasses, including
the legacy 64x32 renderer, without replacing Custom NPCs' renderer registrations.
NPC skins, size, position/rotation, tint and damage flashes are preserved. Atlas
images are shared by model and skin; tint and damage overlays are applied per draw.
History belongs to the NPC instance, uses weak keys and clears on model reload.
Extra NPC glow/texture layers, invisible-body passes and outlines keep their original
consumer. Menu previews keep full geometry detail. Mirrored, nonuniformly scaled or
sheared parents keep geometry rather than using an incorrectly shaped impostor.
GPU compatibility fallbacks for shader packs, OptiFine, Fabulous and sorted
transparency still apply. Shader shadow passes reuse the ordinary view's track and
impostor choices and conservatively size geometry from the camera. NPC work contributes
to /flansrenderstats; these counters describe workload, not measured FPS.
Ordinary humanoid NPC bodies use Custom NPCs' own renderer. This optimization applies to their borrowed Flan vehicle, plane, mecha and AA-gun body models.
Part arrays and draw state
With tens of thousands of parts per frame, per-part cost is dominated by memory
access rather than arithmetic: each static part used to touch its own object, face
array, pose cache, matrices and baked geometry, scattered across the heap. Arrays
of sibling parts rendered under one parent pose (ModelDriveable.renderPart, which
covers hull, turret, barrel, gun and track-link arrays) therefore keep a compact
cache in parallel arrays: each static baked part's transform snapshot, render-state
revision, its bounding sphere already in the parent's space, its baked geometry and
that geometry's hash and vertex count. Parts without any transform (boxes placed directly in
model coordinates, common in Toolbox exports) are cached the same way with their own
geometry and bounds. Each frame such a part costs one check of its own fields and
sequential array reads. Consecutive cached parts share the parent's
palette entry without comparing poses again.
The cache is built after three normal renders, once parts have baked, and is
invalidated by any geometry change, a different model scale or rotation order, or a
replaced array element. A part whose fields or render state no longer match its
snapshot (animated, hidden, given glow, re-posed through another path) is drawn by
the normal path and stays on it; arrays with such parts retry them every 600
frames. Output is identical to rendering each part: a test compares every draw's
palette layout, geometry, visible ranges and uploaded data through all these changes.
A local benchmark of 54 vehicles from 15 models, 1,100 parts each, measured 127.8
nanoseconds per part before and 24.4 after, excluding OpenGL. /flansrenderstats reports the parts drawn
from array caches and those drawn by the normal path, with the reasons.
Consecutive GPU draws of one model also share their render state: the render type,
samplers, matrices, fog and lights are set up once, and each further draw changes
only the pose palette, the vertex buffer and the draw call. The state is restored
before CPU fallback vertices, nested renderers and the end of the model. Any other
draw in between sets its own shader, which the next draw detects to set up its state
again. /flansrenderstats reports these setups next to the GPU calls.
Size culling hides parts by drawing only the visible index ranges of a batch, and
drivers process each range of a multi-draw much like a separate draw. With large
batches the holes multiplied: about 47 ranges per draw, some 7,400 per frame in a
76-vehicle scene. A hidden stretch of at most 600 indices (100 quads) between two
visible ranges is therefore drawn too, merging the ranges. Those parts are already below
the pixel threshold, so drawing them costs a few cheap GPU vertices instead of another
range; hidden parts at either end of a batch, longer stretches and the CPU fallback
path still skip them. /flansrenderstats counts bridged vertices as drawn.
Building geometry for a part, by its own shape methods or derived meshes such as the track-link envelopes, adds polygons to that part's own texture groups without bumping the global geometry epoch: the part is marked to re-read its groups anyway, and no other part reads them. Bumping the epoch made every part of every model re-validate and every part-array cache rebuild, a hitch whenever a tracked vehicle model appeared. Changes through the public lists and polygon mutators still bump it.
Invalidation and ordering
Projected-size culling of eligible rigid parts updates reusable index ranges rather than removing geometry from the cached layout. Hidden ranges submit no GPU vertices and are also omitted from upload-throttled or failed-draw CPU fallback. A partially visible batch retains geometry for both visible and size-culled parts in its VBO; this trades some resident geometry for reuse as the camera crosses part thresholds. The cache's memory budget still applies. Structural changes such as damage, track grouping, animation-frame geometry swaps and model mutations can still change the layout. Unsupported geometry retains its original culling path.
Range drawing uses GL 3.2-compatible multi-draw with the current type of Minecraft's shared quad index buffer, including growth from 16-bit to 32-bit indices. Visible ranges retain submission order, including fast unsorted translucent rendering. No additional shader uniforms or higher OpenGL baseline are needed.
Static TMT leaves automatically bake their local pivots, offsets, rotations and positive scale into cached vertices at upload time. After two unchanged render observations, their geometries can share the enclosing vehicle/turret transform. No individual model source changes are required. Screen-size culling and visibility still run per part, and submission order is preserved. The cache key includes the geometry-to-palette mapping so changing which parts share a transform cannot reuse an incompatible mesh.
That mapping depends on where a transform comes from, not on coincidentally equal values. A turret at exactly zero yaw, or a barrel or wheel at exactly zero rotation, has the same matrix as its hull; sharing on equal values alone would give each vehicle one mesh at rest and another once it moves, doubling the memory of crewed vehicles. Transforms from different pose-stack entries therefore never share an entry. An animated leaf keeps its own composed pose even when its animation returns to zero, and keeps its slot while size-culled. Equal composed poses of animated parts, such as barrel pieces turned about one pivot, still share an entry.
addShape3D shapes, which Toolbox exports use for wheels and other round or
irregular parts, build bone-capable vertices. Attached to no bone, these always
resolve to their neutral position, so parts whose geometry never reached outside
code cache them like any other rigid geometry and size-cull them. Bound vertices,
and parts whose polygons, vertices or texture groups were handed to outside code
(which could bind a bone later), keep the CPU path. Internal readers such as the
track-link envelopes use a read-only polygon accessor that does not hand them out.
Any observed local-transform change permanently disables baking for that part instance, retaining the original animated path and preventing continuous VBO rebuilds. Moving or rotating the parent vehicle/turret does not invalidate a baked mesh. Geometry edits replace the baked snapshot through the existing revision checks. Parents with children, custom parts, nonpositive scales and non-finite local matrices retain the existing path. Baking uses the existing packed vertex normal format, so rotated normals may have slightly different quantization.
Polygon mutators and the default texture-group list update a mutation epoch. Each part remembers its last checked epoch and polygon revisions. Membership changes compare polygon identity and order, not just polygon count. Geometry, UV, normals, face flipping, mirroring, texture fitting and construction invalidate the relevant CPU/GPU representation and bounds.
copyTo, external polygon inputs, imported model geometry and exposed texture
groups may retain writable aliases. Those parts keep a compatibility scan, including
when a caller replaces TextureGroup.poly with an arbitrary list. New direct
vertex edits can also explicitly call TexturedPolygon.invalidateCompiledVertices().
Transform vertices always remain deformable. A retained normals list is excluded
from immutable GPU geometry even while empty; filling that list cannot silently
freeze the old lighting representation.
Initial entry, CPU fallback and nested rendering establish buffer ordering barriers. A client-only accessor checks Minecraft's started-buffer set before subsequent GPU draws, so custom renderers that queue work outside the supplied consumer also cause a flush. If this accessor is unavailable, flushing remains conservative. GPU shader state is restored after each draw rather than left active across arbitrary model code.
VBO storage is limited by gpuModelCacheMegabytes, at most 2 MiB is uploaded per
client tick, and the number of cached sequences is capped at the budget divided by
32 KiB, but at least 2,048. The budget has to hold the working set: the meshes of every
distinct model drawn in a frame. Models are shared by all entities of a type, so the
working set grows with the number of different vehicle and gun types in view, not
with entity count; one test scene of parked tanks filled 64 MiB with 700 meshes. A smaller budget makes the least recently used eviction discard meshes that
the next frame needs again, which shows as misses, then upload deferrals, then CPU
fallback vertices. Video memory sets the ceiling, so the automatic budget takes a
sixteenth of what NVIDIA (GL_NVX_gpu_memory_info) or AMD and Mesa
(GL_ATI_meminfo, which reports free memory) drivers report, within 128-512 MiB.
Other drivers get 128 MiB. Eviction, replacement and clearing close owned resources.
Shader registration resolves new uniform references and clears old bindings before
reload; failed shader registration disables the GPU path. Runtime submission errors
fall back to CPU rendering until reload. Clearing only the mesh cache retains the
currently registered shader.
Shader and compatibility
The pose palette is a std140 uniform block (PartPalette) rather than plain uniforms.
Each entry takes 144 bytes: the pose (mat4), the normal matrix (mat3, three columns
padded to four floats), the tint, and the light and overlay coordinates. Ninety-six
entries take 13,824 bytes, inside the 16 KiB block size OpenGL 3.1 guarantees (see the
OpenGL 3.2 core specification),
where plain uniforms limited the palette to 24 entries. Fancy track links each take an
entry, so a track now shares one or two draws instead of six to eight. Each draw writes
only its own entries at the next aligned offset of a 4 MiB ring buffer and binds the
block's range there (binding point 7); the buffer gets a fresh data store only when the
ring wraps, so writes never wait for draws still reading earlier palettes. The shader is
applied once per render state; each further draw binds only its palette range and mesh.
/flansrenderstats reports the average palette entries per separate call.
Glow, deforming/custom geometry, legacy compiler flags, outlines and other buffer wrappers, glint, GUI previews, thermal rendering, sorted transparency, Fabulous, OptiFine and an active Oculus or Iris shader pack retain their conservative fallback behavior. Parent/child inheritance, old rotation order and negative, zero and non-finite scale handling remain supported. Public transform fields must still be compared because direct assignments cannot increment a revision counter.
Shader packs (Oculus and Iris)
Oculus on Forge 1.20.1 and Iris on NeoForge 1.21.1 are optional. Flan's Mod uses their public API to check, while rendering, whether a shader pack is active and whether its shadow map is being drawn. With Oculus or Iris installed but shaders turned off, Minecraft's normal pipeline is used, so the GPU model cache stays available.
While a shader pack is active:
- The GPU model cache falls back to the standard path, including in the shadow pass. The pack replaces Minecraft's programs and vertex formats and blocks the output of shaders it does not know, so the cache's own shader cannot draw there.
- Glowing model parts, muzzle flashes and instant-hit bullet trails use the pack's
glowing-entity program (
gbuffers_spidereyes), like vanilla spider eyes. They stay full-bright at night and can receive the pack's bloom. Without a pack they look as before.
Shadow pass
Most shader packs draw every entity a second time into their shadow map, from the sun's view and with the shadow map's projection instead of the camera's. Flan's vehicles are detailed, so this pass can cost as much as the normal view. It is kept cheap as follows:
- Small driveable parts are sized as the camera sees the model's nearest point, not
as the shadow map sees them. A part is left out of the shadow only when the normal
view would also leave it out (
minimumDriveablePartPixelSizeand the distance LOD settings), so the shadow keeps every part that is visible. - A driveable shown as a distant impostor casts no shadow: a billboard turned towards the camera would cast an edge-on one. The shadow pass reuses the normal view's impostor and track-link decisions instead of making its own, which would make the normal view flicker between detail levels.
- Supported fancy tank tracks always use single-link envelopes in the shadow pass,
or the normal view's coarser grouping where it uses one. A shadow shows only each
link's outline, which its envelope keeps, at a fraction of the detailed link's
geometry. With
driveableTrackLinkLodPixelSize = 0, detailed links are kept there too. The player's own vehicle keeps its normal detail. - Muzzle flashes, additive glow and glow drawn without depth writes cast no shadow, since they add light rather than block it. Solid glowing parts still do.
- Bullets, grenades, thrown weapons and the invisible seat and wheel entities of driveables are left out before any rendering work.
/flansrenderstats counts this work while a pack is active; see
In-game cache diagnostics.
Impostors and screen previews
Impostor views are always generated at the start of a frame, before the world is drawn, still at most one view per client tick. This keeps the shader pack's world and shadow programs out of the impostor atlas. With or without shaders, it also keeps fog (including underwater and blindness fog) and the camera's current lighting out of the atlas: every view is lit from above, as in the world.
At default quality, impostors provide 32 horizontal views and nine elevations at
22.5-degree intervals from directly below to directly above. The horizontal count
can rise to 64 through driveableImpostorYawAngles; the quality multiplier scales
resolution linearly and horizontal samples by its square, with caps of 256 pixels
and 64 views. Billboards face the camera position and keep the model's bank angle,
so rolled aircraft retain their orientation. The requested view must be captured
before it replaces geometry.
Each model/paintjob entry allocates atlas pages of four horizontal views and three elevations as needed, rather than reserving all views at once. A page costs 0.75 MiB of GPU storage at the default 128-pixel resolution, plus the same native image storage. Visiting many directions grows this usage; one fully populated default entry costs 18 MiB of each. Changing capture quality or reloading resources releases the pages. See Mod Config for the controls and cache-size limit.
Inventory and other screen previews are not drawn by the shader pack. While a pack is active, glowing parts there use Minecraft's emissive entity shader and may show slight directional shading.
Sodium and Embeddium
Embeddium on Forge 1.20.1, and Sodium or Embeddium on NeoForge 1.21.1, are optional. Flan's Mod looks for their public bulk vertex writer at runtime; neither is a dependency.
- Flan's model parts are normally drawn one vertex call at a time. With one of these renderers, each part's vertices are collected in native memory and handed to the renderer's bulk vertex writer a few hundred at a time, the same API they use for vanilla entity models. The written data is byte for byte what the normal path writes. This covers ordinary and glowing model passes, guns in hand, and the GPU model cache's CPU fallback and mesh uploads.
- With Iris and Sodium on NeoForge 1.21.1, a bulk write converts a whole batch to the shader pack's extended vertex format in one pass, instead of extending every vertex separately. This speeds up the path that models take while a shader pack keeps the GPU model cache off.
- Embeddium declines bulk writes into Oculus's extended shader-pack formats (Oculus also turns off Embeddium's own entity fast path), so with Oculus and an active pack those vertices keep the normal path. Without a pack, bulk writing applies.
- Vanilla model cubes, such as the player's arms holding a gun, Flan's particles, and enchantment glint and outline layers already go through these renderers' own fast paths.
- Their occlusion culling hides an entity whose culling box lies in hidden chunk sections. Driveables, AA guns and deployed guns report boxes that cover their whole model, so they are not hidden while part of them is visible.
/flansrenderstats reports the bulk writes and vertices, and which renderer took them.
EntityCulling
EntityCulling is optional; Flan's Mod looks for it at runtime and does not depend on it. EntityCulling hides entities that blocks hide from the camera. By default it also skips the client tick of any entity that is hidden or outside the view, which it calls tick culling. Flan's Mod adds its own entity types to EntityCulling's whitelists during client setup:
| Entity types | Hidden behind blocks | Client tick while unseen |
|---|---|---|
| Vehicles, mechas, seats, wheels, grenades | Yes | Always runs |
| Planes and helicopters, bullets | No | Always runs |
| AA guns, deployed guns | No (culling box too large) | Runs while manned or within their render distance |
| Parachutes | No (never culled) | Always runs |
| Thrown weapons, dropped guns, team items, flags | Yes | EntityCulling default |
- Driveables use their client tick to follow the server, to predict the driver's own movement and to play engine sounds. Without it, an unseen vehicle would freeze, go quiet and jump when it came back into view. A vehicle frozen behind a wall could even stay hidden after driving into the open. Riders sit on separate seat entities, not on the driveable, so EntityCulling's exemption for vehicles with passengers does not apply.
- Seats and wheels follow their driveable only in their own tick, so their interaction and hit boxes would be left behind.
- Bullets play flyby sounds, leave trails and steer manually guided missiles in their client tick. Grenades emit their trail and smoke there, so smoke behind cover still rises above it.
- EntityCulling reads culling boxes only every few ticks by default (
captureRate). Bullets and aircraft can outrun those boxes and would appear late, so they are never hidden. Aircraft are mostly in open sky, where hiding them would rarely save anything. - Ground vehicles and mechas are still hidden behind buildings and terrain, where EntityCulling saves the most drawing.
These entries are added in memory at startup, not to EntityCulling's config file, so its config screen shows them as off. EntityCulling releases before June 2025 have no working tick whitelist. With those, every type above that always ticks is also never hidden.
EntityCulling draws the name tags of players it hides. Flan's Mod applies its Teams name tag rules there too, so enemy tags do not show through walls.
Validation and measuring
Regression tests cover geometry/UV/normal mutation, group order and replacement, construction buffers, legacy hooks and flags, deforming and glow fallbacks, original transform math, conservative culling, identity keys, LRU disposal, partial/full batches, nested scopes, upload-throttle fallback and injected draw failures. Additional checks cover shared palette identities, upload-size splitting, parent motion, local-transform edits, and baked versus original vertex/normal output. Size-culling regressions cover stable mesh keys, adjacent and separated visible ranges, completely hidden batches, and visible-only fallback after upload deferral or injected failure. The optional driver test checks rendered pixels before and after shared-index-buffer growth.
Set FLANS_GPU_TEST=true while running Gradle tests to enable the hidden-window
OpenGL shader test. It checks the actual shader JSON, matrix-array upload through
Minecraft's uniform implementation, and shader registration/reload/failure lifecycle.
It expands vanilla GLSL imports for the standalone harness; it does not launch Forge.
Set FLANS_RENDER_BENCHMARK=true and run test --tests '*RenderingAllocationTest'
to measure the warmed TMT submission and mesh-cache lookup path. The local harness
measured 0 allocated bytes over 800,000 animated part submissions (40 parts,
64 boxes per part). It excludes OpenGL calls, uniform uploads and the rest of a
Minecraft frame. No expensive instrumentation runs in production.
The culling benchmark moves a 120-part static model repeatedly between 10 and 30 blocks from the camera, crossing projected-size thresholds. It checks that the warmed mesh stays cached and that submission remains free of per-part allocations. This harness excludes actual GPU draw costs.
The static-hull case uses 120 parts with eight boxes each and a moving parent. After warming, it submits one batch per frame instead of five with the previous 24-parts-per-batch path, with no additional mesh-cache misses. The local harness measured zero allocated bytes over 2.4 million part submissions. This measures submission and cache lookup, not actual OpenGL draw time or in-game FPS. Very large geometry groups split earlier to respect the upload size cap.
Compare actual frame times in the same in-game scene with the option on and off, after warming the cache. GPU fill rate, transparent overdraw, texture changes, bone animation, exposed mutable geometry validation, and Minecraft's shader uniform/driver submission can still dominate. No universal FPS gain is claimed.
In-game cache diagnostics
Use these client-local commands; no server permissions are required:
/flansrenderstats start: reset counters and begin recording./flansrenderstats: show accumulated counters and current cache status./flansrenderstats stop: stop recording and show the result./flansrenderstats reset: reset counters without changing whether recording is active./flansrenderstats bridge [indices]: show or set the size-culling gap bridged between visible ranges (0 turns bridging off, default 600), to compare frame times from one spot. It is not saved.
The report includes mesh hits/misses, uploads and uploaded bytes, LRU evictions, upload-budget deferrals, GPU draw calls and visible ranges, visible vertices, submitted and size-culled parts, rigid CPU fallback vertices, and excluded GPU scopes. It also shows resident meshes and bytes against their limits with the percentage of the budget in use, the peak reached by uploads while recording, how the budget was chosen, and why the cache is globally disabled or unavailable. These are cumulative workload counters, not frame times; CPU fallback counts cover eligible rigid batches, not every vanilla renderer. Excluded scopes count requests rejected by GPU eligibility checks.
The report opens with the recorded frames, elapsed time, average FPS and frame time, then the driveables rendered per frame and the CPU time their render calls took, including how much of it (and of held guns) went to GPU model draw submission: split into mesh lookup (including uploads), palette packing and upload, and the GL draw calls; mesh lookup, uploads, buffer flushes, state setup and the GL calls. Clocks are read only while recording, twice per driveable and per GPU draw. GPU execution time is not measured. Per frame it also lists the parts offered the GPU path that still drew on the CPU, by reason (custom part or polygon class, glow, legacy compiler, bone transform vertices, inverted or per-vertex normals) with the vertices they wrote, the geometry epoch changes (each makes every part re-validate its polygons once; the first three callers per recording are logged), and the part re-validations, including those scanning every vertex for outside edits. Then come the draw calls, ranges, vertices, parts, CPU fallback vertices and uploads per frame. Evictions are followed by how many evicted meshes had been drawn in the current or previous frame. Those are the working-set evictions: any steady count means the budget is smaller than the scene needs. Evictions of older meshes are normal once the cache is full.
The shadow-pass counters only move while an Oculus or Iris shader pack draws its shadow map: driveables drawn and those left out as impostors, rigid driveable parts tested and size-culled there, and projectiles and seat or wheel entities left out. Compare the parts tested against the size-culled ones to see how much detail the shadow pass skips.
Warm a fixed scene, run start, observe it for a fixed duration, then run stop.
Repeat with the same settings and camera movement. Use Minecraft's frame-time
display alongside the counters: a high hit rate does not by itself establish an
FPS gain. Recording is disabled by default and adds only primitive counter updates
when enabled; reports allocate text only when requested. Reloads can release
resident meshes without resetting the accumulated counters.
Further architectural work
Cross-vehicle instancing was implemented and measured, then removed. It queued batches
during the entity pass and drew each mesh's copies with one glDrawElementsInstanced
call, reading per-vehicle palettes from a texture buffer by gl_InstanceID (no vertex
attribute divisors, so it fit the GL 3.2 baseline). In a mixed 76-vehicle scene a mesh
had about two copies per frame, so it saved few draws while the per-instance palette
upload cost about as much: frame time was within noise of drawing separately. After the
palette moved to a 96-entry uniform block, most batches exceeded its 24-entry instance
layout, and draw calls had already fallen from about 567 to 158 without it. It would
only pay off for many identical vehicles, at about 13.8 KB of palette per instance.
Track links, a link mesh repeated with different poses, remain the most duplicated
geometry should draw submission become the limit again.