# Edits to the overridden game classes `src/overrides/` is **not in the repository**: it holds decompiled game code. Regenerate it with `scripts/regen-overrides.sh` (Vineflower output of `zombie.iso.IsoChunk` and `zombie.iso.WorldStreamer` from the installed jar, revision `4a0e9546ec`, Build 42.21, since 2026-09-28; before that `b0bbce05d5`, Build 42.20.4), then re-apply the edits below by hand (the last section, "Port to Build 42.21", lists what the port changed). Every edit is marked `// pzopt:` in the working copy. One decompiler fix is needed first: the `switch` expression on `carSpawnRate` in `IsoChunk.addVehicles` lacks a `default -> chance;` arm. ## zombie.iso.IsoChunk 1. **Load marker.** A `static {}` block at the top of the class calling `pzopt.Overrides.onClassLoaded("zombie.iso.IsoChunk")`. 2. **Split of `loadInWorldStreamerThread()`.** The method becomes `recalcLoop1(); recalcPooled();`. `recalcLoop1()` is the original first loop (per level/square: create missing squares, `ensureNotNull3x3`, `RecalcProperties`). `recalcPooled()` is everything after it — the rain/roof column pass, the `RecalcAllWithNeighbours(true, getter)` pass, and the `propertiesDirty = true` pass — with one difference: instead of binding the static `chunkGetter` (`assert chunkGetter.chunk == null; chunkGetter.chunk = this; … chunkGetter.chunk = null;`) it allocates a local `IsoChunk.ChunkGetter`, sets its `chunk` to `this`, and passes that. Both new methods are `public`. At the top of `recalcPooled()`, a dev-only injected failure: if `pzopt.Guard.DEV` and `pzopt.RecalcPool.failChunk` equals `"wx,wy"` of this chunk, clear `failChunk` and throw `IllegalStateException`. 3. **Frame hook.** First statement of `update()`: `pzopt.Stats.frameTick(IsoCamera.frameState.frameCount);`. 4. **Dev-build guards** at the two pathfinding-registration gates: a `pzopt.Guard.assertGameThread(...)` call immediately before the `if (… Thread.currentThread() == GameWindow.gameThread || … GameServer.mainThread)` test in the level-change path, and immediately before the `MapCollisionData.instance.addChunkToWorld(this)` block in `doLoadGridsquare()`. 5. **Decompiler fixes in `AddVehicles_OnZone` (2026-09-23, Workshop reports "cars barely spawn").** Two Vineflower mis-renders, present since the override was first committed, restored to the jar's bytecode (not optimizations): the stall-row loop was a `while` ending in an unconditional `break`, so only the first row of stalls of each parking / driveway zone in a chunk ever spawned (the "type not found" path advanced a row and broke out too); it is a labelled `for (y = yOffset; …; y += stallLen)` again, and the not-found path is `continue rows`. The `carSpawnRate` switch rendered stock's `chance *= 2` (High) as `case 5 -> 2`, a flat 2 % per stall; it is `chance * 2` again. Checked by compiling the override and comparing the method's `javap -c` with the jar's (same `imul` / `Rand.Next` counts, outer back-edge restored). Imports added: `pzopt.Guard`, `pzopt.RecalcPool`. ## zombie.iso.WorldStreamer 1. **Load marker + settings line.** A `static {}` block calling `pzopt.Overrides.onClassLoaded("zombie.iso.WorldStreamer")` and `pzopt.Log.info("settings: " + pzopt.Config.describe())`. 2. **Streamer wake-up.** In `create()`, the worker lambda calls `pzopt.StreamerWake.register()` before its loop. In `addJob(...)`, after `this.jobQueue.add(chunk)`: `pzopt.StreamerWake.signal()` (preceded by `pzopt.Stats.onEnqueued(chunk)`). In `threadLoop()`, the two `Thread.sleep(140L)` calls (the one taken when `jobList` is empty, and the one at the end of the loop when a player exists) become `pzopt.StreamerWake.idle(140L)`. The `Thread.sleep(20L)` and `Thread.sleep(0L)` are unchanged. 3. **Retries.** In `threadLoop()`, `pzopt.RecalcPool.runRetries();` just before the loop that drains `jobQueue` into `jobList`. 4. **Pool hand-off in `DoChunkAlways(chunk, fromServer)`.** Around the body: `pzopt.Stats.Timing timing = pzopt.Stats.begin(chunk);` before `chunk.LoadChunk(...)`, and `timing.loadEndNs = System.nanoTime();` after `VehiclesDB2.instance.loadChunk(chunk)`. In the non-Convert/non-SoftReset branch: if `pzopt.RecalcPool.active()` and the current thread is `this.worldStreamer` and `chunk.refs` is non-empty, call `chunk.recalcLoop1()` then `pzopt.RecalcPool.submit(chunk, timing)` inside the existing `try/catch (Exception) { ExceptionLogger.logException(ex); }`, and `return` on success (the pool publishes to `IsoChunk.loadGridSquare`); if loop 1 threw, fall through. Otherwise the stock path, with `timing.recalcStartNs`/`thread`/`recalcEndNs` recorded around `chunk.loadInWorldStreamerThread()` and `pzopt.Parity.capture(chunk)` after it. After the stock `IsoChunk.loadGridSquare.add(chunk)`: `timing.publishNs = System.nanoTime(); pzopt.Stats.done(chunk, timing);`. 5. **`isBusy()`** additionally returns true when `pzopt.RecalcPool.inFlight() > 0`. 6. **`stop()`**: after the streamer thread has ended ("stop 3" debug line) and before `this.worldStreamer = null`: `pzopt.RecalcPool.drain(); pzopt.Stats.flush();`. ## zombie.iso.ChunkSaveWorker (added 2026-09-18) Regenerated with `scripts/regen-overrides.sh` (now lists this class); the pristine Vineflower output compiles as is. 1. **Load marker.** A `static {}` block calling `pzopt.Overrides.onClassLoaded("zombie.iso.ChunkSaveWorker")`, plus two static fields: the nanosecond stamp of the last ancillary hot save and a counter of skipped drains. 2. **Hot-save throttle.** In `Update(IsoChunk)`, the branch that calls `HotsaveAncilliarySystems()` when the save queue has just drained now runs it only if `pzopt.Config.HOTSAVE_INTERVAL_SEC` is 0 (stock), the overrides are disabled, or at least that many seconds have passed since the previous hot save; otherwise the drain is counted and skipped (logged with the count when the next hot save runs). Background: the hot save serialises the whole meta grid (`IsoMetaGrid.saveToBufferMap`, building/room counts of every meta cell) on the game thread via `MainThread.invokeOnMainThread`, and while moving the queue drains about every 0.45 s (JFR, `attr-jfr-1`), so it was a 2–5 ms game-thread stall twice a second. ## zombie.core.VBO.GLVertexBufferObject (added 2026-09-18) Regenerated the same way (no inner classes). All edits are guarded by `pzopt.Config.PERSISTENT_VBO && pzopt.Overrides.enabled()` and by the `GL_ARB_buffer_storage` / OpenGL 3.2 capabilities; otherwise every method is stock. 1. **Load marker** in a `static {}` block. 2. **Persistent mapping for fixed-size buffers.** New private state: a `persistent` flag, a per-buffer fence handle, a static list of buffers unmapped since the last fence, and dev counters (waits, wait time, stalls). The no-argument `map()` (used by the sprite `RingBuffer`, the water `SharedVertexBufferObjects` and the world-map VBOs, all constructed with a size) takes a new path: the first call allocates immutable storage with `glBufferStorage(MAP_WRITE | MAP_PERSISTENT | MAP_COHERENT)` and maps it once with the same flags; later calls only reset the buffer position. Before handing the buffer out it (a) creates one `glFenceSync` per buffer unmapped since the last map — those buffers' draw calls were issued in between — and (b) waits on this buffer's own fence with `glClientWaitSync(FLUSH, 1 s)`, so the CPU never overwrites a batch the GPU is still reading (the stock ring has 128 buffers, which is the depth of the pipeline before a wait happens). `unmap()` on a persistent buffer only records it in the pending list; `clear()` returns early (no `glBufferData` on immutable storage); `doDestroy()` unmaps for real and deletes the fence. Stock behaviour was `glBufferData` (orphan) + `glMapBufferRange(WRITE | INVALIDATE_RANGE | UNSYNCHRONIZED)` per 64 KB batch, ~25 % of render-thread samples at max zoom (`attr-jfr-z25`). Default off since 2026-09-19 (evening): with the in-game 240 fps limiter back, two 120 km/h drives with it on and off had the same frame profile (mean 4.2 ms, p99 7.2 / 7.3 ms), and one stationary run with it on drew a whole building lot floor opaque black for a minute (`artfix-opt120-2`), which its fence logic is the only edit able to cause. Re-enable with `--prop persistentVbo=true` for uncapped runs. 2026-09-20 afternoon: the black chunk squares seen with it on were the light-info chunk gate (see the FBORenderCell entry of that day), not the fence logic: a `glFinish` before every map (`persistentVboFinish`) still showed them, and the fix in FBORenderCell removes them with the fences untouched. 3. **Diagnostics and variants added during that bisect (2026-09-20), all off by default and marked `// pzopt:`:** `persistentVboFrameFence` (one fence per frame from `RenderThread` after `SpriteRenderer.postRender`, `pzoptFrameEnd()`, kept in an 8-entry ring; a slot is rewritten only after the frame it was drawn in is done; `persistentVboFrameLag=N` also waits for the N following frames); `persistentVboSlots=K` (K immutable storage buffers per `GLVertexBufferObject`, rotated and re-bound on every `map()`, so the 128-buffer ring reuses a slot after 128*K batches; slot 0 keeps the original buffer id); `persistentVboCoherent=false` (MAP_FLUSH_EXPLICIT plus `glFlushMappedBufferRange` at `unmap()` instead of MAP_COHERENT); `persistentVboDelayUs` (CPU-only park per map); `persistentVboFinish` (`glFinish` per map). With `instrument=true` a "persistent VBO:" line every 5 s counts maps, maps per frame, per-batch and per-frame fence waits and stalls. ## zombie.iso.fboRenderChunk.FBORenderCell (added 2026-09-18) Regenerated with Vineflower; three decompiler fixes were needed before it compiled: two dropped `boolean` declarations in `renderTilesInternal` (the `runChecks` / `recalculateGridStacks` result variables), and explicit comparator types for the three `timSort.doSort` lambdas (world inventory objects, chunks by lighting counter, translucent squares). Every fix is marked `// pzopt: decompiler fix`. 1. **Load marker** in a `static {}` block before the private constructor. 2. **Trees in the chunk texture.** `isTreeRenderedEveryFrame(IsoObject)` returns `false` when `pzopt.Config.TREES_IN_CHUNK_TEXTURE` is set and the overrides are enabled (stock: `object instanceof IsoTree`). With that, a tree is classified `Translucent` (drawn every frame) only while it is in the player stencil (`isTranslucentTree`), fading, wind-animated or carrying render effects; every other tree bakes into its chunk-level texture, and the existing `checkTreeTranslucency` pass invalidates the level (flag 4096) when a tree changes state. Two additions (2026-09-19, evening): a tree whose texture is not ready yet (`Asset.isReady`) stays per frame and `checkTreeTranslucency` re-dirties its level when the texture arrives; and `renderMinusFloor(IsoObject)` bakes a tree with `FBORenderTrees.current` temporarily null (`treeBakeDirect`, default on), so `IsoTree.render` takes the plain sprite path. The batch path (`FBORenderTrees` in chunk-texture mode) dropped most JUMBO trees around town buildings (they never appeared, even after a forced redraw), while the plain path draws every tree. Per-frame JUMBO trees cost 8.1 ms mean on the max-zoom route versus 4.2 ms baked, so this is the difference between the edit paying off and not. Measured on the max-zoom teleport route (`trees-1` vs `pvbo-1`): frame mean 6.2 → 5.2 ms, p99 18.7 → 17.4 ms, GPU busy 84 → 61 %. 3. **Windows and glass doors in the chunk texture.** In `isObjectRenderLayer_Translucent`, the `object instanceof IsoWindow` and the `doorTrans` door clauses are skipped when `pzopt.Config.WINDOWS_IN_CHUNK_TEXTURE` is set (they still go per-frame through the fading / obscuring-player clause that follows). Default off since 2026-10-01 (Workshop discussion "zombies not visible at windows", RCwuhui's comment of 2026-09-30 "the window looks like it's become opaque"; glass doors and showers too): a baked window goes through `tileWithDepth.frag`, which writes the sprite's depth for every texel the depth texture covers, glass included, while stock draws windows in `renderOneChunk_Translucent` with `glDepthMask(false)`. A zombie outside the pane (farther from the camera than the glass) then failed the depth test against the composited chunk texture and was not drawn where the window covers it. It measured no gain when adopted (`windows-1` vs `hotsave-1`: 5.0 / 5.0 ms mean). Rig: harness `--flag find=winzombie` (runs `wz3-win-on` / `wz3-win-off`: a zombie behind the right-hand room's window shows through the glass only with the key off). The maintainer's bus shelter (Riverside 6209-6213,5290-5292, 2026-10-01 screenshot): its panes are windows (`walls_commercial_01_96` / `_97`, `IsoWindow`, glass alpha 0.6-0.8 under opaque brown frame strips `walls_detailing_01_36` / `_45`). Runs `gs2-on` / `gs2-off` / `gs2-stock` (player at 6207,5288 behind it, three pinned zombies, `--flag pin_zombies=`): with the key on the zombie behind the left pane came out washed out and cut; off it matches stock. The player's body behind the shelter is hidden in stock too: it stands behind the opaque top strip, only the head shows above it. 4. **`Translucent`-flagged tiles in the chunk texture.** A private static helper `pzoptPerFrameTranslucentTile(IsoSprite)` returns `depthFlags & 2 != 0` unless `pzopt.Config.TRANSLUCENT_TILES_IN_CHUNK_TEXTURE` is set; it replaces the three literal `depthFlags & 2` tests (the early `return true` in `isObjectRenderLayer_Translucent`, the early `return false` in `isObjectRenderLayer_MinusFloor`, and the `TranslucentSE` / `MinusFloorSE` choice in `calculateObjectRenderLayer`). Default off since 2026-09-19 (evening): the flag is the tileset property `Translucent` (road decals, dirt patches, puddles), and baked into the opaque chunk texture those tiles come out as opaque black one-tile rectangles on the floor (the maintainer's screenshot, walking, not only at speed). The per-frame pass draws about 20 of them per frame; not worth it. Since 2026-09-28 the helper also keeps a Translucent light fixture with a lit sprite (`IsoFlagType.HasLightOnSprite`) per frame while `pzopt.Config.TRANSLUCENT_LIGHTS_PER_FRAME` (`translucentLightsPerFrame`, default on) is set. Baked, such a fixture's "on" sprite (`_on_`) is drawn by the per-frame animated-attachments pass, where `IsoSprite.startTileDepthShader` pulls it 5e-5 towards the camera so it wins over its own baked lamp; the top of a ceiling fixture lies in the plane of the floor above, so the pull put the lit tubes of the Fossoil canopy (`lighting_indoor_03_19..21` on level 1, roof `location_shop_fossoil_01_38/39` on level 2, Riverside 6056-6063,5302-5308) on top of the roof as dashes that moved with every zoom step (maintainer's save `Sandbox/2026-09-28_22-02-56`). Per frame the fixture and its lit sprite go through the translucent pass with no pull, as in stock. A depth nudge of the baked lamp changed nothing (the baked lamp never showed; `_20` / `_21` have no tile geometry either), and a bake that left every Translucent tile out still showed the dashes: the lit sprite was the whole problem. Runs `canopy-*` (2026-09-28): white pixels on the roof 70-270 per frame, jumping with the zoom, before; 5-45 and steady per zoom level after, like `translucentTilesInChunkTexture=false` and stock. Rig: harness `--flag find=translucent [--flag find_box=x0,y0,x1,y1]` lists the objects (sprite, class, depth flags, attached / overlay sprites, the floor above). Since 2026-10-01 the helper also keeps Translucent glass tiles per frame while `pzopt.Config.GLASS_TILES_PER_FRAME` (`glassTilesPerFrame`, default on) is set: `pzopt.GlassTiles` says a sprite is glass when its definition names a glass material (`MaterialType` / `Material` / `Material2` / `Material3` containing "Glass": Glass, Glass_Light, Glass_Solid), a glass `GroupName` or has a `GlassRemovedOffset` (182 of the 15.8k Translucent definitions of 42.21: shop and restaurant display cases, glass-door fridges, escalator and mall balustrades, glass doors, shower screens), cached per sprite. Baked, such a pane writes its depth like any baked tile (`tileWithDepth.frag` keeps every texel the depth texture covers; the alpha test only drops alpha 0), so a character behind it fails the depth test against the composited chunk texture; stock draws the pane per frame after the characters with depth writes off and it blends over them. A depth-only second bake pass for the opaque texels was tried and dropped: the panes are 0.6-0.8 alpha, so the character came out on top of the glass instead of behind it, and every Translucent bake drew twice. Rig: harness `--flag find=glasszombie` (nearest such tile, three zombies pinned behind it in the same room). Confirmed against stock with `harness/glass-judge.py` (2026-10-02, runs gw-* vs gv-*-stock / -stock2, default options): the zombie regions behind the glass were 2.5-7.3x the stock-to-stock difference before, 0.67-0.98x after, in both scenes (shelter windows, placed `location_shop_mall_01_24` balustrade); Jev: balustrade fixed 0.88, shelter fixed 0.51 vs not_fixed 0.43 on the same numbers. Since 2026-10-05 the helper (now taking the object) also keeps Translucent tiles that lie on the floor of a puddle square per frame while `pzopt.Config.FLOOR_DECALS_PER_FRAME` (`floorDecalsPerFrame`, default on) is set: `pzopt.FloorDecals` says a sprite lies on the floor when its texture's opaque rectangle fits the floor diamond at the bottom of the tile (half the tile's width high, plus a sixteenth of the width for a rim; cached per sprite: manhole covers, drains, litter, broken glass), and the square draws puddles (a floor, puddle geometry that renders: the same test the bake walk caches the puddle squares with). Stock draws such a tile in the translucent pass after the puddles, so it covers the water. Baked, it sat in the chunk texture under the puddle pass, which is depth-tested against it: the cover's object depth (no tile geometry, `UseObjectDepthTexture`) lies within the puddle's 1e-4 lift over the floor, and the two traded places in horizontal bands that moved with the camera (maintainer's report "manholes flicker zoomed out all the way, in the rain puddles", `street_decoration_01_15`). Bisected with `harness/manhole-flicker.py` (Jev walks circles at max zoom, puddles pinned full): stock clean, puddle keys, enhancements, fogPass, tileDepthFix, zoomRetain all innocent, `translucentTilesInChunkTexture=false` alone clean. Before: bands in 436 of 520 frames, 31 % of the cover changing per frame; after: 0 band frames, 1.2 % (stock 0.3 %); Jev fixed 0.95. In daylight rain (`weather=rain`): bands in 107 of 520 frames before, 0 after, 0 in stock; the cover adds 8.5 % flips over the road beside it before, 0.6 % after (stock -2.5 %; our rain streaks flicker the road itself ~2 points more than stock's); Jev fixed 0.83. Video: `docs/media/manhole-puddle-flicker-before-vs-fix.mp4` (`harness/stitch-manhole.sh`). Cost: the per-frame translucent pass draws ~200 more tiles a frame on the wet spin route (420-480 vs 200-280; nothing baked would be ~3,000); uncapped spin 352 vs 377 fps over four noisy pairs. Since 2026-10-25 the helper also keeps Translucent tiles that are a piece of a multi-square sprite grid (`IsoSpriteGrid` wider or deeper than one square) per frame while `pzopt.Config.TRANSLUCENT_GRIDS_PER_FRAME` (`translucentGridsPerFrame`, default on) is set. Discord bug report "Broken textures for picnic tables" (fresh save, no other mods; a second player: large metal shelves, some rugs): baked, the ground showed through the picnic tables `camping_01_8..11` (2 x 2 squares, Muldraugh 5977-5978,5425-5428) in wedges along the seams between the pieces, the tabletop and the benches alike. Stock draws these tiles in the per-frame translucent pass (`glDepthFunc(GL_LEQUAL)`, no depth writes) and is clean at the same camera. Not the cause: the order inside the bake (drawing a level's baked Translucent tiles after all its other objects, in the stock translucent pass's square order, changed nothing) and the tile's depth box (`tileDepthFix=on` changed nothing). Shots at the report spot (`--flag start=5981,5431 --flag zoom=1 --shot-at 1`, default options): released behaviour and `translucentGridsPerFrame=false` show the wedges on all three tables, `translucentTilesInChunkTexture=false`, stock and the fix none (runs `picnic-*`, `picnic-v2-on` / `-off`). The other reported furniture, placed beside the tables (`--flag place_tile=`): the large metal shelf (`furniture_shelving_01_24..27`, depthFlags 2) shows ground flecks through its back piece baked and matches stock with the fix (runs `grid-on` / `-off` / `-stock`); of the rugs the animal hides (`rugs_animals_*`, 3 x 3 grids) and the shag rug (`floors_rugs_02_0..7`) are Translucent, the other fabric rugs (`floors_rugs_01_*`) have depthFlags 0. The shag rug's tiles are single squares, so this key leaves them baked, and baked a neighbour cuts the fringe in a straight line (runs `shag-on` / `-perframe` / `-stock`; per frame and stock are clean): still open. A Holstein hide under a table: baked, the hide drew over the table's legs and bench in the same wedges (4,325 px off stock in the crop); with the fix 211 px, the swaying plants (runs `hide-on` / `-off` / `-stock`). Cost (runs `gridcost-*`, uncapped, GPU-bound at 100 %, the spot with four tables, the shelf and two hides on screen, alternating on / off pairs): the per-frame pass draws 100 instead of 56 Translucent tiles; 1113.7 / 1111.4 vs 1120.1 / 1108.5 fps, p99 2.1 ms on both sides, i.e. ~1.4 us a frame against a 9 us spread between two runs of the same side. 5. **Dev counters** (only with `instrument=true`): `renderTranslucent(IsoObject)` and `renderTranslucent(IsoGridSquare)` count what the per-frame pass draws by kind (window, door, tree, Translucent-flagged tile with a per-tileset tally, animating, fading, other, squares); `renderInternal()` logs the per-frame averages every 1800 frames as "translucent pass per frame". 6. **Bake budget** (`pzopt.Config.BAKE_BUDGET`, 0 = stock). Two new fields (bakes started this frame, the set of textures deferred this frame; both reset at the top of `performRenderTiles`) and a block at the top of the render decision in `renderOneLevel`, before `beginRenderChunkLevel`: when a level is dirty and, at its texture's lowest level, the frame has already started `BAKE_BUDGET` bakes, the texture is deferred (its upper level follows the decision through the set). Since 2026-09-19 (evening) only a never-baked level (`DIRTY_CREATE` still set) can be deferred: a re-bake of a texture that is already on screen (obscuring set, trees, cutaways, lighting, object changes) always lands in the same frame, because drawing the stale texture for a frame while the per-frame translucent list already reflects the new state showed windows and glass doors flickering as the car passed buildings. A deferred level whose texture was baked before (`DIRTY_CREATE`, 512, no longer set) takes the existing "clean" path — the manager's current chunk is pointed at its texture, `endRenderChunkLevel(..., false)` queues it for drawing and the cached translucent lists are re-registered; a never-baked level returns without drawing. Deferred levels are retried next frame in chunk order. 7. **Lighting budget** (`pzopt.Config.LIGHTING_BUDGET`, 0 = stock). With a budget `updateChunkLighting` calls a new private `pzoptUpdateChunkLightingBudgeted`: in the frame the lighting counter changes it still asks `LightingJNI.getChunkDirty` for every on-screen chunk level (sorted by chunk lighting counter as in stock) and records the dirty levels as a bitmask per chunk in a `LinkedHashMap`; then, on that frame and the following ones, it refreshes the square light info (`cacheLightInfo` over the level's renderable squares) of at most that many chunks per frame, oldest entry first, dropping entries whose chunk left the on-screen list. The first version (2026-09-18) simply returned after the budget and re-entered the loop next frame; the JNI rewrites its dirty bits on its next pass, so the chunks it had not reached lost their update and stayed with stale light info (unlit tiles and tree silhouettes behind the car at 120 km/h, darker chunk-sized patches on grass). The stock branch (budget 0) is unchanged, including the debug-only `Lighting.SplitUpdate`. 8. **Occluder-mask replay** (`pzopt.Config.CUTAWAY_FAST`). In `calculateOccludingSquares(int)`, a chunk level that is not dirty and whose mask was computed before (tracked in a bounded `HashSet`) has its stored `occludingSquares[playerIndex]` bitmask replayed into the occluded grid (bit → square x/y, level z, same window test and `max` update as the stock per-square loop); dirty or never-computed levels run the stock `ChunkLevelData.calculateOccludingSquares`. The replayed mask is pzopt's own (`pzoptExactOccluderMask`, computed after the stock call with the stock test but without the on-screen clip). Since 2026-09-20 the mask lives on the `IsoChunk` override (a `long[64]` indexed by level + 32 plus a bit per stored level, cleared in `resetForStore`) instead of a map keyed by `ChunkLevelData`: the per-frame map lookup for every on-screen level was 1.8 % of the game thread. The stock `occludingSquares` mask is built with an `int` shift (`1 << x + y * 8`): bits 32-63 wrap and bit 31 sign-extends when cast to long, which stock never notices because it only compares the mask with its previous value. Replaying it marked whole rows of tiles beside house walls as occluding, drawn as black one-tile rectangles (2026-09-19). Every input of that test (cutaway flags, vision matrix, square existence) dirties the level when it changes, so a clean level's mask is current. 9. **Cutaway visit radius** (`pzopt.Config.CUTAWAY_RADIUS`, chunks). The `doCutawayVisitSquares(playerIndex, chunks)` call in `renderTilesInternal` receives, instead of every on-screen chunk, the on-screen chunks whose chunk coordinates are within the radius of the camera character's chunk (a reused list; the stock list when the radius is 0). 10. **Grid-stack interval** (`pzopt.Config.GRID_STACK_INTERVAL`, frames). In `recalculateGridStacks`, `CalculatePointsOfInterest`, `CalculateBuildingsToCollapse` and `checkHiddenBuildingLevels` are skipped while the camera character's square and facing are the same as at the last scan and fewer than that many frames have passed (never skipped when `player.dirtyRecalcGridStack` is set); `recalculateAnyGridStacks` still runs every frame. 11. **Tree translucency check** (2026-09-20). In `checkTreeTranslucency` the `HashSet.remove` of the trees-awaiting-texture set is only attempted when the set is not empty (it almost always is: 1.9 % of the game thread was that remove per tree per frame), and the aim-key state is read once per chunk level instead of once per tree: `isTranslucentTree(IsoObject)` now delegates to a private overload that takes the aim flag. 12. **Re-bake budget** (`pzopt.Config.REBAKE_BUDGET`, default 4; `REBAKE_MAX_FRAMES`, default 3; 2026-09-20). Inside the bake-budget block of `renderOneLevel`: a texture that was baked before and whose dirty flags are only lighting (32), redraw (1024) and/or cutaways (2048) is deferred (previous image drawn through the existing stale-texture path) once that many such re-bakes have started this frame, for at most `REBAKE_MAX_FRAMES` frames per texture (an identity map from texture to the frame it was first held, bounded at 4096). Object, item, tree and obscuring changes are never held, for the flicker reason in item 6. On the 25 s Rosewood teleport route with the facing spinning, bakes were 2.4 to 4.3 per frame and 87 % of them re-bakes; this took the p99 from 15.6 to 11.2 ms (`docs/archive/2026-09-24/results.md`, 2026-09-20). Counters `budgeted rebakes` / `held` join the instrument line. 13. **Defaults changed 2026-09-20**: `lightingRebakeMs` 0 → 250, `cutawayRadius` 0 → 6, `gridStackInterval` 0 → 8 (measured on the same route: +8 fps for the two cutaway keys, +7 fps for the lighting hold; recordings side by side with the keys off show the same frames). ## zombie.GameWindow The update check starts in `mainThreadInit` right after the `server` property is read (2026-09-26, `if (!GameServer.server && pzopt.Overrides.buildMatches()) pzopt.Updater.check();`), so its answer is there when the menu appears (the menu's own call is then a no-op). Two edits. In the boot sequence (`init`, between `Translator.loadFiles()` and `LuaManager.init()`): the call to `doEpilepsyWarningText()` is wrapped in `if (!pzopt.Overrides.enabled())`, so the photosensitivity warning frame is not drawn at start-up while the build guard is active. The method itself is unchanged. In `mainThreadStep` (edited 2026-09-19, reverted to stock later that day): the stock limiter is back. `frameStep()` runs once the `accumulator` reaches `1 s / PerformanceSettings.getLockFPS()` (the `frameRate=` value from options.ini) unless `isFramerateUncapped()`, exactly as in stock. Earlier that day the condition had `|| pzopt.Overrides.enabled()` appended, which made every main-loop iteration a frame whenever the build guard was active; that removed the player's choice, so it was undone. In `mainThreadStep` (second edit, 2026-09-19): the two reads of the cap, `isFramerateUncapped()` and `getLockFPS()`, go through `pzopt.FrameCap.uncappedNow()` and `lockNow()`. Those return the in-game values while a world is up or loading (`isIngameState()` or the current state is `GameLoadingState`) and the separate menu cap otherwise; with the build guard off or the menu cap left at "same as in-game" they are the stock values, so the loop shape is unchanged. After each `frameStep()` (both branches) one call to `pzopt.FrameCap.onFrame(now)` counts frames per phase and prints one console line per menu/game transition ("frame cap: menu phase 3.2 s, 144 frames, 45.0 fps (cap 45 fps)"), which is how a hands-off run verifies the menu cap. In `InitDisplay` (added 2026-09-19): one call to `pzopt.FrameCap.afterLoadOptions()` right after `Core.loadOptions()` (both branches), before the sprite renderer is created. Stock ships an "Uncapped" entry for the frame-rate combo in `MainOptions.lua` but it is dead twice over: nothing ever sets the `SystemDisabler` flag that gates the entry, and `Core.loadOptions` resets a saved `uncappedFPS=true` to a 60 fps lock. `FrameCap` sets that flag so the combo shows "Uncapped", then re-reads the `frameRate=` / `uncappedFPS=` lines from options.ini and re-applies them to `PerformanceSettings`, so what the player picks in Display options survives a restart. It also loads the menu cap from `Zomboid/pzopt/framecap.ini`. Config key `uncappedFps`: `auto` (default) honours options.ini, `true` / `false` force the in-game cap off / on for one run (`harness/run.sh --prop uncappedFps=true`). No-op when the build guard is off. Nothing else consults the cap for timing (`GameTime` uses measured deltas); Lua's `getAverageFPS` clamps the displayed number to the in-game lock value only when capped. The harness metric "frames below 240 fps cap" (`harness/analyze.py` `FPS_TARGET`) keeps its meaning as the share of frames slower than 4.17 ms. The class is otherwise verbatim Vineflower output (revision `b0bbce05d5`), which recompiles without fixes. Together with the committed `src/shims/zombie/gameStates/TISLogoState.java` (logo screens skipped), this is what gets a run from launch to the main menu with no splash screens. Boot edits (added 2026-09-19, evening, `docs/plan-instant-load.md`), each behind a `Config` key and `pzopt.Overrides.enabled()`: - `mainThreadInit`: `FMODManager.instance.init()` is handed to `pzopt.BootAsync.startFmod` (a thread) when `fmodAsync` is on; the construction of `SoundManager.instance`, `AmbientStreamManager.instance` and `BaseSoundBank.instance` (each still choosing the Dummy variant on `Core.soundDisabled`), `VoiceManager.instance.loadConfig()` and the four `SoundManager.instance.set*Volume` calls are wrapped together in one `pzopt.BootAsync.afterFmod(...)` block, which runs at once when the init is synchronous and otherwise at the join. Why: the FMOD system create and the twelve bank files are 1.4 s of native work that nothing needs before the sound scripts, and the VCAs the volume setters read live in the banks. The managers must wait too (issue #3, 2026-09-20): `SoundManager` and `AmbientStreamManager` build their `FMODGlobalParameter` fields (MusicState, MusicIntensity, TimeOfDay, ...) in field initialisers, and each constructor resolves its parameter description from the banks loaded so far. Built while the banks were still loading they kept a null description, never registered with `FMODManager`, and their values never reached FMOD: the menu music never stopped and the in-game music and ambience were dead. This was true on Linux as well; nobody had listened to a run. `joinFmod` now logs whether `MusicState` is registered. - `initShared`: `pzopt.BootAsync.joinFmod()` right before `ScriptManager.instance.Load()` (whose last step, `GameSounds.ScriptsLoaded`, is the first FMOD consumer). After `SpriteModelManager.getInstance().init()`, when `earlyModels` is on: `ModelManager.instance.create()` and `pzopt.BootAsync.startAnimSets()`; `enter()` later finds the manager created and skips (its own guard). Why: `create` only needs the scripts and the file system, and registering the 3,990 animation imports 2 s earlier lets the boot pump finish them during the Lua load. - `init` (first statement): `pzopt.BootPump.start()`; `mainThreadStart` calls `pzopt.BootPump.stop()` after `enter()`. Why: the file pool is only pumped by `GameWindow.logic` (per frame), so during init its threads idled. - `init`, after `ZomboidFileSystem.instance.loadModPackFiles()`: `pzopt.LuaPrecompiler.start()` (mods are known, so the file list is right). - `enter`: `pzopt.BootAsync.startAnimSets()` after `ModelManager.instance.create()` (a no-op when `initShared` already started it). ## zombie.fileSystem.FileSystemImpl (added 2026-09-19, game load) Vineflower output needs one fix: in `updateAsyncTransactions` the decompiler typed the reused local as `boolean priority` (`= (boolean)1`, later `= (boolean)(16 - inProgress.size())`); the first assignment is dropped and the second becomes `int canAdd`. Edits (`// pzopt:`): 1. **Load marker** in a `static {}` block; a new `private final int maxInFlight`. 2. **Pool size.** The constructor's `numThreads` (stock: 2 on ≤ 4 cores, else 4) becomes `pzopt.Config.FILE_THREADS` and `maxInFlight` becomes `pzopt.Config.FILE_INFLIGHT` when the overrides are enabled (stock 4 / 16 otherwise); one log line reports both. 3. **In-flight cap.** The two literal `16`s in `updateAsyncTransactions` (how many in-progress items are checked per frame, and how many pending tasks may be submitted at once) read `maxInFlight`. ## zombie.tileDepth.TileDepthTextures (added 2026-09-19, game load) Pristine Vineflower output compiles. Edits: 1. **Load marker** in a `static {}` block. 2. `tilesets` becomes a `ConcurrentHashMap` (same private field, same uses) and a `claimedTilesets` concurrent key set is added. 3. **Concurrent load tasks.** `LoadTask.call`, when `pzopt.Config.PARALLEL_DEPTH_MAPS` is set and the overrides are enabled, skips the stock `synchronized (this.textures)` block: if the tileset is not in the map and this task is the first to claim its name it calls `createTileset(tilesetName, true)` directly. Everything `createTileset` does is per tileset (cached row count, its own `PNGDecoder`, its own tiles, GPU uploads queued on the render thread), so the 218 tasks decode concurrently instead of one at a time. Stock path otherwise. ## zombie.core.textures.TextureIDAssetManager (added 2026-09-19, game load) Pristine Vineflower output compiles. Edits: load marker, and `waitFileTask`'s literal `52428800L` (50 MB of decoded textures waiting for the render thread before the decoders sleep in 20 ms steps) becomes `WAIT_BYTES` = `pzopt.Config.TEXTURE_BUFFER_MB` MB when the overrides are enabled. ## zombie.MapCollisionData (added 2026-09-19, game load) Pristine Vineflower output compiles. Edits, all under `pzopt.Config.LOADER_CPU_FIXES && pzopt.Overrides.enabled()`: 1. **Load marker** in a `static {}` block. 2. **Lot header once per cell.** A private static `pzoptZombieIntensity(lotHeader, chunkX, chunkY, cache, cached)` is a copy of `LotHeader.getZombieIntensityForChunk` whose `mapFiles.getLotHeader(cellX, cellY)` result is cached per map-files index in two arrays allocated per cell in `init`; the 32×32 chunk loop calls it instead of the static. Same loop bounds, same `bgHasCell300` test, same returned byte; only the per-chunk thread-local/`String.format`/`HashMap` lookups go. ## zombie.iso.IsoMetaGrid (added 2026-09-19, game load) Pristine Vineflower output compiles (`MetaGridLoaderThread` comes along as an inner class). Edits: 1. **Load marker** via `pzopt.Overrides.onClassLoadedQuiet` in a `static {}` block: `IsoMetaGrid` is constructed inside `IsoWorld`'s own static initializer, and `DebugLog` reads `IsoWorld.instance` (still null) for the frame number of every line, so logging there kills the game at boot (`ExceptionInInitializerError` in `IsoWorld.`, nothing in console.txt). The marker is printed by the next override that loads. 2. **`checkVehiclesZones` dedupe.** Under the same guard the O(n²) scan is replaced by one pass with a `HashSet` keyed on `(getX(), getY(), w, h)`: a zone whose key was seen is removed, so the first zone of each key survives exactly as in stock (stock removes the later index). The stock debug string is only built when `DebugType.Vehicle.isEnabled()`; one `[pzopt]` line reports the counts. ## org.lwjglx.opengl.Display and org.lwjglx.input.Mouse (added 2026-09-19) These two are The Indie Stone's LWJGL 2 compatibility shim over GLFW 3.4 (the window and mouse the whole game talks to), not `zombie.*` code. They are overridden for one reason: native Wayland on a scaled desktop. The game selects the Wayland GLFW platform only when the JVM property `zomboid.wayland=1` is set (otherwise the shim forces X11). On Wayland GLFW hands the window size out in screen coordinates while the framebuffer is scaled (`GLFW_SCALE_FRAMEBUFFER` is on by default), so on a 5120x2160 panel at KDE's 125 % scale the stock shim told the game the display was 4096x1728 and the game drew that viewport into the bottom-left of a 5120x2160 buffer, leaving black bands on top and right. Edits in `Display` (all marked `// pzopt:`): - `getWidth()` / `getHeight()` return the framebuffer size whenever it is known (the stock code returned the screen-coordinate size). On X11 and XWayland the two are identical, so nothing changes there. - two new helpers, `getFramebufferScaleX()` / `getFramebufferScaleY()`, give framebuffer pixels per screen coordinate (1.0 when GLFW does not scale). - the cursor-position callback multiplies GLFW's screen coordinates by those scales before handing them to `Mouse.addMoveEvent`, so clicks land where the cursor is drawn. - the lock-cursor-to-window clamp in `updateMouseCursor` clamps to the screen-coordinate size (GLFW's space), not the framebuffer size. Edit in `Mouse`: `setCursorPosition` divides the game's framebuffer pixels by the same scales before calling `glfwSetCursorPos`. Third edit in `Display` (added 2026-09-19, evening): **MangoHud on native Wayland.** `swapBuffers()` first calls a private `pzoptHudSwap()`. On the first call it checks that the overrides are enabled, the GLFW platform is Wayland, `MANGOHUD=1` is set and `/proc/self/maps` shows `libMangoHud_opengl.so` already loaded; if so it resolves that library's exported `eglSwapBuffers` with the JDK foreign-function API (`SymbolLookup.libraryLookup` + `Linker.downcallHandle`, signature `int (void*, void*)`). Every swap then calls it with `GLFWNativeEGL.glfwGetEGLDisplay()` / `glfwGetEGLSurface(window)` (cached per window handle) and returns; MangoHud draws the HUD and forwards to the real `eglSwapBuffers`. Any failure (no handle, `EGL_FALSE`, exception) logs one warning and falls back to `glfwSwapBuffers` for the rest of the process. Why: GLFW resolves EGL entry points with `dlsym` on its private `libEGL` handle, so the `LD_PRELOAD` hook never sees the swap on Wayland (on X11 the Steam overlay's own `dlsym` hook chains to MangoHud, which is why it works there); MangoHud's `dlsym` shim library deadlocks the game's JNI launcher. Verified with `tools/GlfwSwapProbe.java` (the same LWJGL build, "hud" mode) and the `wl-gl-mh-*` runs. Both classes are otherwise verbatim Vineflower output (revision `b0bbce05d5`) and recompile without fixes; `Display`'s inner classes `$Window` and `$Callbacks` come along as loose classes. Verified 2026-09-19 with `harness/run.sh ... --env JAVA_TOOL_OPTIONS=-Dzomboid.wayland=1`: the console logs "Display mode changed to 5120x2160", the recording is full-screen and the route numbers match the XWayland runs. Fourth edit in `Display` (added 2026-09-22, midday): **the window is created at its final size.** The stock shim creates the GLFW window at the shim's 640x480 placeholder, makes the context current, and `Core.setDisplayModeInternal` then resizes it (`glfwSetWindowMonitor`) to the options' resolution and fullscreen state; the window manager applies that resize asynchronously, after the shim has already re-bound the context. On the Dell (GTX 960M rendering through NVIDIA PRIME render offload over XWayland, first boot into KWin 6.7.5 / Xwayland 24.1.13) the GL drawable kept the 640x480 geometry the context was first made current with: the fullscreen 1920x1080 window showed the frame's bottom-left 640x480 and black elsewhere, with the stock shim as well (`dell-lo2-probe-*` runs, the game's own `--shot-at` capture of the default framebuffer lit only that corner). The previous day the same box was fine because KWin had grown the fresh window to 1920x1022 before the switch ("Display mode changed to 1920x1022" in every 09-21 Dell console), i.e. the bug was masked, not absent. - `create()`: when `Core.width` / `Core.height` are known (or the fullscreen option is set) the window is created at `Core.width x Core.height` — the desktop size for a borderless window — and, for fullscreen, directly on the primary monitor with `GLFW_REFRESH_RATE` = the desktop's rate so no video mode switch happens; `gameWindowMode` is set to the matching `DisplayMode` (the 4-argument constructor for fullscreen, read back from the window in case GLFW chose another mode) so `Core.setDisplayModeInternal`'s first check (`getWidth() == width && getHeight() == height && isFullscreen() == fullscreen`) returns before any switch. The "closest width=" search and the "Display mode changed to 640x480" line therefore no longer appear at boot. - `setDisplayModeAndFullscreenInternal()`: after `glfwSetWindowMonitor` (a resolution change from the options screen, the fallback path) a new `pzoptAwaitWindowSize()` polls `glfwGetFramebufferSize` (a synchronous `XGetWindowAttributes`) until the window reaches the requested size, or its size has stopped changing for 200 ms, or 1 s has passed, records the size in `displayFramebufferWidth/Height` and `latestWidth/Height`, and the context is then unbound and re-bound (`glfwMakeContextCurrent(0)` + the window) so a driver that latches the drawable geometry on MakeCurrent sees the final size. Not gated on `Overrides.enabled()` (like the HiDPI edit: a display-correctness fix with no game-internal dependency). Verified 2026-09-22 on the Dell with the `--shot-at` capture (`dell-lo2-fix-probe-*`: the whole 1920x1080 frame) and on the desktop's windowed 5120x2160 runs. Follow-up (2026-09-23, user report "opens with 1920x1061 instead of 1920x1080"): on Windows a new decorated window is clamped to the screen's maximum track size, so a windowed 1920x1080 window on a 1920x1080 screen is created with a 1920x1061 client area. `gameWindowMode` already said 1920x1080, so `Core.setDisplayModeInternal` saw a mismatch and called `Display.setDisplayMode(1920x1080)`, but `setDisplayModeAndFullscreenInternal` found `gameWindowMode` unchanged and did nothing. The stock resize from 640x480 (a `SetWindowPos`, not clamped) never ran, and every launch stayed at 1061. `create()` now reads the window size back for windowed windows too and stores it in `gameWindowMode`, so the stock switch issues that resize whenever the window came out smaller than asked. Follow-up (2026-09-23, issue #14 and Workshop reports "borderless windowed: the menu is a little off centre and clicks land above the cursor", Windows 11, build 4bb5acb): the borderless window was created *decorated* at the desktop size and only lost its decoration in Core's switch. `glfwSetWindowAttrib(DECORATED, 0)` keeps the client rect, so the result depended on Windows: clamped to the max track size (1920x1058 under Wine, the 1061 report) the switch resized it to 0,0; not clamped (a max track size larger than the monitor, e.g. more than one monitor) the client stays at the caption offset (8,31) with its bottom rows off screen, and `setDisplayMode` finds the size unchanged and never moves it (read from the call sequence; Wine always clamps, so only the clamped branch was replayed). Replayed with GLFW 3.4 under Wine (a C copy of the call sequence, stock / 4bb5acb / new): stock ends as a 1920x1080 `WS_POPUP` at 0,0 after two transitions. Now `create()` sets `GLFW_DECORATED` 0 and `isBorderlessWindow` for a borderless window on Win32, so it is created undecorated at the monitor origin, which is stock's final state; Core's switch finds it done. Linux keeps the decorated creation: nobody reported the offset there, and with the window mapped undecorated at the screen size (desktop, KWin) the Workshop uploader's native confirm dialog was not on top and the upload of 1718aa0 did not go through. `setDisplayModeAndFullscreenInternal` also re-places a borderless window that is not where `calcWindowPos` puts it (`pzoptBorderlessMisplaced`, not on Wayland: no window position there), which covers switching windowed -> borderless at the desktop size in the options screen (a stock bug as well). The HiDPI cursor scale (framebuffer / window size) is applied only on Cocoa and Wayland, the platforms where the two sizes differ; on Win32 and X11 both are the client area, so a ratio other than 1 could only be two sizes recorded at different moments. ## zombie.scripting.ScriptParser (added 2026-09-19, evening, boot) `stripComments` first tries `pzopt.ScriptText.stripComments` (one forward pass with a nesting depth) when `scriptParserFast` is on and falls back to the stock backward `StringBuilder.replace` loop when that returns null (unbalanced comment markers). `parseTokens` returns `pzopt.ScriptText.parseTokens`, the same split with an index instead of a new substring per block, including the stock quirks (searches start one character in; a brace-less remainder is a token of its own). Why: the stock stripper is quadratic in the number of comments and cost 1.5 s on `tileGeometry.txt` alone. `tests/pzopt/ScriptTextTest` compares both functions against the jar's class on every `.txt` under `media/` (1,501 files identical). Otherwise verbatim Vineflower output (revision `b0bbce05d5`). ## zombie.fileSystem.FileSystemImpl (second edit, 2026-09-19, evening) `updateAsyncTransactions` now takes a `ReentrantLock` around its whole body (the body moved to a private method), and a `pzoptExecutor()` accessor exposes the pool. The constructor sizes the pool to `max(fileThreads, bootFileThreads)` when `bootPump` is on. Why: the boot pump thread (`pzopt.BootPump`) pumps concurrently with the main thread's own calls during font loading, and `pending`/`inProgress` are plain lists. ## zombie.iso.IsoMetaCell (added 2026-09-19, evening, game load) `getChunk(int)` resolves the zombie intensity through `pzopt.LotHeaders.zombieIntensity` with a per-cell memo (`pzoptLotHeaderCache`, rebuilt if the cell's `info` changes) when `loaderCpuFixes` is on. Same loop and tests as `LotHeader.getZombieIntensityForChunk`, but `MapFiles.getLotHeader` (a `String.format` plus two hash lookups) runs once per map layer per cell instead of once per chunk. Why: 0.8 s of the loader thread in `IsoMetaGrid.load` → `loadZone` → `addZone` → `getChunk`. ## zombie.buildingRooms.BuildingRoomsEditor (added 2026-09-19, evening, game load) `checkBuildingAndRoomIDs(IsoMetaCell)` builds an `IdentityHashMap` from `roomList` (walked backwards so the first occurrence wins, as `indexOf` does) and uses it for the two `roomList.indexOf(roomDef)` lookups when `loaderCpuFixes` is on. Same checks and messages. Why: O(rooms²) per cell, and `Basements.beforeLoadMetaGrid` calls it three times per load (0.85 s). ## zombie.gameStates.GameLoadingState (added 2026-09-19, evening, game load) `exit`: `screenFader.startFadeToBlack()` is skipped when `noLoadFade` is on, so the `while (isFading)` loop with its 33 ms sleeps ends at once (the world's own 2 s fade-in through `UIManager.FadeOut` is untouched). `enter`, first statements: `pzopt.BootPump.onLoadStart(executor)` shrinks the file pool to `fileThreads`, `pzopt.BootAsync.joinAnimSets()` waits for the boot preload of the animation sets, and the Lua precompile statistics are logged. Why: F dropped from 0.41 to 0.05 s; the other hooks are the load-side ends of the boot threads. `render` (2026-09-20): the click-to-start gate for a new game (`newGame && time < 33`) is also satisfied when `noIntroWait` is on, so the prompt shows, and `showedClickToSkip` lets `update` accept the click, as soon as `done && playerCreated`. The three intro lines keep their timers and still fade behind the prompt. Why: stock makes a new save wait the full 33 s intro ("This is how you died") even when the world finished loading in 4 s. ## se.krka.kahlua.luaj.compiler.LuaCompiler (added 2026-09-19, evening) `loadis(Reader, String, KahluaTable)` (the overload `LuaManager.RunLuaInternal` uses) reads the whole chunk into a string when `luaPrecompile` is on, asks `pzopt.LuaPrecompiler.lookup(name, content)` for a prototype compiled during boot, and returns `new LuaClosure(prototype, env)` on a hit; on a miss it compiles the same characters through the stock path (a `StringReader`). The other overloads are untouched. Why: Kahlua compiles ~1.7 s of Lua serially across boot and load; the boot pool does it in parallel. The precompiler stamps `Prototype.file`/`filename` with what the stock compile would have written (`FuncState.currentFile`/`currentfullFile`). ## zombie.core.skinnedmodel.advancedanimation.AnimationSet (added 2026-09-19, evening) `GetAnimationSet` and `Reset` keep their signatures and now run their bodies inside `synchronized (setMap)`. Why: `pzopt.BootAsync.startAnimSets` parses the player and zombie sets on a boot thread while the game may ask for them (`IsoPlayer`/`IsoZombie` constructors, main-menu previews), and the map is a plain `HashMap`. ## zombie.core.skinnedmodel.model.AnimationAssetManager (added 2026-09-19, evening) `startLoading` creates a `pzopt.CachedAnimationTask` (a `FileTask_LoadAnimation` subclass) instead of the stock task when `animClipCache` is on, and remembers it per asset. `loadCallback` has a new first branch for the task's `CachedClips` result (sets `anim.animationClips`, `onLoadingSucceeded`, `ModelManager.animationAssetLoaded`, exactly what the `ProcessedAiScene` branch does after `onLoadedX`), and the `ProcessedAiScene` branch ends with `pzoptWriteCache(anim)`, which hands the freshly imported clips to `pzopt.AnimClipCache.writeAsync`. Why: 2,209 jassimp imports (14–17 thread-seconds) per boot for data that is a map of keyframes; the cache (one file per source, keyed by path, size, mtime and skinning mesh) replaces them with 2.9 thread-seconds of reading. 2026-10-11: the key's mesh part was the skinning mesh's asset path only (`Skinned/MaleBody`), which stays the same when a mod replaces the mesh. Tomb's Player Body (Workshop 3429790870) ships a MaleBody with another bone order and ten extra `...Nub` bones, so the cache served clips with the vanilla bone indices and every character came out mangled (runs `tomb-cache-on` vs `tomb-cache-off`). `CachedAnimationTask.meshIdentity` adds what the import takes from that mesh (`ImportedSkeleton` copies its `boneIndices` and `skeletonHierarchy`): bone names in index order and their parents. Not the mesh file's mtime: `run.sh --mod` (and a re-download) re-copies the file with the same skeleton. Every install re-imports once after this change (runs `tomb-fix2-a/b`, `vanilla-fix2-a/b`: first boot misses, second 2,161 hits, all four characters correct). ## zombie.fileSystem.FileSystemImpl (third edit, 2026-09-19, evening) `runAsync(FileTask)` wraps the task's `call()` in a timing lambda that reports to `pzopt.FileTaskStats` (count and summed run time per task class; logged when the boot pump stops and when the loading screen starts). Why: to size the asset work per class (animations 14 s, texture pages 12 s of which most is the upload-budget sleep, meshes 0.3 s). ## zombie.fileSystem.TexturePackDevice (added 2026-09-19, evening, boot) `initMetaData` opens a `pzopt.PackIndex` for version-0 packs when `packIndex` is on, and `readPage` looks the page's PNG end offset up in it: on a hit the stream skips to the end instead of the stock loop that reads the PNG bytes one at a time through the synchronized `PositionInputStream` looking for the end marker; on a miss the stock loop runs and the end offset is recorded, and the index is saved after the last page. Why: 0.5–0.6 s of boot scanning 526 MB of packs byte by byte (`TexturePackPage.readIntByte`). The index is keyed by the pack file's size and mtime and lives in `~/Zomboid/pzopt/packs/`. Nothing else changes; `PositionInputStream` already counts skips. ## zombie.scripting.objects.Item (added 2026-09-19, evening, boot) `DoParam(String, String)` starts with a guard: when `itemParamSwitch` is on it calls the new private `pzoptDoParam` and returns. That method is the stock method with its 367-branch `else if (param.trim().equalsIgnoreCase("..."))` chain rewritten as a `switch` on `param.trim().toLowerCase(Locale.ROOT)`; every case block is the stock branch body verbatim, the attribute check that precedes the chain stays first, the chain's tail (the negated `GameEntityScript` test with the unknown-parameter handling) is the `default` block, and the one key that appears twice in the chain (`SwingAnim`) keeps its first branch, as in stock. The rewrite was generated mechanically from the Vineflower output (the script is not kept; re-run the transformation if the class changes). Why: 0.9 s of boot in a linear chain of up to 367 case-insensitive comparisons per item parameter. Both callers trim the key before calling, so the comparison semantics are the same. Verified with the `dumpItems` field dump (`pzopt.ScriptDump`) of every item script: identical with the switch on and off. ## zombie.core.PerformanceSettings (added 2026-09-19, frame limiter) Three public instance methods added, nothing else touched: `getMenuFramerateIndex`, `setMenuFramerateIndex(int)` and `getMenuFramerateChoices`, each a one-line forward to `pzopt.FrameCap`. The class is exposed to Lua (`getPerformance()`), so the added methods are what the "Menu framerate" combo calls; the combo itself is `src/lua/client/pzopt/pzopt_framecap_options.lua`, installed loose into the game dir's `media/lua/client/pzopt/` by `scripts/pzopt.sh` (build.sh copies `src/lua/` under `build/classes/media/lua/`). The Lua wraps `MainOptions:addCombo` and, right after the stock "Framerate" combo is added, adds a second one whose entries are "Same as in-game", "Uncapped" and the fps table; index 1 / 2 / 3.. is the convention `FrameCap` stores in `Zomboid/pzopt/framecap.ini` (`menuFramerateIndex=`), written the moment the option is applied because `Core.saveOptions` only writes keys it knows. Extra caps (added 2026-09-19, later): both combos list 500, 430, 400, 330 and 300 fps above the stock 244 (`FrameCap.FPS_TABLE` and the Lua's copy of it must agree; the framecap.ini index for the stock entries shifted by five). The stock "Framerate" combo is built with the extended list from the same `addCombo` wrapper, and the stock `'framerate'` GameOption, which hard-codes the stock indices in `toUI`/`apply`, is caught on `gameOptions:add` and given replacements that look the value up in the combo and apply it through `PerformanceSettings.setFramerateUncapped` / `setFramerate` instead of `Core.setFramerate` (which only knows indices 1..14). `Core.saveOptions` writes `frameRate=` from `getLockFPS()` so the value persists, but `Core.loadOptions` feeds it through an `IntegerConfigOption` clamped to 24..244 that rejects anything higher and leaves the lock at the option's 60 default; `FrameCap.applySaved` therefore also re-applies a saved capped value above 244 (it already re-read the raw lines for the uncapped case). No Core edit. Correction the same night: that re-read never worked, because the live `Core.loadOptions` ends with `saveOptions()`, so by the time `afterLoadOptions` ran the file already held the clamped 24..244 value and `uncappedFPS=false` (a saved `uncappedFPS=true` becomes `frameRate=60`, which is how forced `--prop uncappedFps=true` runs left the player's options.ini at 60 fps on 2026-09-19). `Core.saveOptions` also refuses a lock above 244 (the fake `IntegerConfigOption` rejects it), so the file can never hold the new caps. Now: `InitDisplay` calls `pzopt.FrameCap.beforeLoadOptions()` right before `Core.loadOptions()`, which snapshots the raw `frameRate=` / `uncappedFPS=` lines; `afterLoadOptions` re-applies them and any cap above 244 from `framecap.ini` (`gameFps=`). The extended combo applies through the new `PerformanceSettings.setGameFramerate(fps)` (0 = uncapped), which persists the above-244 value. A forced `uncappedFps=true|false` run writes a `restore=lock,uncapped,gameFps` line to framecap.ini and the next boot re-applies that instead of whatever the forced run saved on quit, so harness runs no longer change the player's frame-rate choice. Verified with two short boots: forced run logs "game uncapped", next auto boot logs "game 300 fps". Menu means every state that is not in-game or loading: logo, main menu, options, character creation. Optimizations tab (added 2026-09-20): six more public instance methods, again one-line forwards and nothing else touched: `hasPzoptOptions` (true when the build guard is on; since the master switch of 2026-09-20 evening it returns `pzopt.Overrides.buildMatches()`, so the tab is offered when the build matches even if the player switched every optimization off, otherwise nothing could switch them back on), `isPzoptOptionKnown(key)`, `getPzoptOption(key)` (the value in force since boot, from `pzopt.Config.value`), `getPzoptOptionDefault(key)`, `getPzoptOptionSaved(key)` (the player's saved value or ""), `getPzoptOptionPinnedBy(key)` ("" or `pzopt.properties` / `-Dpzopt.`) and `setPzoptOption(key, value)` ("" removes the key). They serve `src/lua/client/pzopt/pzopt_optimizations_options.lua`, which wraps `MainOptions:addDisplayPanel` and adds an "Optimizations" page right after Display: every `Config` key that is an optimization (not `instrument`, `dev`, `devRedrawFrame`, `dumpItems`, `translucentCache`, `uncappedFps`, the last is the Display combo) as a tick box (booleans) or a combo whose first entry is "Default (value on this machine)" (integers), grouped as rendering, chunk streaming, boot and load, with a tooltip per control. The choices go to `Zomboid/pzopt/options.ini` through `pzopt.UserOptions` the moment Apply is pressed, and `Config` reads that file at class init below `-Dpzopt.` and the install dir's `pzopt.properties` (harness runs write that file per run, so a run never depends on a menu choice; a key set there shows disabled in the tab with the pinning source in its tooltip). Choosing "Default" removes the key instead of writing the default's value, because defaults differ per machine (worker counts). Everything applies on the next launch: the GameOption's `apply` compares the boot value with the new one through the stock `GameOption:restartRequired`, so the stock "restart required" dialog appears exactly when a change matters. Verified by a verify run (`opttab-smoke`, `--prop bakeBudget=8`): the console logs "options tab: 35 controls, 1 pinned", no Lua errors; the click path was not exercised hands-off. Master switch (added 2026-09-20 evening): one more forward, `isPzoptEnabled` (`pzopt.Overrides.enabled()`, the value since boot). The switch itself is `Config.enabled` (default true), folded into `Overrides.ENABLED` next to the build check: `enabled=false` makes `Overrides.enabled()` false, which is the same stock fallback every override already takes on a build mismatch, so the other keys are ignored and no override needs a change. `Overrides.buildMatches()` exposes the build check alone. The tab shows the switch as a tick box above the sections, with a "since this boot: on / OFF" note in its heading and two buttons: "Enable all (recommended defaults)" ticks the switch and puts every other control back to "Default", "Disable all (stock game)" unticks it and leaves the other controls alone. Both only change the controls and mark the options changed; Apply / Accept saves them through the same `apply` handlers, so the restart dialog and `options.ini` behave as for any single change. A pinned `enabled` (pzopt.properties / `-Dpzopt.enabled`, e.g. a harness `--prop enabled=false` stock run) disables both buttons. Preview clips (added 2026-09-21 night): five more forwards, one line each, for the tab's preview panel: `getPzoptGifFrame(path, nowMs)` (the frame of an animated GIF under the game dir, as a `Texture`, or null while it decodes / if the file is missing), `getPzoptGifState(path)` ("loading" / "ready" / "missing" / "error"), `getPzoptGifWidth` / `getPzoptGifHeight(path)` and `releasePzoptGifs()`. They call `pzopt.GifTextures`, which decodes the GIF with ImageIO on a daemon thread (compositing the frame deltas per their disposal rule, thinning to 96 frames and scaling to 512 px wide), and makes one game `Texture` per frame on the game thread the way a Steam avatar is made (`ImageData` from RGBA rows -> `TextureID` -> `Texture`, uncompressed, at most four per call). The tab (Lua) draws, right of the control list, the stock and the optimized clip of the same route side by side for the setting under the mouse, the setting's description, its value since boot and at the next launch, and the effect bars (game thread, render thread, other cores, GPU, VRAM, RAM, disk, load time, chunk arrival, from a table in the Lua); the clips are `media/ui/pzopt/compare/-{stock,opt}.gif` (`harness/menu-gifs.py`). The two clips in use are the only textures held (96 x 512x256 RGBA each at most); `MainOptions:setVisible(false)` releases them. Since 2026-09-26 the clips are off by default (`previewClips`, Config key read by the Lua only): a "Before / after clips" tick box under "Sort by" in every tab's header saves it at once through `setPzoptOption` (no Apply), switches all three tabs, and releases the decoded clips when unticked; with it off the preview drops the clip slot and never asks for a GIF. Verified in game on 2026-09-21 (queue job `menu-check4`: hover, scroll, wheel over the preview, clips playing, panel sized to its content). Main-menu update item (added 2026-09-22): eleven more one-line forwards to `pzopt.Updater` for `media/lua/client/pzopt/pzopt_mainscreen_update.lua`: `pzoptUpdateCheck()` (starts the release check once per boot; a no-op with `updateCheck=false` or in a harness run), `getPzoptUpdateState()` ("idle", "checking", "up-to-date", "available", "downloading", "installing", "installed", "error"), `getPzoptUpdateTag` / `Notes` / `Published` / `PageUrl` / `InstalledCommit` / `Message` / `Progress`, `canPzoptUpdateInstall()` (a `pzopt-installed.txt` or `pzopt-files.txt` exists to replace) and `pzoptUpdateInstall()`. The check lists the GitHub releases on a daemon thread and picks the newest one (publish date) that carries `pzopt--classes.zip` for the running game; it is an update when the tag's commit (`b---`, before 2026-09-29 `win--`) differs from build-info's `commit=` and its publish date is after build-info's `built=` (both stamped by `build.sh` since this change, so a from-source build newer than the last release stays quiet). The install downloads the zip next to the game folder, checks the zip's revision, unpacks it into `pzopt-update.tmp/`, moves every file over the installed one, deletes what the previous manifest listed and the zip no longer has, and rewrites `pzopt-installed.txt` in the installers' format. The Lua adds an `ISLabel` styled like the stock items (`UIFont.Large`, the hover fade of `MainScreen.prerenderBottomPanelLabel`, the menu sounds) between Credits and Exit (Exit and the panel move down one row); it is always there like the stock items, greyed out and inert (no fade, no click) while the check runs, when the build is current or when the check failed, enabled once a newer build is offered; its text follows the state ("UPDATING... 43 %", "RESTART TO FINISH THE UPDATE"), and the dialog (`PzoptUpdateDialog`) shows the installed and offered builds, the release notes, a progress bar and Update now / Later, then Quit game / Later: classes the JVM already loaded stay the old ones until a restart. Never in the pause menu; no joypad entry (the stock list is hard-coded). Steam Workshop source (issue #16, 2026-09-26, `updateFromWorkshop`, default on): one more forward, `getPzoptUpdateSource()` ("workshop", "github" or ""). Before the GitHub request the check looks for the Workshop item in the library that holds the game (`/workshop/content/108600/3805285544/mods/ PZ_Optimization//pzopt-classes/`, the unpacked release zip that `workshop.sh --tag` stages); a copy counts when its build-info names this revision and a commit and every file of its `pzopt-files.txt` exists (Steam mid-update skips it). It is offered at once when newer than this build by the same rule, with its `built=` standing in for the publish date (no downgrade after a GitHub install). The GitHub answer then replaces the offer only with a later release; when GitHub is unreachable the item goes grey ("up-to-date") instead of "error". Installing the copy stages its files and runs the zip path's swap (`swapFolder` → `swapStaged`); Steam's folder is not touched. The dialog names the source and the Workshop change-notes page. Near-instant updater (2026-09-26, `docs/findings-updater-2026-09-26.md`): two more forwards, `pzoptRestartGame()` (`pzopt.Restart.relaunch()`: a helper process starts the game again once this one has quit; the dialog's "Quit game" is now "Restart game", which calls it and then the stock `quitToDesktop`) and `getPzoptUpdateDrive()` (the `devUpdateDrive` rig's mode for the Lua: "", "drive", "restarted:"). The install now goes through `pzopt.UpdateDelta`: the changed zip entries only (range requests, prefetched in the background once offered), written beside their targets and renamed over them; unchanged files are not rewritten. Settings export / import (2026-10-01): three forwards to `pzopt.UserOptions` for the "Export settings" / "Import settings..." buttons on the Optimizations, Enhancements and Profiler tabs: `getPzoptSettingsExportText(body)` (the export's comment header, date and build, above the Lua's `key=value` lines), `pzoptSettingsExportWrite(text)` (writes `settings-export.ini` beside options.ini; its path, or "" when the write failed) and `getPzoptSettingsExportFile()` (that file's text for the import dialog, "" when absent). The clipboard is the stock `Clipboard` the Lua already reaches; the controls are set by the Lua like the profile buttons, saved on Apply. In-game uninstall (2026-09-27): three forwards to `pzopt.Uninstall` for the Optimizations tab's "Uninstall PZ Optimization..." button: `getPzoptUninstallUnavailable()` ("" or why the button is off: a harness run, or no `pzopt-installed.txt` / `pzopt-files.txt` in the game folder), `pzoptUninstall()` (undoes AotCache's and GcChoice's launcher edits, writes the file list, starts a helper that deletes the files once this process has ended; the Lua then calls the stock `quitToDesktop`) and `getPzoptUninstallMessage()`. The button is off in a world (the quit would skip the save). Performance overlay item (added 2026-09-23): three forwards to `pzopt.Overlay` for `media/lua/client/pzopt/pzopt_mainscreen_overlay.lua`: `togglePzoptOverlay()` (`Overlay.toggle()`, the same path as the key binding, which now calls it too: show / hide, or the "sampling is off" notice while `overlaySampling` is off), `isPzoptOverlayVisible()` and `isPzoptOverlaySampling()`. The Lua adds an `ISLabel` styled like the stock items right below Options in both the main menu and the pause menu (every item under Options moves down one row; the multiplayer pause menu's per-frame re-layout in `MainScreen:render` is redone after the stock one), "SHOW / HIDE PERFORMANCE OVERLAY" following the overlay's state. Controller: the label gets a row after Options in `joypadButtonsY` (after the stock `onGainJoypadFocus` rebuild and every frame while the menu has the focus) and A on it toggles. Not added with `enabled=false`. ## zombie.core.skinnedmodel.model.Model (added 2026-09-19, night, game load; GitHub issue #1) `CreateShader(name)`: the stock method always posts a lambda to the render thread and waits for it, even when `ShaderManager` already holds the shader. Every `Model` constructor calls it, and the render thread only drains that queue once per render step, so each model built off the render thread costs one loading-screen frame. On the desktop that is ~1 ms; on the laptop the laptop of issue #1 the loading-screen step is ~220 ms and the 73 animal models `AnimalDefinitions.loadAnimalDefinitions` builds (all `animalEffect`) were 16.5 s of the 31 s load. Now, when `shaderCache` is on, the method first asks `pzopt.ModelShaders` for a shader an earlier model already created for the same name and static flag and takes it without the round trip; only the first model per shader still posts to the render thread, and that call's wall time is recorded. `ModelShaders.summary()` ("model shaders: N cached, M render-thread round trips (x s waited), K served from the cache") is logged when the boot pump stops, when the load starts and at the harness's "world ready", so the trace shows the stall on any machine. The cache is safe because `ShaderManager` never removes shaders and a shader reloaded by the debug file watcher recompiles in place. Config key `shaderCache` (default true). The class-load marker goes in a static initializer like the others. ## zombie.core.textures.ImageData (added 2026-09-20, texture load; GitHub issue #2) A file-pool worker (`pool-1-thread-18`, `FileTask_LoadPackImage.call` → `initMipMaps` → `generateMipMaps`) crashed the JVM on the laptop the issue #2 laptop with a SIGSEGV inside the C2-compiled `scaleMipLevelMaxAlpha`. Nothing at the Java level can produce it: the `ImageData` is local to the task, its `MipMapLevel` buffers are freshly malloc'd and only ever read through bounds-checked `ByteBuffer.get(int)` / `put(int, byte)`, and no other thread sees the object until `call` returns. Reading the `hs_err` (copied from the laptop over SSH) settles it: the faulting instruction is a plain reload of a spill slot from the thread's own stack, `mov r14d, [rsp+0x88]`, with no address-size prefix; the reported fault address is exactly that stack address truncated to 32 bits, and the stack page was mapped (the crash log dumps it a few lines later). Eight seconds after the JVM died, `systemd-coredump` (compressing the core, in libzstd) segfaulted on the same laptop with a 32-bit-truncated address too, and fifteen minutes earlier the same laptop's previous game process had aborted inside the C2 register allocator (`PhaseChaitin::Simplify`, a `SIGABRT` coredump with an empty `hs_err_pid3802.log`), after which the machine was rebooted. Three faults in three unrelated code bases within fifteen minutes, two with truncated addresses, is the machine (CPU, memory or its `7.2.4-1-cachyos-custom` clang-built kernel, while the packaged 7.2.6 kernels are installed but not booted), not the game or the overrides. A 5-minute, 18-thread hammer of the stock mipmap loops on the laptop's own Zulu 25 JRE (`tools`-style reflection driver, see the issue) did not reproduce it. The override still exists because it was written before the crash log was readable and is harmless: when `mipmapArrays` is on and the `worldMipmapColors` debug option is off, `scaleMipLevelMaxAlpha`, `scaleMipLevelAverage` and `performPreMultipliedAlpha(MipMapLevel)` return early into `pzopt.MipMaps`, which reads each parent row pair with one bulk `get`, builds the sub row in a thread-local `byte[]` with plain array indexing, and writes it with one bulk `put`; the compiled loop has no per-byte direct-buffer access left. The sub buffer is still rewound first, as in stock. Output is byte-identical (`tests/pzopt/MipMapsTest` drives the jar's own private methods by reflection over 40 size/alpha variants) and the speed is the same (2048² → level 1: stock 14.3 ms, ours 13.5 ms, warmed). Config key `mipmapArrays` (default true); off, or with the debug colours on, the stock loops run untouched. Class-load marker in a static initializer. It is not a fix for the laptop: if the crash recurs there, boot the packaged kernel and run a memory test before touching the code. ## zombie.iso.weather.fx.WeatherFxMask (added 2026-09-20, game thread) Regenerated with Vineflower; one decompiler fix (the "Calc Bounds" profile area local shared its name with the `bRender` boolean captured by the two rasterize lambdas; renamed, marked `// pzopt: decompiler fix`). 1. **Load marker** in a `static {}` block. 2. **Idle skip** (`pzopt.Config.WEATHER_MASK_IDLE_SKIP`, default true). At the top of the masked branch of `renderFxMask`, a private `pzoptMaskIdle` returns when nothing of the pass could reach the screen: the player is exterior (no interior tint layer), no cloud layer, no fog layer at fog quality 2, no precipitation layer at the precipitation option 1, and no debug mask view. Then neither `scanForTiles` nor `drawFxMask` runs; the mask state is left as it was, so the next active frame continues it. 3. **Scan gate.** `scanForTiles` returns at once when the scan could not add a mask: the player mask needs no update or has nothing to draw, or the player is in no building and no fog-mask region (`isInPlayerBuilding` can then never be true). Stock rasterizes the whole view every frame in that state (tens of thousands of squares at max zoom, `isInteriorLocation` per exterior tile: 1.5 to 3.7 % of the game thread, 70 % of the pass in slow frames). 4. **Building-only scan.** When the player stands in a building and not in a fog-mask region, a private `pzoptScanBuildingOnly` visits the squares of the building's `BuildingDef` bounds plus one tile of margin (a square outside the building only consults its N, W and NW neighbours) that pass the class's own `isOnScreen`, calling `addMaskLocation` for each, instead of rasterizing the view; `scanForTiles` then returns. The mask contents are the same squares stock would have found. Same Config key. The whole pass went from 2.6 to 0.5 % of the game thread outdoors and from 6.6 to 2.4 % on the spinning route through Rosewood. ## zombie.iso.objects.IsoLightSwitch (added 2026-09-20, game thread) 1. **Load marker** in a `static {}` block. 2. **Electricity check cache** (`pzopt.Config.LIGHT_SWITCH_CHECK_FRAMES`, default 15; 0 = stock). `hasElectricityAround()` keeps its last answer and the frame it was computed (`IsoWorld.getFrameNo`) in two new fields and returns it while fewer than that many frames have passed; the stock body moved to a private `pzoptHasElectricityAroundNow`. `LightingJNI.checkLights` asks every light source's first switch for power every frame (grid power, generators, the 3x3x2 neighbourhood): 2.3 % of the game thread on the Rosewood route, 0.3 % after. A power change shows on a lamp up to 15 frames late. ## se.krka.kahlua.j2se.KahluaTableImpl (added 2026-09-20, Lua VM) 1. **Load marker** in a `static {}` block. 2. **Single lookup in `rawget(Object)`.** Stock does `containsKey` and then `get` on the delegate map (two hash lookups per table read; the Lua UI and `OnTick` handlers do millions per second). Now one `get`; a null result means the key is absent, which is exactly the stock `containsKey` test because `rawset` removes the key on a nil value and never stores null. The metatable fallback is unchanged. The reload-replace and data-breakpoint code before it is untouched. ## zombie.core.opengl.RenderThread (added 2026-09-20, performance overlay) Three one-line hooks in `lockStepRenderStep`, all into `pzopt.Overlay`, plus the load marker `static {}` block. Inside the `spriteRendererPostRender` probe, `pzopt.Overlay.gpuBegin()` precedes `SpriteRenderer.instance.postRender()` and `pzopt.Overlay.gpuEnd()` follows it: a `GL_TIME_ELAPSED` query around the frame's draw-command replay, which is the frame's GPU work (the swap is outside it). After the `displayUpdate` probe closes, before the stock `FPSGraph.addRender` call, `pzopt.Overlay.onSwap()` records the presented-frame time, the same instant MangoHud logs from. With the build guard off every hook is a static boolean test. Harness virtual pad (2026-09-24): in `renderLoop` the stock `GameWindow.GameInput.poll()` is `pzopt.VirtualPad.poll()`, which is that same call unless the harness flag file names a pad script (`--flag pad=