From a68497089b61487dcb0d92762d0b5e43b09adfa0 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 26 Sep 2026 10:18:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E5=99=A8=E8=81=94=E6=B5=8B?= =?UTF-8?q?=E5=8A=A0=E5=BC=BA=20+=20Sodium=20=E8=AF=8A=E6=96=AD=E6=8E=A2?= =?UTF-8?q?=E9=92=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## tests/test_loader_render.py ★ 划清本文件的**射程**(诚实标注): 它证明的是「Knot 能被我们驱动,且在加载器下渲染能力照常」。 它**不**证明「真实 mod(带 mixin 的)能工作」—— 因为它用空 mods 目录。 实测原因:真实 mod 的 mixin 目标是 intermediary 名,而我们跑 named ⇒ 同一 JVM 只能一个命名空间 ⇒ 真实 mixin mod 在本路线下不生效。 ⇒ 「加载器能起动」与「mod 能用」是**两件事**,别把前者读成后者。 这条注释是被自己坑出来的:早先那份 6/6 全绿的报告, 读起来像"加载器路线可用",实际只验证了"Knot 能起来"。 ## tools/probe_sodium.py 诊断脚本:在装了 Sodium 的实例上跑一次注入 + debug, 打印它自己的状态(chunkStatus / chunkReady / loadQueue / rebuildQueue / visibleChunks / chunksDebug),并出一张图。 ★ 为什么需要它:vanilla 的 `countRenderedChunks()` 在 Sodium 下 **永远返回 0**(它数的是 vanilla 的网格),光看这个数会把 「换了渲染器」误判成「没有数据」。而 blockinfo 能读方块 ⇒ 数据是在的。 ⇒ 排障必须问**渲染器自己**,不能问 vanilla 的计数器。 (debug 命令里的 sodium* 字段就是这条思路的落地: sodiumLoaded / sodiumTracker_chunkStatus / sodiumTracker_chunkReady / sodiumRebuildQueue / sodiumChunksDebug …) --- tests/test_loader_render.py | 16 ++++- tools/probe_sodium.py | 134 ++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 tools/probe_sodium.py diff --git a/tests/test_loader_render.py b/tests/test_loader_render.py index 0ab1383..d3bcc3d 100644 --- a/tests/test_loader_render.py +++ b/tests/test_loader_render.py @@ -7,9 +7,19 @@ 世代(1.20.1 / 1.21.1) × 启动架构(裸启动 / fabric) 本文件专测第二个轴 —— `launcher="fabric"`。 -★ 为什么必须有这个文件 - 上一个会话把 `launcher="fabric"` 写进了后端,却**没有留下任何可重跑的验证** —— - 「经加载器能起来」这件事只活在对话里。这个文件把它变成可复算的。 +★★ 本文件的射程(诚实地划清) + +它证明的是 **「Knot 能被我们驱动,且在加载器下渲染能力照常」**。 +它**不**证明「真实 mod(Iris/Sodium 等带 mixin 的)能工作」—— +因为这里用的是**空 mods 目录**。 + +实测原因(2026-09-26,见 test_mixin_namespace_is_incompatible): + 真实 mod 的 mixin 目标是 **intermediary** 名(`net.minecraft.class_310`), + 而我们的代码跑在 **named** 命名空间(`net.minecraft.client.Minecraft`, + 由 `-Dfabric.gameMappingNamespace=named` 指定)。 + ⇒ 同一 JVM 只能一个命名空间 ⇒ 真实 mixin mod 在本路线下**不会生效**。 + +所以「加载器能起动」与「mod 能用」是**两件事**,别把前者读成后者。 需要(缺任一项则跳过,不误报失败): - MCFOLD_RENDER_CLASSPATH(含 client-named.jar 与编译产物) diff --git a/tools/probe_sodium.py b/tools/probe_sodium.py new file mode 100644 index 0000000..0bc6bf4 --- /dev/null +++ b/tools/probe_sodium.py @@ -0,0 +1,134 @@ +"""探针:Sodium 下区块到底有没有进它的 tracker(diagnose sodium tracker)。 + +用 Python 层的实例 + build.sh 的产物,跑一次 setblocks + debug,读 Sodium 自己的状态。 +用法: python3 tools/probe_sodium.py [mods|empty] +""" +import json +import math +import os +import subprocess +import sys +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from mcfold.render import SECTION_VOLUME # noqa: E402 + +STORE = Path("/home/newqqagent/mcview/store/vanilla-1.20.1") +OUT = STORE / ".mcfold" +INST = Path("/tmp/exp-inst-root/demo-shader") +USE_MODS = (sys.argv[1] if len(sys.argv) > 1 else "mods") == "mods" +MODS = str(INST / "mods") if USE_MODS else "/tmp/empty-mods" +PNG = sys.argv[2] if len(sys.argv) > 2 else "/tmp/sodium-probe.png" + + +def classpath() -> str: + libs = [l.strip() for l in (OUT / "classpath.txt").read_text().splitlines() + if l.strip() and "/org/ow2/asm/" not in l] + fab = sorted(str(p) for p in (OUT / "fabric").glob("*.jar")) + return os.pathsep.join(libs + fab + [ + str(STORE / "versions/1.20.1/1.20.1.jar"), + str(OUT / "classes-intermediary.jar"), + ]) + + +def rle(v): + out = [] + for x in v: + if out and out[-1][0] == x: + out[-1][1] += 1 + else: + out.append([x, 1]) + return out + + +def main() -> int: + Path("/tmp/empty-mods").mkdir(exist_ok=True) + proc = subprocess.Popen( + ["java", "-Xmx2000M", "-Djava.awt.headless=true", + f"-Djava.library.path={OUT / 'natives'}", + f"-Dmcfold.names={OUT / 'names-intermediary.json'}", + f"-Dfabric.modsFolder={MODS}", + "-cp", classpath(), "LoaderBoot", + "--gameDir", str(INST), + "--assets", str(STORE / "assets"), "--assetIndex", "1.20.1"], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + text=True, bufsize=1, + env={**os.environ, "DISPLAY": ":98", "HOME": str(INST)}) + + def rpc(obj): + proc.stdin.write(json.dumps(obj) + "\n") + proc.stdin.flush() + while True: + line = proc.stdout.readline() + if not line: + return None + s = line.strip() + if s.startswith("{") and f'"id":{obj["id"]}' in s: + return json.loads(s) + + t0 = time.time() + while time.time() - t0 < 300: + line = proc.stdout.readline() + if not line: + break + if '"ready"' in line: + break + + try: + cap = rpc({"id": 1, "cmd": "capacity"}) + ids = rpc({"id": 2, "cmd": "resolve", "blocks": [ + "minecraft:stone", "minecraft:grass_block[snowy=false]", + "minecraft:oak_log[axis=y]", "minecraft:oak_leaves[persistent=true]"]})["ids"] + stone, grass, log, leaves = ids + rpc({"id": 3, "cmd": "viewport", "cx": 0, "cz": 0, "radius": 12}) + rpc({"id": 4, "cmd": "view", "hideGui": True}) + + secs = [] + for idx in range(cap["sectionsCount"]): + y0 = idx * 16 - 64 + v = [0] * SECTION_VOLUME + for yy in range(16): + for zz in range(16): + for xx in range(16): + wy = y0 + yy + h = 68 + int(2.5 * math.sin(xx * 0.4) * math.cos(zz * 0.4)) + i = yy * 256 + zz * 16 + xx + if wy < h - 4: + v[i] = stone + elif wy == h - 4: + v[i] = grass + elif wy == h - 3: + if (xx + zz) % 7 == 0: + v[i] = log + elif (xx * zz) % 5 == 0: + v[i] = leaves + secs.append({"index": idx, "rle": rle(v)}) + n = 10 + for cx in range(-3, 4): + for cz in range(-3, 4): + rpc({"id": n, "cmd": "setblocks", "cx": cx, "cz": cz, "sections": secs}) + n += 1 + + dbg = rpc({"id": 89, "cmd": "debug"}) + print(" ── debug ──") + for k, v in (dbg or {}).items(): + if k != "image": + print(f" {k} = {v}") + + rpc({"id": 90, "cmd": "camera", "x": 8.0, "y": 72.0, "z": 8.0, + "yaw": 42.0, "pitch": 30.0}) + res = rpc({"id": 91, "cmd": "render", "frames": 120, "out": PNG}) + print(f" vanilla计数: chunks={res.get('chunksRendered')} " + f"colors={res.get('distinctColors')} → {PNG}") + return 0 + finally: + try: + rpc({"id": 999, "cmd": "shutdown"}) + proc.wait(timeout=20) + except Exception: + proc.kill() + + +if __name__ == "__main__": + sys.exit(main())