Per mod/docs/optimize-layer-design.md Stage 3 (continued). This batch
implements the Stage 1 mixin-contract interfaces on their target MC classes
so CachedName, StateEntity, TickCached, CachedLight, CachedValidity and
CachedStack actually get used. Seven new mixin'ов; no new helpers (the
helpers were all written in Stage 1 already).
Entity-contract mixins (mixins/optimize/entity/):
- EntityNameMixin: implements CachedName on Entity — adds the per-tick
cached name-tag fields (Component + tick). Vanilla Entity.getDisplayName()
rebuilds the team prefix + name + suffix on every call.
- EntityRenderStateEntityMixin: implements StateEntity on EntityRenderState —
adds the back-reference field to the source Entity. Render-states are
rebuilt every frame so they can't keep their own pointer; this is captured
by EntityRendererStateMixin.
- EntityRendererStateMixin: Inject at RETURN on EntityRenderer.createRenderState
— captures the source Entity on the freshly-built state via the StateEntity
contract. ZakoOpt used @ModifyReturnValue; we use plain @Inject because the
MixinExtras ModifyReturnValue transformer is more fragile across MC versions.
- EntityRendererNameMixin: WrapOperation on EntityRenderer.getNameTag's
Entity.getDisplayName call — caches by game tick so the per-frame cost is
one identity check + read instead of a full Component rebuild.
- SodiumEntityRendererMixin: WrapOperation on Sodium EntityRenderer.renderCuboid's
MemoryStack.stackPush — uses CachedStack (Stage 1) to skip the ThreadLocal
lookup on the render thread. Registered as optional via LoVisualMixinPlugin
(only applied when Sodium is loaded); remap = false because the target is
a Sodium class, not a Mojang one.
BlockEntity-contract mixins (mixins/optimize/block/):
- BlockEntityMixin: implements TickCached + CachedLight + CachedValidity on
BlockEntity — adds the per-tick cache slot, per-tick block-light lookups
(single + pair for double-chests), per-tick should-render distance check,
and per-state validity cache. All four are read by the (Stage 3, future)
ChestRendererMixin / BlockEntityRenderStateMixin / BlockEntityRenderDispatcherMixin.
- BaseSpawnerMixin: implements TickCached on BaseSpawner — adds the per-tick
cache slot that SpawnerCache (Stage 1) reads back.
Folder limit (≤4 .java per folder):
- mixins/optimize/entity/ hit 6 files after this batch — split into
mixins/optimize/entity/renderer/ for EntityRendererStateMixin +
EntityRendererNameMixin (the two that target EntityRenderer rather than
Entity/EntityRenderState directly). The other 4 stay in entity/.
lovisual.mixins.json: seven new entries in the 'client' array, alphabetical:
optimize.block.BaseSpawnerMixin
optimize.block.BlockEntityMixin
optimize.entity.EntityNameMixin
optimize.entity.EntityRenderStateEntityMixin
optimize.entity.SodiumEntityRendererMixin
optimize.entity.renderer.EntityRendererNameMixin
optimize.entity.renderer.EntityRendererStateMixin
LoVisualMixinPlugin: added SodiumEntityRendererMixin to OPTIONAL_SODIUM_MIXINS
so the mixin is only applied when Sodium is loaded (matches the existing
convention for sodium.* mixins).
Tests: no new tests in this batch (the new mixins are pure side-effect
field-bags — they don't have unit-testable logic). Existing suite still green.
Total: 866 tests, 0 failures. checkFolderLimit: OK (0 папок >4).
Per mod/docs/optimize-layer-design.md Stage 3 (continued). Three more simple
mixin'ы are added on top of the existing RenderBackendOps + lazy-clear/TBO/
RenderTypes trio:
mixins/optimize/entity/LivingEntityLodMixin.java:
- ModifyExpressionValue on LivingEntityRenderer.submit's shouldRenderLayers
call — return false when the entity's distanceToCameraSq is beyond the
Stage 1 entity_lod threshold. Skips the layer pass (armour, held items,
eyes, cape, elytra, stuck arrows) for far entities. Ported from
zako.opt.mixin.entity.LivingEntityLodMixin.
mixins/optimize/particle/BubbleColumnUpParticleMixin.java:
- WrapOperation on BubbleColumnUpParticle.tick's ClientLevel.getFluidState
call — consult BubbleColumnCache first, store on miss. Cache is cleared
per-tick (BlockPos.asLong key + game-tick tracking). Ported from
zako.opt.mixin.particle.BubbleColumnUpParticleMixin.
mixins/optimize/world/LevelRendererOutlineMixin.java:
- Inject on LevelRenderer.doEntityOutline HEAD-cancellable — skip the
outline render when no glowing entity was seen this frame (OutlineTracker).
- WrapOperation on LevelRenderer.addMainPass's clearColorTexture — call
OutlineTracker.markGlowing() to record that the renderer is about to
draw an outline this frame. (Conservative: marks every clear, not just
the entity-outline one — ZakoOpt uses @Local for precision, we keep it
simple to avoid MixinExtras @Local fragility across MC versions.)
- Ported from zako.opt.mixin.entity.LevelRendererOutlineMixin.
Helpers (features/.../optimize/):
- particle/TickCache<V>: generic per-tick keyed cache (Long2ObjectOpenHashMap).
BubbleColumnCache wraps it for FluidState. Other per-tick caches
(ParticleLightCache etc.) can reuse it in later Stage 3 commits.
- particle/BubbleColumnCache: thin wrapper over TickCache<FluidState>.
- world/OutlineTracker: per-frame state for the outline-skip optimization
(glowingThisFrame + dirty flag, beginFrame/markGlowing/markCleared API).
lovisual.mixins.json: three new entries in the 'client' array:
optimize.entity.LivingEntityLodMixin
optimize.particle.BubbleColumnUpParticleMixin
optimize.world.LevelRendererOutlineMixin
Tests (JUnit 5):
- TickCacheTest: 9 tests covering get/put/clear-on-tick-change/multi-key/
overwrite/null-values/long-keys.
- OutlineTrackerTest: 8 tests covering enabled flag, skip-when-no-glowing,
begin-frame-clears, dirty-tracking, needsClear logic.
- (BubbleColumnCache itself is a 4-line wrapper around TickCache so its
test would just re-test TickCache — skipped.)
Total suite: 866 tests, 0 failures. checkFolderLimit: OK (0 папок >4).
Per mod/docs/optimize-layer-design.md Stage 3 (continued). The mixins/optimize/
folder hit the project's ≤4 .java-files-per-folder rule when we tried to add
the next batch of mixin'ов (LivingEntityLodMixin, BubbleColumnUpParticleMixin,
LevelRendererOutlineMixin). Move the existing 4 mixin'ов into semantic subfolders:
- mixins/optimize/gl/GlCommandEncoderClearMixin.java (was mixins/optimize/)
- mixins/optimize/gl/GlCommandEncoderTexBufferMixin.java
- mixins/optimize/gl/RenderTypesMixin.java
- mixins/optimize/hud/OptimizeDebugHudMixin.java
lovisual.mixins.json: the 'client' array entries are updated to use the
new dotted paths (optimize.gl.*, optimize.hud.*). The package field in
each mixin file is updated to match.
No functional changes — this is purely a code-organization commit to make
room for the new Stage 3 mixin'ов in the next commit.
Per mod/docs/optimize-layer-design.md.
Status header: was 'черновик на согласование (2026-10-09)', now
'Этап 1 готов, Этап 2 + начало Этапа 3 готовы (2026-10-10)'.
New 'История изменений' section: three dated entries (Stage 1, Stage 2,
Stage 3 partial) listing files / tests / patch references.
Stages section: each line now carries a status icon (✅/🔄/❌), a completion
date, and a one-line summary of what shipped. Stage 3 partial lists what's
done (RenderBackendOps + GlRenderBackendOps + 3 GL mixins) and what's still
left (FrameFence, SharedDepthFbos, SkinAtlas, AnimFreeze, ItemLod,
LivingEntityLodMixin, ParticleMixin, SpawnerRendererMixin, etc.).
New 'Оценки для Этапов 3-4' section: rough effort estimates for the
remaining Stage 3 work (~25 mixins + ~8 helpers, 3-4 work sessions) and
Stage 4 (6 Vulkan items, profiling-driven, 1+ session for vkCmdCopyImage
which is needed for SkinAtlas to work on Vulkan).
Per mod/docs/optimize-layer-design.md Stage 3 (partial). This commit ships
the simplest GL mixins and the strategy interface that splits GL/Vulkan
backends. SkinAtlas/AnimFreeze/HudCache/FrameFence deferred to a later pass
because they're more complex (vertex-format / glCopyImageSubData /
RenderPipeline+RenderPass).
RenderBackendOps interface (features/.../optimize/gl/RenderBackendOps.java):
- Backend-agnostic surface for lazy clear, TBO cache, RenderTypes cache.
- current() resolves the active backend — returns GlRenderBackendOps on GL,
null on Vulkan (so all WrapOperations fall through to the vanilla call).
- Stage 4 will add a VulkanRenderBackendOps no-op implementation.
GlRenderBackendOps (features/.../optimize/gl/GlRenderBackendOps.java):
- Concrete GL implementation. Three caches ported from ZakoOpt:
* Lazy clear (from zako.opt.mixin.gl.GlCommandEncoderClearMixin).
* TBO cache (from zako.opt.gl.TexBufferCache + the TexBufferMixin).
* RenderTypes cache (from zako.opt.mixin.gl.RenderTypesMixin).
- All three gated on OptimizeToggles.microOpts() (the Stage 1 'micro_opts'
toggle wired in Optimize.java).
- Includes test helpers (resetForTesting, boundTboTextureForTesting,
cachedRenderTypeCountForTesting).
Three new mixins (mixins/optimize/):
- RenderTypesMixin: WrapOperation on RenderTypes.entityTranslucent's
BiFunction.apply — cache by Identifier when flag=true (the common case).
Drops the per-call Objects.hash(Pair) hit.
- GlCommandEncoderClearMixin: WrapOperation on GlCommandEncoder's
clearColorAndDepthTextures → GlStateManager._glBindFramebuffer — skip the
bind-framebuffer-0 because the next render pass binds its own target
anyway and GlStateManager's cache already skips redundant binds.
- GlCommandEncoderTexBufferMixin: WrapOperation on GlCommandEncoder's
trySetup → glBindTexture (track bound) and glTexBuffer (skip re-attach
when the same (format, buffer) pair is already attached). Re-attaching
makes the driver revalidate the texture (clouds: ~2% of a frame).
lovisual.mixins.json:
- Register all three mixins + OptimizeDebugHudMixin in the 'client' array,
alphabetically under optimize.* (before accessors.*).
OptimizeDebugHudMixin (mixins/optimize/OptimizeDebugHudMixin.java):
- F3 overlay for the Optimize layer — Stage 2 of the design doc.
- Adds three blocks of lines to F3 (after the existing LoVisual Engine block):
* 'LoVisual Optimize' — toggles summary + budget mode + scale + frame time.
* 'frame:' — EMA-averaged per-section timings (entities/particles/HUD/effects/text).
* 'props:' — when any -Dlovisual.opt.* is set, list which ones.
- Mirrors the existing hud.DebugHudMixin pattern (ModifyArg on extractLines).
Tests (JUnit 5): 11 new tests in GlRenderBackendOpsTest — pure-logic pieces
of RenderBackendOps + Identifier equality/hash. Total suite: 849 tests,
0 failures. checkFolderLimit: OK (0 folders >4).
Per mod/docs/optimize-layer-design.md Stage 1: pure helpers and mixin-contract
interfaces for the Optimize module, plus the OptimizeToggles engine-side state
holder and nine new toggles in Optimize. GL/Vulkan hooks are Stages 3-4.
- /download: the jar as an in-game item (pixel heart in an inventory slot, vanilla-style tooltip with version, size and SHA-256, copy button), spec list, three install steps, release timeline
- releases read through a cached nginx proxy (/api/releases): Forgejo's API has no CORS headers
- README, footer and landing links point to git.wihuk.pro/boba/LoVisual (the GitHub repo they used does not exist)
- release workflow publishes through the Forgejo API and ships lovisual.jar.sha256
- fix i18n interpolation placeholders ({{x}}) in the download strings
minio/minio is gone from Docker Hub (use the bitnami image that starts without a command), install psql/protoc/socat in the bare job container (no sudo), forward the services to localhost, and fetch non-mirrored actions from github.com.
All four services now share one builder stage (dependencies compile once per
deploy, not once per service). BuildKit cache mounts keep the cargo registry
and target/ between deploys: a one-line change rebuilds in ~16 s instead of
recompiling the whole dependency tree.
- mail/ module (lettre, ru/en templates), links built only from PUBLIC_BASE_URL
- migration 0006: accounts.email_verified_at, shared email_tokens table (verify + reset), existing accounts marked verified
- reset mailer no longer logs tokens; RESET_MAIL_MODE=log is dev-only and refused with COOKIE_SECURE=true; Disabled by default answers 503
- forgot-password and resend-verification do their work in a background task (no timing oracle)
- device linking requires a verified email; email_verified exposed via /me and gRPC
- gateway rate limits, SMTP_* in compose and .env.example
- frontend: verify, forgot-password, reset-password pages, verify banner, ru/en strings
- Cosmetics: new Settings category with kind pills (Wings, Hats, Models, Masks, Weapons) and a card grid; Wings, ChinaHat, TazikHat,
CustomModels, TrollfaceMask and Knife moved to ModuleCategory.ACCESSORIES and hidden from the Modules panels
- Card previews: pick a picture (native dialog or drag and drop) and crop it in an editor; PreviewCrop, PreviewStore, PreviewEditor
stored under config/lovisual/cosmetics/previews
- HUD: right click a widget while the chat is open to get a popup with its toggles, sliders and modes (quick settings)
- Themes tab: HUD and ClickGUI pages; new ClickGuiLookConfig replaces hard-coded panel blur constants, HudGlobalConfig look defs
- TotemSound: own sound on totem pop (everyone/self/others, volume, pitch, mute vanilla through SoundEngineMixin)
- tests: CosmeticKind, PreviewCrop, QuickSliderMath, TotemSound; ./gradlew test green
- GodRays: PostProcessPass PRE_HAND, godrays.frag (radial sky blur to the sun over the scene, depth from mainDepth),
GodRaysUniforms, GodRaysSkyMixin takes sunAngle/sunset colour from SkyRenderState; off under shaderpacks (Iris)
- Knife (Melee Skins): 33 skins from Bedrock geo/anim in assets/lovisual/knife, animator (static_idle, draw/inspect/melee/run,
0.2 s crossfade, Catmull-Rom) with background baking, first/third-person render, sounds, mixins ItemInHand/PlayerItemInHand/Minecraft
- Wings: wingSource Procedural/Model with 14 textured Bedrock models (WingModelDef/WingModels/WingModelRenderer, BedrockClip +
procedural angel/demon/dragon); shared Bedrock parser util/bedrock (GeoParser, GeoFaces, Channel, Clip)
- EnchantGlint: Pulse/Fire/Galaxy/Hologram/Glitch/Sparkle/Aurora styles in glint/style, coverage setting, custom animated
texture matrix via GlintTextureMixin.styleMotion; lang en/ru for all new settings
- Themes: shader backdrop for ClickGUI (Backdrop record, 6 modes, intensity/speed/scale, ui_theme_backdrop_batch.frag with
700 ms REVEAL/ERASE wipe, ThemeBackdrop2D in BackgroundComponent), ThemeEntry.backdrop persisted in JSON and profile codec,
6 presets with backdrops, backdrop settings in the theme editor
- tests: GodRaysSun, GlintStyles, Knife assets/animator, WingModel, Backdrop, bedrock Channel; ./gradlew build green
- AutoSprint, TotemCounter, EnchantGlint (styles, texture repaint, scroll speed/scale mixin), ArmorColor (durability colouring, targets, pulse),
PlaceAnimation (block slides in from the hand; chunk-mesh hiding incl. Sodium, moving-block render), aligned with evo behaviour
- CustomModels: Chicken, Rabbit and Verity (Blockbench) models for chosen players, bundled assets, render-state carrier + submit redirect
- KineticLyrics: fix track key wiped by reset() (lyrics never applied, lrclib hammered every tick); new LyricsFinder with ranked lrclib search,
unsynced timing estimate, NetEase fallback and retry on busy services; tests for the pure parts
- fix mode()/modeSetting() misuse for new modules; TODO updated
Mod:
- %config slots/pull/publish/unpublish for the four cloud slots
- %showcase [new|popular] [page] | load | copy, with number references
- %cloud opens a CloudScreen (link/unlink, slots, showcase) on vanilla widgets
- %config load IDDQD works offline (everything off except ChinaHat)
- default backend URL is now the production gateway
- shared ConfigRemoteApplier and ClientThread replace per-class copies
- %link unlink revokes the link on the server, then clears the local token
Backend:
- POST /device/revoke (RFC 7009 style self-revoke, always 204), rate limited
to 10/min per IP at the gateway
CloudScreen is compiled but has not been opened in a running client yet.
Lighthouse (production, default mobile 4x-CPU profile) scored
Performance 33/100, dominated by one ~536-538 KB entry chunk that
bundled every route's code regardless of which page a visitor landed
on -- the "chunks are larger than 500 kB" Vite warning on every build
this session.
Converted every route except / and the 404 catch-all to React Router
8's native route.lazy (blocks navigation until the chunk is ready --
no separate Suspense fallback needed). AppShell/HomePage/NotFoundPage/
RequireAuth stay eager since they're needed on every load regardless.
Result: main chunk 490.84 KB, no >500kB warning, one small chunk per
route (LoginPage 1.4 KB, ThemesPage 15.5 KB, AccountPage 11 KB, etc.)
loaded only when actually visited.
Fixed a real race the lazy boundary exposed: /login and /register
share an "Почта" field label, and during the chunk swap between them
the old page can stay mounted a beat longer than before -- a test
grabbing the field by label could land on the about-to-unmount one.
Also bumped RTL's asyncUtilTimeout and vitest's testTimeout, since the
route-transition + findBy chain didn't always land inside the old
1000/5000ms defaults under full parallel test-suite load.
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
- robots.txt now also points at a real sitemap.xml (public pages
only: /, /download, /themes, /showcase) and disallows the
auth-gated routes (/account, /configs, /link) that have nothing
useful to a crawler
- canonical <link> on the root page
- SoftwareApplication JSON-LD so Google can render a rich result
(name, free price, icon) instead of a plain blue link
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
Lighthouse audit findings:
- No robots.txt existed, so nginx's SPA fallback (try_files -> index.html)
served the React app's HTML at /robots.txt -- crawlers got 44 "syntax
not understood" lines instead of directives.
- No Cache-Control on /assets/*, so every visit re-fetched a hashed,
content-addressed bundle that can never actually change under that
URL. Lighthouse estimated 936 KiB/visit wasted on this alone.
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
Settings values and visibility predicates move to ButtonStyleSettings,
icon drawing to ButtonIconRenderer; facade keeps the public static API
and getters. Drops three dead one-liners (getLastContext,
renderCenteredText, renderLeftAlignedText) and the unused
renderButtonInternal wrapper.
Rendering moves to ScriptedTriangulatorRenderer, prop maps and cache
signatures to scripted/panel/; records and row helpers stay nested so
caller references (Panel, Palette, row, rows, idString) keep working.
React.memo alone does nothing here: PaletteTabs built a fresh
`(hex) => setColor(slot, hex)` closure per field on every render, so
memo's prop comparison always saw a "changed" onChange and re-rendered
anyway. Stabilized it properly:
- setColor/setGradient read draft/onChange from a ref (updated in a
useLayoutEffect, not during render -- oxlint's react(refs) rule
correctly flags a ref write in the render body) so they keep a
stable identity (useCallback, empty deps) across edits
- per-slot wrapper closures are built once in a useMemo keyed off
those now-stable functions, not off `draft`
This doesn't change the preset-switch case measured earlier (every
field's value genuinely changes then, so memo correctly re-renders
all of them -- that cost is what startTransition already covers). It
does stop sibling fields re-rendering on interactions that don't
touch them: undo/redo, tab switching, hovering unrelated panel
elements.
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