Skip to content

Latest commit

 

History

History
165 lines (127 loc) · 9.28 KB

File metadata and controls

165 lines (127 loc) · 9.28 KB

Architecture

How the pieces fit together, and what breaks if you remove any one of them.

For the evidence behind every claim here — crash logs, measurements, the experiments that distinguished one cause from another — see findings.md.

The problem

Voxy is a level-of-detail renderer. It needs desktop OpenGL 4.6: compute shaders writing indirect draw buffers, glMultiDrawElementsIndirectCount, gl_DrawID, large SSBOs. Android provides OpenGL ES, which has none of that, and Android's EGL will not even hand out a desktop GL context.

So the stack has to supply desktop GL itself, on top of Vulkan, on a mobile GPU — and then survive the places where a desktop-oriented mod meets a phone.

The graphics stack

   Minecraft + Fabric + Sodium + Voxy          Java
 ─────────────────────────────────────────
   LWJGL  ->  libglxshim.so                    GL entry points
 ─────────────────────────────────────────
   Kopper Zink        (libgallium_dri.so)      desktop GL 4.6 -> Vulkan
   libEGL_mesa.so                              Android present path
 ─────────────────────────────────────────
   Turnip             (libvulkan_freedreno.so) Vulkan -> Adreno
 ─────────────────────────────────────────
   KGSL kernel driver                          Android

Zink translates OpenGL to Vulkan; Kopper is the part that gets the result onto an Android surface. Turnip is Mesa's open Vulkan driver for Adreno. Both ship as launcher plugin APKs because Android will not dlopen a library out of an app's writable data directory — it must come from an installed package's nativeLibraryDir.

Why the Zink build is a fork, not upstream Mesa

The renderer plugin is built from the AngelAuraMC / Swung0x48 Mesa fork, branch 25.0-kopper-android, not from upstream. Upstream is missing three things, each fatal:

  1. The Android desktop-GL gate. Upstream eglcurrent.h refuses to create a desktop GL context on Android at all. Without lifting it you get ES 3.1 and Minecraft dies on the first desktop-only enum.
  2. droid_swap_buffers_kopper and the Android Kopper surface. Upstream has only the generic present path. Missing this renders correctly and presents nothing — a black screen with a fully interactive game.
  3. VULKAN_PTR. The launcher loads Turnip through a linker-namespace bypass and passes the loader handle in this environment variable. Without it Zink dlopens the system loader and binds the proprietary driver instead of Turnip.

Porting these to a newer Mesa was attempted and abandoned; the fourth missing piece turned out to be a whole subsystem. Build the fork.

Why Turnip and not Qualcomm's driver

The proprietary Vulkan driver renders correctly, and for a while it was the only thing that did. But its shader compiler (libllvm-qgl.so) segfaults compiling Voxy's fused compute/indirect shader, and Voxy ships only one section-renderer backend, so there is no way around it in config. Turnip's compiler handles the same shader.

That made Turnip mandatory rather than preferable — and its own blocker (a scrambled, tiled image) had to be solved rather than worked around. It turned out to be a build-configuration problem on this side, not an upstream driver bug: with the gralloc metadata backends compiled out, Mesa handed Turnip DRM_FORMAT_MOD_INVALID for the window buffers, so Turnip picked its own tiled layout while the display scanned them out linear.

Package naming

The plugin APKs use in.alexkarl.zalithvoxy.<role>.<name> — the reverse of alexkarl.in, a domain this project controls:

Component Package
Turnip driver in.alexkarl.zalithvoxy.driver.turnip
Kopper Zink renderer in.alexkarl.zalithvoxy.renderer.kopperzink
RocksDB natives in.alexkarl.zalithvoxy.nativeplugin.rocksdb
Zink env (debug aid) in.alexkarl.zalithvoxy.renderer.zinkenv

⚠️ These were originally com.movtery.zalithlauncher.*, copied from the launcher's own plugin examples. That is the launcher author's namespace, and using it implied they published these and risked an ID collision with anything they might ship. Renamed before the first release, which was the last moment it was free: the package name is the plugin ID that goes in version.config, so changing it later would strand every existing install with no upgrade path.

The 26 was dropped from kopperzink26 at the same time — the renderer is built from the Mesa 25.0.7 fork, so the name was simply wrong.

Environment

