Files
agentmc/tools/probe_sodium.py
JianFeeeee a68497089b 加载器联测加强 + Sodium 诊断探针
## 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 …)
2026-09-26 10:18:36 +08:00

135 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""探针: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())