Compatibility
Supported Loaders and Versions
| Minecraft | Loaders | Java | Status |
|---|---|---|---|
| 1.20.1 | Forge 47.4.0, Fabric | 17+ | Fully supported |
| 1.21.1 | NeoForge, Fabric | 21+ | Fully supported |
The API surface is identical on all loaders. Differences are limited to the mod entry point and network registration.
On MC 1.21.1, some internal Minecraft APIs differ from 1.20.1 (e.g., ResourceLocation.fromNamespaceAndPath() vs. the constructor, BuiltInRegistries.ITEM vs. ForgeRegistries.ITEMS). These differences are handled internally — the public API is identical across all loaders and versions.
HUD / Chat Layering
Immersive messages are rendered in a HUD layer intentionally ordered after vanilla chat. This prevents chat background and chat glyphs from hiding immersive overlays when they overlap.
| Loader | Render Hook | Chat Ordering |
|---|---|---|
| Forge 1.20.1 | RegisterGuiOverlaysEvent | registerAbove(VanillaGuiOverlay.CHAT_PANEL, ...) |
| NeoForge 1.21.1 | RegisterGuiLayersEvent | registerAbove(VanillaGuiLayers.CHAT, ...) |
| Fabric 1.20.1 / 1.21.1 | HudRenderCallback | Callback runs after InGameHud chat rendering |
The render path also forces standard HUD state (blend on, depth test off, color reset) and uses a positive GUI Z translate to keep immersive content visually on top without changing layout logic.
Font Renderers
ETA hooks into Minecraft's BakedGlyph rendering at the character level. This means it works with:
- The default Minecraft font renderer
- Custom fonts loaded via resource packs (using the
<font>tag) - ETA's built-in SDF font provider (
emberstextapi:sdf) for TrueType/OpenType fonts - Any font that produces
BakedGlyphobjects through the standard pipeline
It does not work with external text rendering libraries that bypass Minecraft's glyph system entirely.
SDF Font Provider
ETA includes a custom GlyphProvider type (emberstextapi:sdf) that renders vector fonts using signed distance field textures. SDF glyphs are baked into MC's standard FontTexture atlas via SheetGlyphInfo, and a mixin on FontTexture swaps the GlyphRenderTypes to use a custom SDF fragment shader. Because BakedGlyph carries its own GlyphRenderTypes, all existing effects work on SDF glyphs with zero changes to the effects pipeline.
The SDF provider requires LWJGL FreeType. MC 1.21.1 includes FreeType natively. For MC 1.20.1 (both Forge and Fabric), ETA includes a NativeFreeType compatibility layer that calls FreeType functions directly via JNI.invoke*() by address, bypassing the LWJGL 3.3.3 FreeType wrapper class that is incompatible with MC 1.20.1's LWJGL 3.3.1. SDF fonts work on all supported platforms.
MC 1.20.1 Alpha-Preserving Blend (Fringe Fix)
MC 1.20.1's blit-to-screen reads the framebuffer alpha channel. Because MSDF text produces semi-transparent edge fragments for anti-aliasing, those alpha values caused a visible halo (fringe) around SDF-rendered text on all 1.20.1 loaders. MC 1.21.1 does not exhibit this issue because its blit pipeline ignores framebuffer alpha.
The fix uses an alpha-preserving blend function defined in the SDF shader JSON files (rendertype_eta_sdf_text.json and rendertype_eta_sdf_text_see_through.json). The blend section specifies srcalpha=0, dstalpha=1, which tells OpenGL to leave the framebuffer alpha untouched while blending RGB normally. This is defined in the shader JSON rather than in a TransparencyStateShard because MC 1.20.1's ShaderInstance.apply() applies the JSON blend immediately before the draw call — ensuring nothing can override it.
The immersive renderer normalizes very low alpha bytes (0..3) to 0 before glyph draw. This avoids a vanilla font edge case where near-zero alpha can appear as fully opaque for a single frame during fade-in/fade-out.
Other Text Mods
If another mod also hooks into BakedGlyph or StringRenderer, there may be conflicts depending on mixin priority. ETA's mixin only activates its custom path when an EffectSettings object is present — otherwise it falls through to vanilla rendering. This means:
- Text not using ETA effects renders completely normally.
- Conflicts are most likely when two mods both try to modify the same glyph rendering call.
If you encounter a conflict with another mod, report it with the mod combination and mixin configuration. Priority adjustments in the mixin config can usually resolve ordering conflicts.
Emojiful Compatibility
ETA automatically detects Emojiful and enables a compatibility layer. No configuration is needed — effects work alongside emoji sprites transparently.
How It Works
Emojiful replaces Minecraft's font renderer with its own EmojiCharacterRenderer, which bypasses ETA's normal rendering hooks. ETA includes an optional @Pseudo mixin that intercepts Emojiful's character renderer and applies effects to non-emoji characters, while letting Emojiful handle emoji sprite rendering normally.
- Regular characters in styled text get full ETA effects (rainbow, wave, shake, etc.)
- Emoji sprites render normally through Emojiful's pipeline
- Mixed text like
<rainbow>Hello :thinking:</rainbow>— "Hello" gets the rainbow effect, the emoji renders as its sprite
Supported Versions
| Minecraft | Loader | Emojiful Version |
|---|---|---|
| 1.20.1 | Forge | 4.x (EmojiFontRenderer) |
| 1.21.1 | NeoForge | 5.x (EmojiFontHelper) |
| 1.20.1 | Fabric | 4.x (if available) |
| 1.21.1 | Fabric | 5.x (if available) |
Graceful Degradation
Emojiful is not a dependency. The compatibility mixin uses @Pseudo (target class may not exist) and require = 0 (injection is optional). If Emojiful is not installed, the mixin is silently skipped. If a future Emojiful update changes its internals, the mixin simply won't apply — ETA continues to work normally, and effects on emoji-containing text fall back to Emojiful's default rendering.
FTB Quests Integration
ETA includes an optional mixin for FTB Quests (QuestScreenMixin) that enables immersive messages to render in quest UI screens. This mixin is included in the mixin config but gracefully fails if FTB Quests is not present — no errors are thrown.
Performance Considerations
Per-Character Cost
Effects are applied per character per frame:
Cost ≈ (character count) × (effects per character) × (effect complexity)
Most simple effects (wave, shake, rainbow) are very cheap — a few math operations per character. Complex effects (neon, glitch) are more expensive because they create sibling layers.
Sibling Layer Cost
| Effect | Extra Render Passes Per Character |
|---|---|
| Neon (q=1) | ~6 |
| Neon (q=2, default) | ~12 |
| Neon (q=3) | ~20 |
| Glitch (slices=2) | 1 slice layer (+ optional chromatic) |
| Glitch (slices=4) | 3 slice layers |
Recommendation: Use q=1 for neon on messages with many characters. Reserve q=3 for short hero text (titles, single words). Don't stack neon on top of glitch on 50-character strings.
TextLayoutCache
TextLayoutCache caches computed text layouts and is cleared automatically when the GUI scale changes. It significantly reduces redundant layout calculations for messages that don't change between frames.
Integration Tips
Always Send from the Server Thread
EmbersTextAPI.sendMessage(serverPlayer, immersiveMessage);
Never construct or render ImmersiveMessage objects directly on the client side. Rendering is always client-only; sending is always server-only.
Thread Safety
EffectRegistryis thread-safe for reads after initialization.- Effect registration must happen during mod initialization events, not at arbitrary runtime.
ClientMessageManageroperations happen on the main client thread.
Message Lifecycle
- Messages have a finite duration (in ticks). Plan your durations accordingly.
- Use
fadeIn/fadeOutto avoid jarring appearance/disappearance. - For persistent UI elements: use
sendUpdateMessage()to refresh a message before it expires, rather than closing and reopening it.
Avoiding Common Pitfalls
- Don't stack many neon effects. At
q=2, neon on a 50-character message generates 600 extra render passes per frame. - Don't send a message every tick. Use
sendUpdateMessage()with the same UUID to update content without creating new network traffic. - Don't register effects too early. Register during
FMLClientSetupEvent(Forge/NeoForge) oronInitializeClient()(Fabric), not earlier. - Always use
ValidationHelper.clamp()for numeric parameters in custom effects to prevent extreme values. - Don't depend on specific message UUIDs across server restarts — UUIDs are generated fresh each time
sendMessage()is called.