For a v1 renderer plugin the launcher skips its own Mesa environment block, so the plugin must declare everything itself. This is the whole contract, carried in the plugin manifest's pojavEnv:

Variable Why
LIBGL_ES=3, MESA_LOADER_DRIVER_OVERRIDE=zink, LIB_MESA_NAME=libglxshim.so basic Zink wiring; without LIBGL_ES the launcher derives a nonsense value from the renderer id
MESA_GL_VERSION_OVERRIDE=4.6, MESA_GLSL_VERSION_OVERRIDE=460, allow_higher_compat_version=true advertise desktop GL 4.6
FD_GRALLOC_FORCE_LINEAR=1 stops Turnip guessing the window buffer layout — fixes the tiled image
ZINK_MAX_SSBO_SIZE_MB=128 caps the advertised SSBO size; Voxy otherwise asks for a 4 GiB geometry buffer and fails
ZINK_DEBUG=norp disables renderpass tracking, which crashes under a shaderpack

Two of these — FD_GRALLOC_FORCE_LINEAR and ZINK_MAX_SSBO_SIZE_MB — are not upstream Mesa options. They are added by this project's patches in patches/.

⚠️ POJAV_ZINK_PREFER_SYSTEM_DRIVER has two sources: the plugin manifest and the launcher setting AllSettings.zinkPreferSystemDriver, which is applied separately and works even for v1 plugins. Both must be off or you silently land on the proprietary driver.

Storage

Voxy stores LOD sections through a pluggable backend, chosen per world in saves/<world>/voxy/config.json — and per server in .voxy/saves/<host>/config.json. Voxy writes that file the first time it sees a world, defaulting to CompressionAdaptor(ZSTD) → RocksDB.

On Android, every backend Voxy ships fails:

Backend Why it fails
RocksDB the rocksdbjni jar carries no Android natives
LMDB, ZSTD compressor LWJGL natives built for linux/windows only
Redis needs a server
Memory works, but never evicts — see below

Memory was the obvious stopgap and turned out to be a trap. It retains every ingested section in its own ~64 KiB native allocation. That size sits just past the largest class Android's scudo allocator serves from its primary pool, so each section becomes an individual mmap. At around 29,000 live sections the process exceeds vm.max_map_count (65530) and the next allocation anywhere in the JVM fails — with gigabytes of RAM still free. In practice: a hard crash after one to two minutes of play, immune to heap tuning.

The fix is rocksdb-native-plugin, which supplies the natives RocksDB was missing. Nothing patches Voxy or rocksdbjni to achieve it: RocksDB's own loader tries System.loadLibrary("rocksdbjni") against java.library.path before falling back to extracting from the jar, and Zalith's native plugin API puts a plugin's nativeLibraryDir on that path. So the library simply has to exist in the right kind of package.

The compatibility mod

voxy-android-compat/ is two mixins:

  • ThreadUtilsMixin — Voxy's ThreadUtils.<clinit> loads libc.so.6, the glibc soname, which does not exist on Bionic. Voxy means to tolerate the failure, but catches Exception while LWJGL throws UnsatisfiedLinkError, an Error. A mixin cannot widen a catch — an exception table is structure, not instructions — so the mixin redirects the call and rethrows as an unchecked Exception, letting Voxy's own handler run.
  • VoxyClientInstanceMixin — replaces only the default storage config, so new worlds and servers get RocksDB. Existing configs are read and honoured, deliberately: a config the user chose is never overwritten. That also means worlds created before this mod keep whatever they had and need scripts/fix_voxy_storage.sh.

It is deliberately not built with fabric-loom. Every mixin target is a Voxy or LWJGL class, and those names are stable, so nothing needs remapping and no Minecraft mappings are involved — which sidesteps loom not tracking this Minecraft version. If a future mixin needs a Minecraft class, it has to move to loom.

Shaders

Voxy ships real Iris integration, and it is enabled. But MixinShaderPackSourceNames registers three extra source names that the shaderpack must provide — voxy.json, voxy_opaque.glsl, voxy_translucent.glsl. Without them Voxy has no program to draw its geometry through in Iris's pipeline, so LODs are simply absent while everything else still renders.

Support is per-pack and recent. The log line at world load says which path is in use: NormalRenderPipeline (no Voxy under shaders) or IrisVoxyRenderPipeline.