Perler Render Optimization
A client-side rendering optimization mod for Brainlayers. It replaces Brainlayers' bead rendering paths via Mixin, significantly reducing per-frame CPU and GPU submission cost without changing a single pixel of the output.
Background
Brainlayers' bead renderers repeat a large amount of work every frame:
- Pegboard tables: every frame walks all
size × sizecells, resolves each one throughBeadPalette, and emits a full quad per bead. A 128×128 board is up to 16384 quads / 65536 vertices per table, per frame. - Worn and held bead decorations: every frame rebuilds the full extruded artwork — up to six quads per bead, so up to ~98k quads for a 128×128 piece — plus a deep copy of the decoration NBT.
- Pattern paper and artwork items: every draw starts with
stack.getTag().copy(), a full NBT deep copy (potentially a 16384-entry int array), purely to read a few values. PegboardCanvasgetters:pixels()/colors()/stock()all returnclone(), producing tens of KiB of pure garbage per table per frame.
The result: frame rate collapses specifically when a pegboard comes into view.
What This Does
The core idea is to bake geometry once and submit it as a single draw per frame, while eliminating per-frame allocation and deep copies.
| Optimization | Description |
|---|---|
| Static VBO cache | Bakes the quad soup into a VertexBuffer (BakedMesh). Vertices are stored in model space and the pose is applied at draw time, so an animated decoration can reuse the same geometry. |
| Greedy run merging | BeadGeometry collapses adjacent same-colour cells into single rectangles using a column-height sweep. Real artwork typically loses 70–95% of its quads; the two large flat faces are almost fully merged, while silhouette faces stay per-cell so the rasterized result is unchanged. |
| Deep-copy-free NBT reads | The renderers only read, so they borrow the live tag and skip Brainlayers' data(stack) deep copy entirely. |
| Constant-time cache keys | MeshKey keys off object identity (live int[] / NBT tag) or PegboardCanvas.revision, giving O(1) comparison instead of the old O(size²) full-pixel comparison every frame. |
| Pre-resolved colour tables | BeadColors folds the palette into flat int[] tables at class-init time, so the hot loops collapse to a single bounds-checked array read with no branches and no floating point. |
| Scratch object reuse | The pose chain reuses static Quaternionf / Vector3f instances, removing roughly a dozen temporaries per decoration per frame. |
| Bypassing cloning getters | @Accessor exposes PegboardCanvas's backing arrays directly, avoiding a full array clone per getter per frame. |
| Memoized legacy decode | Legacy byte[] artwork is decoded once and cached for the last two sources, instead of once per frame. |
What Is Deliberately Untouched
- Pendulum simulation: animation physics are left exactly as stock Brainlayers.
- Connectors (leash / string / chain): drawn the same way as before.
- Collision model: the per-triangle separating-axis sweep is unchanged — it affects gameplay, and it is not what costs frame time here.
Cache Design
MeshCache bounds baked meshes by a GPU byte budget, and specifically handles the classic failure mode where the working set exceeds the budget:
- Hot-frame protection: any mesh drawn within the last
hotTicksticks is never an eviction candidate, so whatever is currently on screen stays resident. - Overflow pool: a mesh that genuinely does not fit is parked in a small keep-alive pool rather than deleted on the next lookup, turning an alternating working set from one rebuild per frame into one rebuild per pool pass.
- The eviction clock is driven by the client tick rather than the render thread, so it does not depend on frame pacing and a paused game still ages entries.
Without these two changes, a plain LRU would re-bake and re-upload every visible board every frame — strictly worse than the stock renderer, which at least wrote into one shared buffer.
Configuration
All switches are client-side. With the master switch off, every patched renderer behaves exactly like stock Brainlayers.
general
enabled— master switch.
gpuCache
tableBoards— cache the pegboard table board mesh (the single biggest win in the world).decorations— cache the extruded artwork of worn and held bead decorations.patternPapers— cache printed pattern paper sheets.artworkItems— cache the extruded artwork item mesh.maxMegabytes— upper bound on total vertex buffer memory (default 128 MB; a 128×128 board is roughly 0.25 MB fully merged).hotTicks— hot-frame protection window.overflowPool— size of the keep-alive overflow pool.
runMerging
enabled— greedy same-colour run merging.
itemRenderers
artworkItems— artwork item renderer optimization.
Implementation Notes
- Mixin package isolation: render helpers live in
com.betterbeads.render, notcom.betterbeads.mixin. Every class in a package claimed by a mixin config is treated as a mixin, and Mixin fails such a class with anIllegalClassLoadErrorthe moment ordinary code touches it. remaptrade-offs: Brainlayers' own members useremap = false(no obfuscation mapping exists).renderByItemis Minecraft'sBlockEntityWithoutLevelRenderermethod and must stay remapped, otherwise Mixin aborts with"Overwrite target was not located".@Invoker/@Accessor: used to reach Brainlayers' package-private / private members. An accessor interface may only declare annotated methods, so convenience wrappers live inPegboardCanvasAccess.- Debug plugin:
BetterBeadsMixinPluginlogs at INFO frompostApply, distinguishing the two failure modes that look identical from the outside — a mixin config that was never read, and one that was read but whose entries silently did not apply.
Requirements
- Minecraft Forge (client)
- Brainlayers
- Log prefix:
[BetterBeads]

