OptiFabric 重铸版

OptiFabric 重铸版

OptiFine在Fabric上,同时加载了Fabric API。

OptiFabric —— 在 Fabric 上运行 OptiFine

一个把 OptiFine 带到 Fabric 上的 Fabric 模组。把 OptiFabric 和 你自己的 OptiFine jar 放进 mods/ 即可,其余的交由它处理——无需手动修改 jar,无需运行 OptiFine 安装器,无需 Forge。

ℹ️ OptiFine 不随本模组捆绑或再分发。请从官方网站获取与你的 Minecraft 版本匹配的构建——1.20.6 和 26.1.2 目前仅有预览构建。

OptiFabric 以每条发布线一个项目的方式开发;各 jar 不可互换使用,每个文件名中都带有对应的 Minecraft 版本:

发布线 Minecraft Jar 加载器 Java
1.20.6 1.20.6 OptiFabric-1.0.0+mc1.20.6.jar ≥ 0.19.3 21+
1.21.x 1.21 – 1.21.11(十个发行版) OptiFabric-1.1.2+mc<version>.jar(1.21 / 1.21.1 为 1.1.0) ≥ 0.19.5 21+
26.x 26.1.2 OptiFabric-Reforged-2.0.0+mc26.1.2.jar ≥ 0.19.5 25

为什么需要它

OptiFine 是为原版(以及 Forge)构建的:它的补丁是针对官方混淆名称编译的,而 Fabric 运行在 intermediary 命名空间中。Fabric API 还会注入到许多相同的类中。两者结合在一起时,会以难以诊断的方式产生分歧——不同的构造器形态、不同的合成字段名、被内联掉的辅助方法、对象创建被替换为 OptiFine 自己的子类,等等。

这些分歧并非无关紧要。在重映射器的类路径上没有游戏的情况下进行重映射,会静默地在一个类中留下 35 个未映射的方法,表现为 281 个损坏的抽象契约和 254 个丢失的虚方法重写。OptiFine 的重新编译器会把原版方法变成转发到其自身重载的薄壳——这会把 Fabric API 的注入点从其下方移走,于是 Mixin 在加载时整个类都会失败。OptiFine 还会整体替换类(它的 ChunkOF 区块、它的视频设置界面包括其超类),并重写 Fabric 用于匹配注入点的 lambda 体。

在 26.x 线上,问题的形态变了:Minecraft 26.1 及更新版本是未混淆的——官方名称就是运行时名称,没有 yarn,也没有真正可用于重映射的 intermediary(26.1.2 只发布了 0.0.0 占位符)。因此该线完全不使用映射,运行在 official 命名空间中,并带有自己的模组 id(optifabric_reforged),因为一些模组声明 "breaks": {"optifabric": "*"},而 Fabric Loader 是按 id 进行匹配的——仅更改显示名称是不够的。

它的功能

