OptiZomb Lite

20 toggleable optimizations. Client-only. Safe on vanilla servers.

Every optimization below has its own toggle in optizomb.properties. All are enabled by default. OptiZomb Lite is client-only, so you can use it on any vanilla server without causing desync.

All 20 Toggles

  1. GPU Error Check Removal opt.glfix
  2. Character Rendering Pipeline opt.glchars
  3. Bone Data Batching opt.bonetbo
  4. Object Classification Caching opt.tileschars
  5. Sprite Batch Merging opt.batchmerge
  6. Floor Rendering Overhaul opt.floorpipeline
  7. Floor Caching opt.floorfbo
  8. Blood Splat Rendering opt.blood
  9. Shadow Batching opt.shadows
  10. Item Container Optimization opt.items
  11. Performance Diagnostics opt.diagnostics
  12. Terrain Rendering Cleanup opt.terrain
  13. Zoom Tuning opt.zoom
  14. Distance Sorting opt.scenecull
  15. Bone Detail Reduction opt.bonelod
  16. Wall Transparency Caching opt.cutaway
  17. Fog Row Skip opt.fog
  18. StartFrame Profiling opt.startframe
  19. Separation Throttling opt.separatethrottle
  20. AI Throttling opt.aithrottle

1. GPU Error Check Removal Biggest Win

opt.glfix

Vanilla PZ calls glGetError() after setting up each light on each zombie, roughly 10,000 times per frame. Each call forces the CPU to stop and wait for the GPU to finish everything before it can return an error code. These are debug checks left in from development that do nothing useful at runtime.

Removing them got rid of over 1.36 million GPU stalls every 5 seconds. This was the single biggest performance win in the entire patch.

This flag also consolidates 8 redundant glTexEnvi calls per vehicle down to 1, and raises the zombie render cap from 510 to 4,096 (configurable via opt.zombiecap).

2. Character Rendering Pipeline

opt.glchars (requires opt.glfix)

Vanilla does a lot of repeated work per zombie. Every zombie triggers the GPU to save and restore its entire internal state, re-activates the same shader, re-binds the same 3D model data, and looks up shader settings through a HashMap. None of this changes between zombies.

We track what's already set up and skip anything unchanged:

At roughly 300 zombies per 5 seconds: 382K texture setup calls bypassed, 382K uniform lookups bypassed, 1.9M array-indexed light lookups replacing if-else chains.

3. Bone Data Batching

opt.bonetbo (requires opt.glchars)

Every zombie has a skeleton of about 60 bones for animation. Vanilla sends each zombie's bone data to the GPU one at a time, about 3,840 bytes per zombie, with overhead on each transfer.

We pack all bone matrices into a single GPU buffer (SSBO) at the start of each frame. Each zombie just gets a single integer telling the shader where its bones start in the shared buffer. One bulk upload replaces thousands of individual glUniformMatrix4fv calls.

Measured: 264K SSBO appends per 5 seconds, 11.9M bones uploaded per 5 seconds (about 145 MB/s). Outline and wireframe shaders auto-detect whether the SSBO is available and fall back to the old uniform path if needed. In practice, zero zombies hit the fallback.

4. Object Classification Caching

opt.tileschars

Every visible object on every tile needs to be identified each frame: wall, door, window, bush, etc. Vanilla does about 15 individual type checks and hash lookups per object per frame. With thousands of objects on screen, it adds up.

We compute a bitmask once when an object is loaded or changed, then cache it. The per-frame check becomes a single bitmask read instead of 15 lookups. Also removes duplicate enableAlphaTest/glAlphaFunc calls and adds frame-stamp sort deduplication.

Result: near 100% cache hit rate (about 418K cached lookups vs 740 recomputes per 5 seconds). tilesChars dropped from 7.2ms to 2.9ms (-60%).

5. Sprite Batch Merging

opt.batchmerge

GPUs work fastest when they can draw many things using the same texture in a row. Every texture switch forces a reconfiguration called a "batch break."

Vanilla compares Texture object identity to decide if a new batch is needed. But sprites from the same texture atlas are different Texture objects that share the same GPU texture. We compare TextureID instead, so sprites sharing an atlas page merge into one batch.

Result: about 32% of texture state changes eliminated (455K fewer glBindTexture calls per 5 seconds). Batches reduced by 15%.

6. Floor Rendering Overhaul

opt.floorpipeline

The floor drawing pass cost about 8ms per frame even though the GPU processed the actual tiles in under 1ms. Almost all the time was CPU overhead: recalculating values that don't change, running profiler code, doing unnecessary lookups per tile.

Eight fixes: parameter hoisting out of loops, building occlusion caching, profiler stripping, bucket bitmask caching, shader hoisting per z-layer, uniform caching, typed FloorShaper selection, and lazy configuration.

Only 54 bucket recomputes out of 514K squares processed per 5 seconds. Floor dropped from 8.0ms to 6.1ms (-24%).

7. Floor Caching

opt.floorfbo

When the camera isn't moving and nothing on the floor has changed, the floor looks the same frame to frame. We render each floor layer into an offscreen buffer and reuse it until something changes.

The cache tracks camera position, zoom level, tile scale, lighting changes, and content changes (corpse creation, object movement). When valid, floor rendering drops to a single texture copy.

Result: floor dropped from 0.6ms to 0.2ms when cached (-67%), batches reduced by 31%. Hit rate is 100% when standing still, lower during movement.

8. Blood Splat Rendering

opt.blood

Vanilla processes every blood splat across all loaded chunks, even ones that are way offscreen. Each splat also goes through multiple layers of coordinate checks and color calculations.

We skip offscreen splats early, cache lighting and color values per splat (98% hit rate), and render directly instead of going through intermediate call chains.

