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.
Brainlayers' bead renderers repeat a large amount of work every frame:
size × size cells, resolves each one through BeadPalette, and emits a full quad per bead. A 128×128 board is up to 16384 quads / 65536 vertices per table, per frame.stack.getTag().copy(), a full NBT deep copy (potentially a 16384-entry int array), purely to read a few values.PegboardCanvas getters: pixels() / colors() / stock() all return clone(), producing tens of KiB of pure garbage per table per frame.The result: frame rate collapses specifically when a pegboard comes into view.
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. |
MeshCache bounds baked meshes by a GPU byte budget, and specifically handles the classic failure mode where the working set exceeds the budget:
hotTicks ticks is never an eviction candidate, so whatever is currently on screen stays resident.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.
All switches are client-side. With the master switch off, every patched renderer behaves exactly like stock Brainlayers.
enabled — master switch.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.enabled — greedy same-colour run merging.artworkItems — artwork item renderer optimization.com.betterbeads.render, not com.betterbeads.mixin. Every class in a package claimed by a mixin config is treated as a mixin, and Mixin fails such a class with an IllegalClassLoadError the moment ordinary code touches it.remap trade-offs: Brainlayers' own members use remap = false (no obfuscation mapping exists). renderByItem is Minecraft's BlockEntityWithoutLevelRenderer method 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 in PegboardCanvasAccess.BetterBeadsMixinPlugin logs at INFO from postApply, 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.[BetterBeads]