在启动的最早阶段(加载器的 preLaunch),OptiFabric 会:

  1. 运行 OptiFine 自己的安装器以提取其针对游戏类的补丁(自 1.21.6 起补丁以 xdelta 差分形式传输;optifine.Patcher 的用法相同);
  2. 去除 volde 化处理,并重建目标被 OptiFine 移动的 lambda(LambdaRebuilder);
  3. 将补丁从 official 重映射到 intermediary——重映射器的类路径和输入中都包含游戏 jar,因此从超类型继承的成员会保留其映射后的名称;
  4. 修复 OptiFine 与 Fabric API 之间已知的结构性冲突,每一处都追溯到字节码层面;
  5. 将结果一分为二:OptiFine 自身的类和资源放在游戏类路径上,被修补的 net/minecraft/** 类放入 ClassCache;
  6. 将修复后的类交给 Fabric Loader 自己的 GameTransformer,并将所有内容缓存到 <game dir>/.optifine/<OptiFine version>/ 下,以便后续启动复用而不是重新处理(1–2 秒而非 5–7 秒)。

第 2 步和第 3 步在 26.x 线上被跳过,这正是它作为独立项目存在的原因。

替换发生在 Mixin 之前:当某个 Minecraft 类即将被加载时,Loader 会向游戏提供者的 GameTransformer.transform(...) 请求现成的字节码。因此 OptiFabric 交出的是 Mixin 的输入,而非其输出——这就是其他模组针对相同类的 mixin 仍能正常工作的原因。由此产生一个硬性约束:在类被交出之前,任何代码都不得对游戏类进行反射——一次 Class.getMethods() 就会加载那些方法签名中的所有类型,并在本次会话剩余时间内将类固定为原版。

缓存目录包含 cache-format.txt(当前为 26;不匹配则重建全部)、Optifine-mapped.jar(不含 MC 类的 OptiFine,即放到类路径上的内容)以及 Optifine.classes.gz(被修补的 MC 类)。

安装

  1. 为你的 Minecraft 版本安装一个带有 Fabric Loader 的客户端(1.20.6 需 ≥ 0.19.3,1.21.x 和 26.x 需 ≥ 0.19.5),运行在 Java 21+ 上——或者对于 26.1.2 使用 Java 25,这是游戏本身的要求。
  2. 将 OptiFabric 和你自有的、对应确切版本的 OptiFine jar 放入该实例的 mods/ 文件夹。直接放入 OptiFine 安装器 jar 即可——你不需要先运行它的安装器(已提取、带有 notch/ 类的 OptiFine 同样可用)。不要安装两个 OptiFine jar(游戏会报告 DUPLICATED),也不要混用来自两条线的 jar。
  3. 使用 Fabric 配置文件启动游戏——而不是启动器生成的 1.21.x-OptiFine_xxx 配置文件,后者会自行注入 OptiFine。成功时 OptiFine 版本会显示在标题界面左上角,其选项会出现在视频设置中。

Fabric API 可以同时加载(本移植版专门为其做了适配)。启用版本隔离时(PCL2 / HMCL),游戏目录和 mods/ 位于 versions/<name>/ 下,.optifine/ 缓存也会创建在那里。

要求

1.20.6 1.21.x 26.x
Minecraft 1.20.6 1.21 – 1.21.11(每个版本一个 jar) 26.1.2
Fabric Loader 0.19.3 或更新 0.19.5 或更新 0.19.5 或更新
Java 21+(在 25 上测试) 21+(在 25 上测试) 25
端 客户端 客户端 客户端
OptiFine preview_OptiFine_1.20.6_HD_U_I9_pre1.jar(仅预览) 你自有的构建,需版本完全匹配 preview_OptiFine_26.1.2_HD_U_K1_pre2(仅预览)
可选 Fabric API(支持) Fabric API(支持) Fabric API(已在 0.155.3+26.1.2 上测试)

已验证的内容

每个发行版都通过一条命令、在同一个加载器中进行检查,方式与游戏加载这些类相同:JVM 验证器加上一个 ASM 数据流验证器,覆盖每个被修补的类和每个 OptiFine 类,随后是五个扫描器(mixin 成员引用、@At 注入点、抽象契约 / 丢失的虚方法重写 / 无法解析的引用、invokedynamic 句柄、局部变量捕获)。

Minecraft 被修补的游戏类(JVM) OptiFine 类(JVM) ASM 验证器 扫描器
1.20.6 425 / 425 — 0 问题 干净
1.21.3 / 1.21.4 440 / 440 · 474 / 474 816 / 816 · 812 / 812 0 仅有被禁用的 Indigo @At 未命中
1.21.6 / 1.21.7 / 1.21.8 487 / 487 · 500 / 500 · 516 / 516 820 / 820 · 823 / 823 · 831 / 831 0 同上
1.21.9 / 1.21.10 / 1.21.11 519 / 519 · 553 / 553 · 570 / 570 832 / 832 · 836 / 836 · 874 / 874 0 同上
26.1.2 567 / 567 879 / 879 0 所有列均为 0(该线未声明 contains_renderer,因此 Indigo 是活跃的)

游戏内验证:启动、标题界面、单人游戏、多人服务器、方块/区块/物品和实体渲染、着色器(已用 ComplementaryReimagined 验证)、在 1.21.3 – 1.21.11 上的抗锯齿、F3 调试界面,均为无 [ERROR] 会话且无崩溃报告。在 26.1.2 上,Indigo 注册自己的渲染器,而按方块位置生成几何体的模组(LambdaBetterGrass 的优化草、连接纹理)在开着色器的情况下也能正确渲染。

已修复的兼容性问题

以下问题均通过真实崩溃发现,并追溯到字节码层面:

  • Fabric API 的 ShaderProgramMixin 在 super() 之前注入 → OptiFine 的委托构造器被重写,内联的标识符创建现在会复制游戏自身的 Identifier.ofVanilla(...) 调用(包装器正是针对该调用,而 new Identifier(...) 会绕过它);
  • OptiFine 留下混淆、描述符不匹配的字段(例如粒子工厂表)→ 按名称、类型和存储值重新对齐,并沿继承层次查找重写;
  • OptiFine 在重新编译时内联掉的私有辅助方法 → 恢复原版方法体,使注入重新有目标;
  • OptiFine 转为其自身重载转发器的方法(区块构建、模型烘焙、方块轮廓、物品模型)→ 恢复原版方法体——这正是“所有模型烘焙失败”和方块隐形的背后原因;
  • OptiFine 重定向到其自身子类的对象创建(ChunkOF 区块对象)→ 一个惰性标记把 Fabric 的 NEW 注入点放回去;
  • 一个移植的修复器连同它替换的构造器一起丢弃了字段初始化 → 该构造器不再被替换(此问题曾在两秒后断开多人会话);
  • 在不同名称和签名下重新编译的 lambda 体(lambda$addMainPass$1 多了一个参数)→ 恢复原版方法体;当 OptiFine 用自己的 lambda 替换方法引用时,LambdaMethodRefFix 将它们重命名回方法名,因为该发行版的 Fabric API 是按 bootstrap 句柄匹配自定义注入点的;
  • 一个从不存储区段位置的区域构造器(ChunkCacheOF.renderStart() 随后在空区域内 NPE)→ 修复器调用六参数构造器,并从该方法已有的打包 long 推导出参数——1.21 – 1.21.4 的形态则接收 ChunkSectionPos 对象,两种形态都已处理;
  • 一个 Fabric 钩子,其上下文从未被 OptiFine 的渲染通道结构填充(BEFORE_BLOCK_OUTLINE)→ StubInjectionTargetFix 重命名目标并保留一个同名副本,使该钩子注入到无人调用的代码中:崩溃消失,方块轮廓仍由 OptiFine 绘制;
  • 同样的技巧在移动方块渲染器钩子上失败,其调用者位于另一个类中并触及了被注入的副本 → CallSiteRedirectFix 也重定向该调用点(若无此项,多人客户端会在约 30 秒后崩溃);
  • 在没有注册任何渲染器的情况下 F3 崩溃——声明 contains_renderer 只会让 Indigo 让路;Fabric 自己的调试条目仍会调用 Renderer.get() → RendererApiFallback 注册一个惰性占位渲染器;
  • 占位渲染器的第一个版本破坏了整个会话——它用 Class.getMethods() 读取 Fabric API 接口,这会在被修补的类交出之前加载游戏类,将它们永远固定为原版 → 现在该占位符仅根据接口的类文件用 ASM 生成(不做类型解析),通过 MethodHandles.findStatic 注册,且仅在类就位之后;在 26.1.2 上,渲染器 API 也已迁移到 api.client.renderer.v1,旧查找在那里会静默失败;
  • 没有修复器修改的类不再重新计算其栈帧——全局重写修复器始终报告有变更,因此对每个被修补的类重新计算会把合并的局部类型退化为 java.lang.Object,游戏以 VerifyError: Bad type on operand stack 拒绝该类;
  • 26.x:OptiFine 的薄壳留下两个同名方法,而 Fabric API 命名其目标时不带描述符 → 每个位置都被消歧(删除无人调用的重载;用 CallSiteRedirectFix 重命名仍被调用的那个并跟踪其调用者)。搞错这一点会表现为 LVTGeneratorError: Could not locate method metadata … 或 Scanned 0 target(s);
  • 26.x:自行发出四边形的模型静默消失——Fabric 的地形钩子注入到原版区块构建的 BlockPos.betweenClosed 循环中,而 OptiFine 自己的编译重载没有这样的循环,因此钩子无处运行。FrapiTesselateBridgeFix + OptifineFrapiBridge 将该次曲面细分调用路由通过 Fabric 的渲染器,并把四边形交给 OptiFine 自己的 BlockQuadOutput,因此顶点格式、层、光照和着色器属性都保持为 OptiFine 的;只有 emitQuads 声明在 net.minecraft. 之外的模型才会被路由(原版模型留在 OptiFine 的路径上,否则着色器会丢失 OptiFine 的额外顶点属性);
  • 抗锯齿:FXAA 链由游戏的后处理链加载器从 post_effect/ 解析,因此本模组早期一个“好心”删除该文件的构建会让每次资源重载都记录 Resource not found: minecraft:post_effect/fxaa_of_2x.json,且每次抗锯齿切换都以 Failed to load post chain 失败。现在 OptiFine 自己的链文件按原样保留发布状态,并且在 1.21.9 / 1.21.10 上——其后处理管线从 gl_VertexID 绘制无属性的全屏三角形,而 OptiFine 的 fxaa_of_*.vsh 仍读取 Position 属性(这就是“抗锯齿把屏幕变黑”的 bug)——两个顶点着色器已相应地重写。

完整列表(症状 / 原因 / 修复)见变更日志,每轮的证据见 docs/DEVELOPMENT.md(未混淆线见 docs/PORT_26.x.md)。

已知问题

  • 与 Sodium 冲突——两者都是渲染器;不要同时安装。
  • 与 RyoamicLights 不兼容——OptiFine 会替换整个视频设置界面(包括其超类),这会使该模组的注入失败,并在界面一打开时崩溃。OptiFine 有内置动态光源(视频设置 → 品质 → 动态光源),因此并不需要它。
  • 依赖 FRAPI/indigo 的模组在 1.20.6 和 1.21.x 上不再获得 indigo 的自定义渲染;地形由 OptiFine 渲染,Renderer.get() 返回一个惰性占位符(F3 显示 OptifineRendererPlaceholder)。在 26.1.2 上情况相反——Indigo 注册自己的渲染器,这些几何体会被渲染。
  • 两个 Fabric API 钩子被有意设为惰性(BEFORE_BLOCK_OUTLINE 事件不会触发;移动方块 FRAPI 路径被绕过)。这两条路径仍能通过原版/OptiFine 正确渲染。
  • 1.21.6 / 1.21.7 一启用着色器就崩溃(ShadersTex.initDynamicTextureNS 中的 NullPointerException … "multiTex" is null)。这不是着色器包的问题(三个不相关的包崩溃表现一致,且剥离包的自定义纹理也无济于事),也不是本模组的问题:这两个发行版的 OptiFine 预览构建在第一次纹理创建时注入了一个调用,却没有后续构建所执行的 setParentTexture 关联。为它们提供的全部七个 OptiFine 构建都以相同方式崩溃。不开着色器时,两个发行版均可启动和游玩。
  • 26.1.2 需要 Java 25——在 Java 21 上启动会在窗口出现前失败。这是游戏本身的要求,不是本模组的。
  • OptiFine 无法看到 Fabric 模组内部的资源——你会在日志中看到 Unknown resource pack type: ...ModNioResourcePack。这是 OptiFine 侧的限制。
  • 与你的 OptiFine 版本不匹配的着色器包会记录 [Shaders] Invalid program name: ...、Unknown macro value: IRIS_VERSION 或 ParseException: Model variable not found: …;这些来自着色器包。
  • 仅覆盖上述发行版——其他 Minecraft 版本需要各自单独适配,而 OptiFine 尚未为 1.21.2 / 1.21.5 或 26.1.2 之后的任何版本发布构建。

故障排除

缓存在哪里 / 如何强制重建? <game dir>/.optifine/<OptiFine version>/。删除 .optifine/ 文件夹即可强制重建——缓存格式(26)通常会在升级后自行重建。

为什么日志里找不到 [OptiFabric]? 它的输出发往启动器控制台,而非 logs/latest.log;加载器只把 log4j 输出写到那里。过滤 [OptiFabric] 可以看到准备了多少个类、Loader 接管了多少个。

找不到游戏 jar / 想手动指定路径——添加 -Doptifabric.mc-jar=<path to the vanilla client jar>。

想检查被修补的类——添加 -Doptifabric.extract=true;重映射后的 OptiFine 类会被解包到 .optifine/<version>/optifine-classes/。

卡在加载界面——采集两份线程转储(jstack <pid>,间隔约 15 秒)并对比。堆栈相同且 CPU 平坦意味着真正的停滞;堆栈停留在原生调用(glfwSwapBuffers)中是呈现问题,并且加载时不要最小化窗口(开启垂直同步时渲染线程会在那里阻塞)。

模型/物品/纹理大批消失(日志中满是 Unable to bake … model)——某个 Fabric mixin 未能转换该类;最外层的消息通常会掩盖真正的原因。OptiFine 把原版方法变成转发器是注入点移动的常见原因。

正常且可忽略的警告——[OptiFine] (Reflector) Class not present: net.minecraftforge.* / sun.misc.SharedSecrets(OptiFine 探测 Forge 和旧 JDK)、Failed to locate initialiser injection point in <init>(class_2591,…)(应用 OptiFine 的 BlockEntity 补丁的代价)、[OptiFabric] Resource not found: minecraft:shaders/post/fxaa_of_{2,4}x.json(OptiFine 探测其 1.21.6 之前的链位置;实际使用的链位于 post_effect/),以及 Skipping bad option: lastServer。

报告问题

请附上:

  • logs/latest.log(如果崩溃还请附上 crash-reports/ 中对应的文件——它以 -- OptiFabric -- 段结尾,列出 OptiFine 版本、jar 状态和重映射后的 jar 路径);
  • 你的 mods/ 文件夹列表;
  • 你的 OptiFine 版本(例如 OptiFine_1.21.11_HD_U_J9)以及你运行的 Minecraft 版本。

许可与致谢

由 Modmuss50 和 Chocohead 移植的 Chocohead/OptiFabric,采用 MPL-2.0 许可;移植文件保留其原始头信息。OptiFine 本身既未包含也未再分发——它是 sp614x 的作品,请从官方网站获取。这是一个社区移植版,与 OptiFine、Fabric 或 Mojang 团队无关联、未获其背书或支持。