Result: blood dropped from 1.2ms to 0.4ms (-67%). In one sample: 48,360 splats culled vs 0 rendered.

9. Shadow Batching

opt.shadows

Vanilla renders each shadow inline with its parent object, which means constant texture switches. We collect all shadows (characters, dead bodies, vehicles) into a buffer, sort them by texture, and flush them in batches.

Roughly 78K individual shadow draws consolidated into about 123 texture-sorted batches, a 639x reduction in batch breaks. For zombies ranked 201+ by distance, we use fixed shadow dimensions instead of computing them from bones. You can't tell the difference at that range.

10. Item Container Optimization

opt.items

Two fixes. First: when the game needs to find items ("how many nails are in this crate?"), vanilla scans every item one by one. Each container now keeps a type index so searches go from scanning everything to a direct lookup. Chunk-load deduplication also went from O(n^2) to O(n).

Second: vanilla checks every item in every loaded container every frame, even though almost none need updates. A hammer in a crate doesn't need per-frame processing. We filter items at chunk load so only items that actually need attention (spoiling food, drying clothes, active alarms, heated items) enter the per-frame list. 97-98.6% of processItems calls filtered out.

Note: custom mod items that extend InventoryItem directly (not Food, Clothing, etc.) and have their own update() logic may not get called for items already in containers when a chunk loads. Disable opt.items if you suspect a mod conflict.

11. Performance Diagnostics

opt.diagnostics

Adds per-frame timing across 16 log categories with 40+ fields, plus a GPU render phase profiler covering 32 phases. Reports to the log every 5 seconds as [ZombPerf] and [GPUPerf] lines. About 0.1ms overhead. Useful for debugging performance issues or checking that other optimizations are working.

12. Terrain Rendering Cleanup

opt.terrain

Narrows overly broad GL state save/restore masks in terrain rendering and removes 5 redundant GL calls per terrain draw. Also fixes a duplicate glBlendFunc call that caused visual artifacts around water puddles.

13. Zoom Tuning

opt.zoom

Manual zoom speed slightly reduced (5.0 to 4.0) for smoother feel. The game also no longer recalculates the visible area every frame during zoom transitions. It waits until zooming finishes, eliminating 96% of zoom-time grid recalculations.

14. Distance Sorting

opt.scenecull

Every frame, vanilla sorts zombies by distance using a comparator that recalculates distance, visibility, and room checks on every comparison. At 1,000 zombies, that's roughly 40,000 score computations per sort.

We pre-compute a cached score per zombie in a single pass, then sort by that number. Score computations drop from about 40K to about 1K. This sort also assigns LOD tiers that other optimizations (shadows, bone LOD, AI throttle) use to scale quality by distance:

RankQuality
0-200Full detail
201-400Simplified shadows, AI every 2nd frame
401-500Simplified shadows, AI every 4th frame
501+Core skeleton only, minimal updates

15. Bone Detail Reduction

opt.bonelod

A zombie skeleton has about 60 bones, but many are fine detail you'd never notice at distance: individual fingers, toes, connector nubs. For distant zombies (rank 501+), we skip these and only compute the structural bones (spine, limbs, head). About 40% of bones are skippable, cutting skeleton work for distant zombies nearly in half.

Measured: roughly 90K bone skips per 5 seconds at 500+ zombies.

16. Wall Transparency Caching

opt.cutaway

The cutaway system figures out which building walls should turn transparent so you can see inside. Vanilla recalculates this for every visible square every frame. The calculation is expensive: checking neighbors, wall types, room IDs, and recursive distance calculations.

We cache the result per square using the player's tile position as the cache key. The cache only clears when the player moves to a new tile.

Result: 95-100% cache hit rate. Standing still: 100% skipped. Walking: about 97% cached. At peak load, roughly 1.5M IsCutawaySquare() calls skipped per 5 seconds.

17. Fog Row Skip

opt.fog

Fog rows more than 15 tiles from the player skip every other row. Skipped rows are doubled in height (96px to 192px) to fill the gaps. You can't see the difference at that distance.

Result: about 25% of fog rows eliminated, roughly 650 ChunkMap lookups saved per frame.

18. StartFrame Profiling

opt.startframe

Instruments Core.StartFrame() with sub-phase timers: offscreen buffer, pre-populate, camera init, and GL commands. Finding: total StartFrame cost is 0.1-0.3ms. Not a bottleneck, but useful for ruling it out.

19. Separation Throttling

opt.separatethrottle

Separation physics push overlapping zombies apart so they don't stack. This is pointless for zombies far from the player since you can't see them overlapping at distance.

DistanceFrequency
>40 tilesSkip entirely
25-40 tilesEvery 8th frame
15-25 tilesEvery 4th frame
8-15 tilesEvery 2nd frame
<8 tilesEvery frame

20. AI Throttling

opt.aithrottle

Distant zombies don't need to think every frame. Zombies ranked 201+ by distance run RespondToSound() and updateSearchForCorpse() every 2nd frame. Rank 401+ run them every 4th frame. Movement and state machines always run every frame regardless of rank.

Configuration

All 20 optimizations are controlled by flags in optizomb.properties. Everything is enabled by default. Each flag is a static final boolean resolved at class-load time, so the JIT compiler removes disabled code paths entirely. No runtime cost for toggling things off.

Config file search order:

  1. {user.dir}/optizomb.properties (inside projectzomboid/)
  2. {user.dir}/../optizomb.properties (parent ProjectZomboid/ directory)
  3. {user.home}/.optizomb/config.properties (home directory fallback)

Only one dependency chain exists: opt.glfixopt.glcharsopt.bonetbo. Enabling a downstream flag auto-enables its parents. All other 17 flags are fully independent.