Merge Iris-on-Metal with upstream master safety fixes - #13
Merged
Conversation
Snapshot of the working tree as extracted from MinecraftMetal(1).zip contents: Metal backend, MetalFX spatial/temporal, MRT backend, frame-generation scaffold (fail-closed). No Iris support yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- iris_metalfx_implementation_audit.md: worktree-verified audit answering the 9 audit questions (buildable w/ JDK25, FFM bridge, MRT E2E receipt, validation harness capability, rollout mining incl. 6th session handoff) - iris-audit/: Blaze3D 26.2 x metallum coverage table, real Iris 1.11.2+26.2 jar call-surface audit (GL renderer, needs Sodium 0.9.1, no SPIR-V), 20-item feature matrix, build/run runbook - iris_on_metal_implementation_plan.md: two-phase plan; integration decision resolved to Form B (Iris semantic layer) with B0-B3 staging Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the mod-private capability layer Iris needs but vanilla Blaze3D 26.2 lacks, across all three layers: - Swift ABI: compute encoder (pipeline/bind/dispatch/indirect/fences), MTLDevice.makeComputePipelineState, blit generateMipmaps, create_sampler_v2 with depth-compare function - FFM bridge: optional downcalls + capability probes (supportsComputeAbi/GenerateMipmaps/SamplerCompare), stale-dylib fail-closed errors - Java: MTLComputeCommandEncoder, MetalComputePipeline (GLSL compute -> shaderc SPIR-V -> SPIRV-Cross MSL with reflected local_size), MetalComputePass (explicit-index binding contract; joins the global MTLFence hazard chain), MetalCommandEncoder.createComputePass/ generateMipmaps + pending-clear pre-flush, MetalGpuSampler compare ctor - Hardening: writeToBuffer/writeToTexture reject heap ByteBuffers on the staging path (previously SIGBUS via Unsafe.copyMemory) Verified: metalComputeBackendIntegrationTest 10/10 GPU readback tests through the production backend (absolute/relative/indirect dispatch, SSBO chains, imageLoad/Store, render<->compute ordering, mip-2 downsample content, shadow-compare semantics) with MTL_DEBUG_LAYER=1 and MTL_SHADER_VALIDATION=1; test+metalMrtBackendIntegrationTest regression green. Suite wired into check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
metalMrtBackendIntegrationTest now also covers: 4 attachments, non-contiguous logical draw-buffer mapping (locations 0/2/5 with inert null slots — Iris DRAWBUFFERS:025 shape), depth+MRT in one pass with depth-content readback (z=0.25 through DepthStencilState), and resize-recreate freshness (no stale storage reuse). 14/14 green under MTL_DEBUG_LAYER=1 + MTL_SHADER_VALIDATION=1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tests - IrisMetalPingPongTargets: main/alt pairs with BufferFlipper semantics (flip/isFlipped/flippedAtLeastOnce/snapshot/restore), feedback-loop guard, resize resets state+storage - IrisMetalRenderTargets: colortex set + depthtex0/1/2 with GPU copy capture points (no-translucents / no-hand) and a DRAWBUFFERS-shaped compact write-descriptor factory over RenderPassDescriptor - IrisMetalShadowTargets: shadowtex0/1 + flip-aware shadowcolor set, pack-config square resize, main-pass state isolation Verified: metalIrisTargetsIntegrationTest 6/6 content-level GPU readback tests (three-pass ping-pong both-sides assertions, snapshot rewind, depth capture trio 0.25/0.5/0.75, shadow depth+color+isolation+resize, flip reset on resize) under MTL_DEBUG_LAYER=1 + MTL_SHADER_VALIDATION=1. Wired into check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Sodium mc26.2-0.9.0 -> 0.9.1 (Iris binary requirement); existing sodium mixins compile+runtime verified; real-client smoke A: Metal backend, ~4min in-world, clean log - Iris 1.11.2+26.2-fabric added to the dev classpath (loads as a mod) - Dormancy shim set (com.metallum.mixin.iris.*, gated on Iris present + default backend, live-backend checked at runtime): cancels Iris's GL entry points (RenderSystem-init chain, GLDebug incl. runtime push/pop/name, IrisRenderSystem.initRenderer/supportsSSBO, IrisSamplers.initRenderer, VanillaRenderingPipeline clip-control, loadShaderpack); GlStateManager._getInteger answers conservative constants during dormant clinit probes; MetalGpuTexture overrides the mixin-injected iris$getGlId with synthetic ids for Iris's per-texture tracking hook - MetalFxManager.reactiveTexture gains RENDER_ATTACHMENT usage (deferred clear materializes it as a color target; Metal validation aborted) - Validation docs: smoke A/B1-B7 evidence, vanilla 26.2 startedCleanly crash-fallback mechanism (forces OpenGL after startup crash), harness discipline; acceptance report: Phase 1 FAIL (8/12 gates), Phase 2 not started per spec ordering Verified: smoke B7 (Metal + Sodium 0.9.1 + Iris dormant) 28s to world, 90s sustained in-world rendering, 0 crash markers; regression green: test + MRT 14/14 + compute 10/10 + iris-targets 6/6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erf audit - MetalIrisShaderCompiler: TransformPatcher output -> std140 loose-uniform wrapping + hostile-identifier rename -> shaderc (auto-bind/auto-locations, 450-core retry lane) -> SPIRV-Cross MSL (production options) -> device MTLLibrary compile - metalIrisShaderTranslationTest: full overworld ProgramSet matrix over real packs; BSL 10.1.3 52/52 + Potato 44/44 stages green incl. shadowcomp compute; report + failure dumps under build/reports/metallum - headless-load shadows (test classpath only): Iris / StandardMacros / IrisRenderSystem; extractIrisNestedJars puts Iris's embedded glsl-transformer/jcpp/antlr on the test runtime classpath - smoke C: BSL installed + enabled in iris.properties; Metal backend joins world in 29s, 90s sustained, 0 crash markers, dormancy holds, sentinels healthy - docs: validation L2 matrix + smoke C, acceptance increments (phase-1 verdict unchanged: fail), runbook fixtures, plan B2 status, new metal_performance_audit.md (fence serialization, blit encoder churn, present-path notes; measure-first plan) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
S1/S2/S3 完成并验证,S5 代码落地未冒烟。判定见 docs/iris-audit/b2-1-design-handoff.md(活文档,S4/S6 有可直接照做的实现规格)。 编译线(已验证): - MetalIrisShaderCompiler 新增 translateSodiumTerrain/linkPatchedPair: TransformPatcher.patchSodium → 松散 uniform 收进 std140 MetallumIrisUniforms 块(两 stage 注入同一份块文本,避免同一 binding 上布局不一致)→ 出 GLSL, 由库存链(vanilla GlslCompiler → IntermediaryShaderModule.rebind → SPIRV-Cross) 接手。std140 偏移表在测试里对着 SPIRV-Cross 反射逐项校验,不靠推断。 - IrisMetalPipelineOverrides:按 IrisPipelines.getPipeline 的字节码判定 sodium 管线的 solid/cutout/translucent,懒构建合成 RenderPipeline(XHFP 顶点格式、 DRAWBUFFERS 决定的 colorTargets、sodium 自身 BindGroupLayout 逐字复制 + 只追加包新增的名字)。全链失败一律 fail-open 回落原生编译。 - MetalDevice 两处 computeIfAbsent 前置查询覆盖注册表。 MetalCrossShaderCompiler:按槽宽重排 varying location(根因修复)。 库存链给 stage 接口变量分配 location 是“一个变量一个槽”,不计类型占用; 光影包的矩阵 varying(Potato 的 out mat2 / flat out mat4x3)按列各占一个槽, 于是后面的变量落进前一个的区间,MSL 出现重复 [[user(locnN)]],MTLLibrary 编译 失败。新增 varyingLocationSpans(SPIRV-Cross 反射算槽宽)+ relocateVertexOutputs /relocateFragmentInputs,两侧按同一起始 location 紧密重排;未连接的 fragment 输入排到 vertex 输出区之后并告警一次。原版路径 varying 全是标量/向量,重排结果 与原编号等价。 唤醒线(编译通过,未做游戏内验证,默认关): - MetalIrisCompat.semanticLayerEnabled() / -Dmetallum.iris.semantic - loadShaderpack 放行;Iris.createPipeline 重定向到 MetalWorldRenderingPipeline (extends VanillaRenderingPipeline,镜像 WorldRenderingSettings 置位含 XHFP 顶点格式;失败回落 VanillaRenderingPipeline,绝不放行 GL 构造器) - StandardMacros 的 GL 面假接成与离线矩阵同款的 pinned GL 4.6 环境 默认关的原因:S4(uniform 供给)与 S6(pass 资源预置)未做,开了会在首次地形 绘制抛 Missing uniform MetallumIrisUniforms。 验证:metalIrisShaderTranslationTest 全绿(B2-2 矩阵 + B2-1 terrain, BSL/Potato × solid/cutout/translucent 6/6 PSO 有效);回归 test / metalMrtBackendIntegrationTest / metalComputeBackendIntegrationTest / metalIrisTargetsIntegrationTest 全绿。 阶段一验收维持不通过:真实渲染验证未做。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recovery point taken before extending the deterministic client validation with core/item acceptance frames. Captures the previously uncommitted MetalEntityObjectPose reconstruction, the core/item motion-pipeline acceptance, the itemMotionDrawsEncoded diagnostic split, the in-progress cutout shimmer remediation, and their unit coverage. No working-tree file contents were altered by taking this snapshot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Captures the interleaved working state of three sessions that were editing this shared checkout simultaneously (Metal 4 migration, Iris adaptation, frame-generation comparison). Serves as the common ancestor for the per-session worktrees created next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
现在 -Dmetallum.iris.semantic 默认为 true(=false 为 kill switch),启用光影包 所需的前置条件已全部就位。 S4 IrisMetalUniformValues:按 S1 算出的 std140 布局逐名填 MetallumIrisUniforms。 矩阵/相机/时间/视口/天气等取真实帧状态(CapturedRenderingState + CelestialUniforms 的公开面),未覆盖的名字置零并每名一次 debug 日志(即扩面工作清单)。GPU buffer 懒 分配(注册发生在 pack 装载期,那时不保证拿得到 device),采样任何一环抛异常则降级为 中性帧并告警一次——每帧都跑的填值不该把客户端弄死。 S6a 资源 fallback:MetalRenderPass.pushDescriptor 在缺名时先问覆盖注册表再抛。 gtexture→sodium 的 u_BlockTex、lightmap→u_LightTex;其余(noisetex/shadowtex*/ gaux*/depthtex*)绑 1×1 占位——sampler2DShadow 必须给深度纹理+compare sampler, 绑彩色纹理是硬校验失败而不是错像素。非覆盖管线一律返回 null,真实的缺绑定照旧抛。 顺带修掉一个“测试全绿但画面全错”的 bug:sodium 的 u_RegionOffset/u_CurrentTime/ u_RegionID 在其 vsh 里是 push_constant 块(#else 分支才是松散 uniform,而 patchSodium 产出走的正是 #else),被我们的 wrapLooseUniforms 折进了包 uniform 块。MetalDrawContext 每 region 写的 20 字节 push_constants 因此永远到不了 shader,所有区块会塌到区域原点。 partitionSodiumPushConstants 把它们摘出来原样重新发射为 push_constant 块,per-draw ABI 与原生 sodium 管线完全一致;类型/顺序对不上直接抛,不让它退化成几何错位。 teardown:注册表关闭时清 MetalDevice 管线缓存(覆盖 PSO 缓存在 sodium 自己的 RenderPipeline 对象上,会活得比 pipeline 长;不清则 reload 后仍用旧 pack 的 PSO)。 离线门新增 verifyUniformSupply:走完 PSO 的整张绑定表,断言每个非 sodium 提供的资源 都能被 fallback 解析(否则就是首次地形绘制必抛),并断言 gbufferModelView 的 16 个 float 确实写进了块的正确偏移。 验证:metalIrisShaderTranslationTest / test / metalMrtBackendIntegrationTest / metalComputeBackendIntegrationTest / metalIrisTargetsIntegrationTest 全绿。 已知边界:DRAWBUFFERS 长度 >1 的 kind 仍走 sodium 原生(BSL solid/cutout 是 [0] 会 生效;BSL translucent 与 Potato 全部不生效),需 S6b 扩展地形 pass 附件。 未验证:任何游戏内运行(S7 冒烟未做)。阶段一验收维持不通过。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
三个真实阻塞,逐条日志栈定位(详见 handoff §6 迭代 4): 1. 放行 loadShaderpack 的注入是空操作 —— 字节码确认它在启动期只有一个调用点: Iris.onRenderSystemInit 的最后一句,而我们对该方法是无条件取消的。改成语义层 开启时由 mixin 自己调 Iris.loadShaderpack()(try/catch:装载失败只让 currentPack 留空,不能把渲染器初始化拖下水)。 2. ShaderPack.<init> → FeatureFlags.isUsable → IrisRenderSystem.supportsImageLoadStore → GL.getCapabilities() 抛 No GLCapabilities。把 supportsImageLoadStore/ supportsBufferBlending/supportsCompute/supportsTesselation 一并假接为 false。 全 false 是故意的:B2-1 只实现 gbuffer terrain,不能让包走 compute/image 分支; 真要求这些特性的包被 Iris 正常拒绝,好过渲染错误。 3. VanillaRenderingPipeline 构造器调用虚方法 shouldDisableDirectionalShading(), 此时子类 programSet 还没赋值 → NPE。覆写加 null 检查(超类构造期给 vanilla 默认值),构造器改用 directives.isOldLighting() 直接算。 真实客户端结果(BSL 10.1.3, enableShaders=true):语义层激活、Profile: HIGH 解析、 Using shaderpack: bsl-shaders.zip、solid/cutout/translucent 三个 kind 全部转译成功、 semantic pipeline generation 1 online;到标题画面 0 崩溃,管线创建后无 ERROR。 注意 in-game 与离线的 drawBuffers 不同:离线是默认 profile,in-game 是 HIGH, translucent 变成 [0,1]。S6b 的必要性取决于 profile,不能只看离线结果。 未验证:没有进世界,地形绘制是否真的命中覆盖、uniform/采样器 fallback 是否真的喂上、 画面是否出现 pack 着色,全部未知。阶段一验收维持不通过。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ne data set archive, residency set
Implements the first two batches of appendix E of
MinecraftMetal_Metal4_Migration_Specs_2026-07-27.md. Every Metal 4 path is a
parallel branch behind a default-off switch; the Metal 3 path is untouched and
is what runs whenever a switch is off, the device or SDK lacks Metal 4, or the
descriptor cannot be translated.
- M1: metallum_metal4_supported folds the compile-time #available and the
run-time supportsFamily(.metal4) into one answer. Switch metallum.opt.metal4.
- M2 step 0 (the hard precondition): new metal4PipelineSmokeTest proves an
MTL4Compiler pipeline state binds to an ordinary Metal 3 render encoder, so
M2 needs no encoder changes and does not have to wait for M7. Green. It also
shows one MTL4 pipeline is valid in passes with and without a depth
attachment, since MTL4RenderPipelineDescriptor has no depth/stencil format.
- M2a: NSMapTable weak-to-strong function -> library side table (MTLFunction
does not expose its library, MTL4LibraryFunctionDescriptor requires it), plus
the lazily built process-wide MTL4Compiler.
- M2b: MTLRenderPipelineDescriptor -> MTL4RenderPipelineDescriptor translation
and a new branch in metallum_MTLDevice_makeRenderPipelineState. Switch
metallum.opt.metal4Compiler.
- M2c archive half: MTL4PipelineDataSetSerializer replaces MTLBinaryArchive on
the Metal 4 path, removing the "an archive loaded from disk can never be
re-serialized" limit that forced S9A into read-only reuse. Java ABI unchanged;
the archive is a sibling file (pso.mtl4archive) so flipping the switch does
not cold-start the other mode's cache.
- M3: MTLResidencySet on the existing Metal 3 queue (macOS 15 / iOS 18, no
Metal 4 needed). Switch metallum.opt.residencySet.
Synchronization layer: no fence or barrier semantics were changed. Nothing in
this commit touches waitForFence/updateFence, the split-fence path, or the
cross-queue event chain. The only submit-path change is a residency commit
immediately before commandBuffer.commit() in metallum_MTLCommandBuffer_commit
and _commitWithSignal, which is a no-op (one nil check) unless
metallum.opt.residencySet is on. M6/M7e are where this line will touch the 34
fence sites, and that will be rebased onto the integration branch first.
Three API truths that only running the code could reveal; all three are written
back into docs/mtl4-api-probe.swift, still typecheck-green on macosx26.0,
macosx14.0 and ios14.0:
- The spec's M2c pairing of .captureDescriptors with
serializeAsArchiveAndFlush(url:) is wrong. configuration is an NS_OPTIONS mask
selecting which serializer method is usable; the archive flush needs
.captureBinaries. The wrong pairing compiles and throws nilError at run time,
so the pipeline cache silently never lands. A typecheck-only probe cannot
catch this class of error, because that file is never executed.
- A synchronous makeRenderPipelineState(descriptor:compilerTaskOptions:)
overload exists, so lookupArchives does not force pipeline creation onto
Swift concurrency.
- Metal returns the same MTLFunction object for an identical library source, so
a "library not registered, must fall back" test that reuses the same MSL still
hits the side table and proves nothing. Distinct sources are required.
Verification, all green: compileJava, compileTestJava, test, buildMacNative,
buildIOSNative, metalMrtSmokeTest, and two new tasks run with
MTL_DEBUG_LAYER=1 - metal4PipelineSmokeTest (API level) and
metal4PipelinePathTest (links the shipping module and drives the real exports:
capability gate agrees with supportsFamily, switch-on pipelines render
pixel-identically to switch-off, an unregistered library falls back cleanly
instead of returning nil, the archive flushes on both a cold and a warm launch,
and the residency set adds, excludes memoryless, and removes correctly).
buildIOSNative passing is what upgrades "no deployment-target bump needed" from
a typecheck claim to a real build result.
One-shot NSLog lines report which path ran ("Metal 4 pipeline path engaged" /
"... unavailable, using Metal 3: <reason>"). Falling back is by design never an
error, so without them a client run cannot distinguish the new path working
from every pipeline silently degrading.
Not implemented, deliberately: the flexible-PSO half of M2c
(metallum.opt.metal4FlexiblePso). The spec's mechanism has no variant matrix to
act on here - the six variants differ only in depth/stencil format, which the
translation already drops - and capturing the saving needs the Java depth
signature collapsed, which is unsafe while the Metal 4 branch can silently fall
back to a depth-less Metal 3 pipeline used in a depth pass. Recorded for
author decision in section 3 of the audit rather than worked around.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
按高频集成协议,在 handoff 加 §4.4。共同祖先 ea2dfd4;本线迄今未动同步层 (fence/barrier/encoder 边界均未碰),唯一与 Metal 4 迁移线重叠的是 MetalRenderPass.pushDescriptor 的资源解析,且是纯加法(非覆盖管线返回 null 走原逻辑)。记忆里 MetalFxManager/Swift 冲突的老风险对本线已不成立——本线没改这些文件。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
缺口 2/3/4 各推进到部分达成:编译链+uniform/采样器供给已落地并有离线 GPU 证据 (6/6 PSO),真机客户端已验证 pack 装载→解析→转译→合成管线上线。但三项都卡在 同一件事——没有进世界,绘制期是否命中覆盖/画面/reload 全部未验证,故计数不变。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 个文件冲突。MetalFxManager/MetalCrossShaderCompiler/MetallumNative.swift 是机械的, MetalDevice 需要把覆盖查询搬进 theirs 的 async prewarm 结构(两处,含后台那条)。 MetalCommandEncoder 是真语义冲突,不自己猜:本线 B0 的单 MTLFence 链 vs S10 的拆分 fence(transferFence/fence 按 MTLRenderStages 收窄),而 compute 编码器在拆分模型下 wait/update 哪条 fence 是 Metal 4 线的设计决定。答错=编译通过、跑得动、同步语义已坏。 本线在同步层的持仓是 compute 编码器 fence 语义,由 metalComputeBackendIntegrationTest 的三条有序性用例守着,合完必须重跑。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… resource The frame graph compiler rejected a write to a Lifetime.EXTERNAL resource from any stage other than FRAGMENT, BLIT or PRESENT. That rule was not grounded in anything: a drawable can legitimately be written by a compute kernel, given shaderWrite usage, and several composition designs do exactly that. The stage check that remains — a pass may only use a resource from a stage the resource's descriptor permits — already covers the real constraint, because a descriptor that omits COMPUTE rejects a compute write on its own. Keeping the rule would have rejected valid pipelines for no benefit. The equivalent restriction was never added to the version pushed to the fork. The frame graph and native ABI layers themselves landed on this branch's history via fbff4d7, which snapshotted three sessions' concurrent work; this is the one correction made after that snapshot. 93 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two follow-ups to the object-motion acceptance frames. Pin the item's spin phase at its capture frame. ItemEntity.bobOffs is randomised per entity and final, so rather than pinning the offset the integer tick base absorbs it: getSpin is ageInTicks/20 + bobOffs, so choosing tickCount at the capture frame fixes the rendered angle there in every run. Two consecutive runs measured horizontal motion spread of 0.023 and 0.028 at frame 164 purely from that phase drift; pinning removes the variance and keeps the capture off the face-on phase, where a block item's visible face shares one depth and the spread carrying the rotation largely collapses. Clear pauseOnLostFocus before the level guard rather than inside applyDeterministicWorldState, which only runs once a level frame has been driven. Under Gradle the window usually opens unfocused and the pause screen opens on the same frame the player joins, before that first level frame; the paused unfocused client is then throttled to effectively zero frames and the timeline never starts. Three runs on 2026-07-27 exited reporting success having captured nothing. This does not fully close the hole — the task still cannot tell "validated" from "never ran" — so the Gradle-side run-state assertion is tracked separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Object motion measures 30-55% off the analytic magnitude (partial-tick and limb-depth broadening, docs/metalfx-frame-generation.md), so a floor picked directly under the observed spread is not safe: the lowest item spread seen across runs (0.023) deflates to 0.0104 at the bottom of that envelope, under the previous 0.012 floor. Lower it to 0.008. This costs nothing in detection power. The regression the floor exists to catch — losing the rotateY term in MetalEntityObjectPose.droppedItem — leaves a translation-only field, which has no horizontal component to spread at all, so it lands near 0.001 rather than merely lower. The floor still sits an order of magnitude above that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
同步层改动:MetalCommandEncoder 的 compute 编码器 fence 归属。按 Metal 4 线给出的 依据(NativeState.transferFence 不变量:split-fence 下唯一在 transfer 链上的 Swift 编码器是 FG 输入 copy blit,其余一律留在传入的 render fence;M6 依赖表「compute 写→ render 读」记的是全量 wait/update 传入 fence),compute 在两种模式下都只 wait/update render fence,不碰 transferFence。这也保证 M7e 迁移时它翻成 dispatch→fragment 的单一 生产者/消费者对,而不是两对。其余同步层代码(split fence 的 render/blit 分派、 waitRenderFences 的 MTLRenderStages 收窄)全部取集成分支侧,本线未改。 逐文件: - MetalCommandEncoder:保留双方(theirs 的 encoderGeneration/waitRenderFences + 本线的 compute 编码器),compute fence 按上述定论 - MetalDevice:取 theirs 的 async prewarm 结构;三条编译路径(render 线程按需/ prewarm 后台/precompile)全部收敛到新的 compileWithIrisOverride 单一漏斗—— 漏掉后台那条会让预热抢先写入原生 PSO,Iris 覆盖静默失效 - MetalCrossShaderCompiler:保留双方(theirs 的 MSL 磁盘缓存 key + 本线的 varying 槽宽重排),vertexAttributeFormats 维持包级可见 - MetalFxManager:取 theirs(代码等价,仅注释与换行不同) - MetallumNative.swift:保留双方的 @_cdecl 段(合并时 HEAD 侧 compare-sampler 函数被截断,已补回闭合括号) 回归全绿:metalComputeBackendIntegrationTest(守 compute fence 语义)/ metalMrtBackendIntegrationTest / metalIrisTargetsIntegrationTest / metalIrisShaderTranslationTest / test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Object motion was produced only for pipelines whose vertex shader is core/entity or core/item. Falling blocks and block entities render through core/block, so they reached the interpolator with no object motion and their movement was left to disocclusion rejection. This is the gap the frame generation doc's Known limits and the attended QA checklist's §3 both name. Verified against the 26.2 sources rather than assumed: RenderPipelines BLOCK_SNIPPET builds SOLID_BLOCK, CUTOUT_BLOCK and TRANSLUCENT_BLOCK on core/block with DefaultVertexFormat.BLOCK; RenderTypes builds SOLID_MOVING_BLOCK, CUTOUT_MOVING_BLOCK and TRANSLUCENT_MOVING_BLOCK on those; and FallingBlockRenderer.submit reaches them via submitMovingBlock. The one thing that separates the family from core/entity is the clip transform. core/entity uses ProjMat * ModelViewMat * Position; core/block uses ProjMat * ModelViewMat * (Position + ModelOffset). Replaying block geometry with the entity shader would produce motion vectors that look plausible and are wrong, which is worse than producing none. ModelOffset lives in the shared DynamicTransforms block that the source pipeline already binds, so the reduced shader reads the same value the color pass used and needs no new uniform. Attribute locations 0/1/2 hold Position, Color and UV0 in both formats, so the explicit-location declaration carries over unchanged; BLOCK's ivec2 UV2 at location 3 is left undeclared because the lightmap is not read. Family is now an enum keyed on the vertex shader path, each member owning its shader and its pipeline location prefix, and build() fails closed for a source no family claims instead of silently using the entity shader. blockMotionDrawsEncoded joins itemMotionDrawsEncoded as a subset counter of motionDrawsEncoded, so a scene with a falling block in view and a zero there localises the failure to this path. Not yet reachable at runtime, deliberately: the sample carrier is attached in ModelFeatureRenderer.prepareModel and ItemFeatureRenderer.prepareSubmit, and moving blocks go through MovingBlockFeatureRenderer instead. Until that hook exists MODEL_BUILD is null for a moving-block draw, shouldSplitEntityDraw declines it, and blockMotionDrawsEncoded stays zero. Hooking MovingBlockFeatureRenderer is the next unit; this commit is the family it will feed. 98 tests pass. MetalMotionFamilyTest asserts each family's shader assets exist, that the prefixes cannot collide, that only the block shader applies ModelOffset, and that both shaders keep explicit attribute locations — the mistake that silently discards every fragment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Third batch of appendix E: the migration's first MTL4 queue. The present thread
is the pilot because no Java ABI crosses it, one command buffer carries at most
an interpolator encode plus a three-vertex copy, the binding surface is one
texture and one sampler, and it touches no MTLFence. Switch
metallum.opt.metal4Present, default off, gated on metal4Compiler because the
MTL4 frame interpolator is built from an MTL4Compiler.
Synchronization layer: the main queue is not touched, and no fence semantics
change. What moves is only the *wait* side of the existing cross-queue
MTLSharedEvent: the main Metal 3 queue keeps calling encodeSignalEvent exactly
as before, while the present queue waits with MTL4CommandQueue.waitForEvent
instead of MTLCommandBuffer.encodeWaitForEvent. Shared events cross the
Metal 3 / Metal 4 boundary, which is what makes this pilot possible in
isolation. Metal 4 fences are same-queue only, so a pilot that used fences
would have collided with the main queue's fence chain immediately - that is why
this path was chosen and not another. Nothing here overlaps the 34 main-queue
fence sites, which stay for M6/M7e.
- New Metal4PresentPath owns only the Metal 4 mechanics: queue, one reusable
command buffer, a two-deep allocator ring (maxOutstandingFrames is 1, and one
allocator would be wrong because reset() reclaims memory the GPU may still be
reading), a 1-texture/1-sampler argument table with initializeBindings, and a
residency set plus the layer's read-only drawable-tracking set.
- present(_:) dispatches two ways; the entire Metal 3 branch below the dispatch
is unmodified. presentMetal4 duplicates the lifecycle, deadline and diagnostic
bookkeeping on purpose rather than factoring it out, so the Metal 3 path stays
as it was.
- installTextureSet is the single choke point every rebuild path funnels
through, so the residency republish hook lives there. A resize additionally
rebuilds the MTL4 interpolator, and clears metal4Path with it on failure so
dispatch reverts to Metal 3 rather than running with a stale interpolator.
- MTL4CommandBufferFeedback has no status, only error, so success is
error == nil. It routes into the existing handlePresentGPUCompletion, which
keeps the failure path that advances readyEvent - without that the present
thread would hang on a stale wait.
- Ordering differences that matter: the event wait is issued only once the frame
is certain to be committed, so a frame dropped by the deadline check does not
leave a wait on the queue timeline; and the reusable command buffer is closed
on every exit path via abandonFrame().
Two API truths found by running the code, both now in docs/mtl4-api-probe.swift:
- MTL4CommandQueue.label is get-only, unlike MTLCommandQueue's. The spec's M4
skeleton line `queue.label = "..."` does not compile; the label has to come
from MTL4CommandQueueDescriptor, and that overload throws.
- A Metal 3 pipeline state binds to an MTL4 render encoder with its texture and
sampler supplied by an argument table. This is the reverse of the M2 question
and had to be checked separately, because the presenter's copy PSO is built by
the ordinary Metal 3 factory. metal4PipelineSmokeTest now covers it, under
MTL_DEBUG_LAYER.
Verification, all green: compileJava, compileTestJava, test, buildMacNative,
buildIOSNative, metalMrtSmokeTest, metalFrameGenerationLifecycleTest (9),
metal4PipelineSmokeTest and metal4PipelinePathTest. The path test now drives the
real Metal4PresentPath headlessly: the whole object graph builds, the MTL4 frame
interpolator factory returns non-nil on this device (had it returned nil the
present path would have fallen back to Metal 3 permanently and silently), the
copy encodes through the shipping encodeCopy, and the four-step
waitForDrawable / commit / signalDrawable / present handshake completes with a
correct pixel readback.
Not yet verified, environment-blocked: the visible-window pacing acceptance
(presentedTime non-zero, deadline misses not increasing, 10 minutes without
deadlock). New task metal4PresentValidation runs the existing presentation
validation harness with the Metal 4 path enabled, but that harness currently
fails identically on the Metal 3 baseline and on the integration branch with no
M4 code at all ("Expected at least 4 generated presentations, found 0"), because
it needs a WindowServer-composited visible window. It has to be re-run from an
interactive session before M4 can be called accepted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. IrisMetalPlaceholderTextures 的深度占位纹理从未初始化(注释宣称的与代码做的相反)。 Metal 新建纹理内容未定义(实践为 0),深度 0 时 LESS_EQUAL 的 sample_compare 对任何 ref > 0 返回 0 —— 全场恒处于阴影,正好是注释宣称的反面。补 clearDepthTexture(1.0)。 离线门只断言 fallback 非 null、不查内容,抓不到这个;不修会污染 S7 冒烟判读。 2. IrisMetalPipelineOverrides.compiledKinds / reportedPlaceholders 的并发安全。 合并集成分支后 MetalDevice 有了后台 prewarm 线程,覆盖可能在渲染线程之外编译, 而绘制期正在读这两个集合。改成 synchronizedMap / ConcurrentHashMap.newKeySet。 这是合并新引入的,不是原有缺陷。 3. handoff 文档三处仍写「默认关」与「为什么默认关」,与 §4 ledger 末条自相矛盾, 已修;并写明 S4/S6a 目前只有编译期证据——真机日志里 compiling terrain override 一行都没有,运行期执行次数为 0。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…me close idempotent Acts on a review of the M4 present pilot. Both changes are on the one path whose failure mode is a permanently wedged present queue, so they are worth taking even though the hazard was already avoided. Synchronization layer: no semantic change. The main queue is still untouched and no fence is involved; this only moves where the existing cross-queue MTLSharedEvent wait is issued from, within the same thread and queue. 1. queue.waitForEvent moves inside submit(), and the standalone waitForReady is gone. Metal 4's queue wait is a queue-timeline operation that takes effect when called, unlike Metal 3's encodeWaitForEvent which is recorded into the command buffer and disappears with an unsubmitted buffer. present(_:) returns from four places after encoding and before commit, one of which — the deadline-miss guard — fires in normal operation. A wait issued before those returns would be an orphan nothing satisfies, and every later commit would queue behind it. The wait was already placed after all four returns, so behaviour is unchanged; what this removes is the ability to put it back in the wrong place, which a separate waitForReady(event:value:) invited. The reason it must sit there is now documented on submit() itself. 2. Frame closing is idempotent via an isRecording flag, so abandonFrame() can be called unconditionally on every exit path (all four already did) without the reusable command buffer being closed twice or closed when never opened. New regression test in metal4PipelinePathTest for exactly the deadlock that placement guards against: encode a frame and abandon it the way the deadline path does, call abandonFrame twice to confirm idempotency, then require a real frame to still encode, submit and complete within 5s. If the wait were issued early this fails by timing out rather than by silently passing. This is the headless half of the "deadline misses repeatedly, is the queue still alive" question that the 10-minute soak is meant to answer. Three review points needed no change, recorded so they are not re-raised: - drawPrimitives on an MTL4 encoder is drawPrimitives(primitiveType:...), not Metal 3's type:. The code already uses the MTL4 spelling. - The Metal 3 copy pipeline on an MTL4 encoder with argument-table bindings was flagged as unverified. It is verified: metal4PipelineSmokeTest gained a case for that direction specifically, under MTL_DEBUG_LAYER, because the M2 step-0 case proves the opposite direction and does not cover it. - NativeState.metal4PresentEnabled, the export, the Bridge downcall and MetalDevice.METAL4_PRESENT are all present and L1 is green. Verification, all green: compileJava, compileTestJava, test, buildMacNative, buildIOSNative, metalMrtSmokeTest, metal4PipelineSmokeTest, metal4PipelinePathTest and metalFrameGenerationLifecycleTest (9). Still environment-blocked, unchanged: the visible-window pacing acceptance (metal4PresentValidation), which needs a WindowServer-composited window and fails identically on the Metal 3 baseline here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fills in the remaining reachable rows of the object-motion coverage table (docs/metalfx-frame-generation.md) so a regression in any single root transform surfaces as its own failing frame rather than going unnoticed: living_turn frames 180-191, capture 188 - pig body yaw, R_y(180 - bodyRot) arrow_turn frames 192-203, capture 200 - R_y(yRot - 90) * R_z(xRot) minecart_rail frames 204-215, capture 212 - rail-sampled pose + hurt shake Each object is held at a fixed world position and only rotated, so per-frame translation is zero; measured object motion runs 30-55% off the analytic value, so a scenario built on large per-frame translation would mostly be measuring that error. The scenario swaps are staging only, fenced by a history reset 8 frames ahead of each capture, and nothing validates a reveal with them. Every angle these three drive is read through a partialTick lerp of old -> new, so pinning old == new leaves them free of any wall-clock term. The minecart is the exception by construction: on a straight rail the renderer re-derives orientation from the front/back rail samples and discards the cart's own yaw, so the hurt shake is the only rotation available without curving the track, and its wobble is asserted on the axis-agnostic spread because it rolls about X rather than yawing. Minecarts under the new behaviour are deliberately absent. That path needs FeatureFlags.MINECART_IMPROVEMENTS, and the validation world enables only minecraft:vanilla, so AbstractMinecart always constructs OldMinecartBehavior here. Reaching it means changing the shared world's enabled features, which would move every existing golden capture. Expected captures 12 -> 15; the timeline now runs past the old 220-frame ceiling, so the timeout moves to 300. Compiles and the 93 unit tests pass, but these three frames have not yet been through a client run - the validation client has been continuously occupied by another session. Thresholds reuse the envelope already calibrated against real item and boat captures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
进世界跑法:./gradlew runClient -Dmetallum.validation.world="New World" 证实(此前全是编译期推断): - compiling terrain override CUTOUT ... → tryCompile 命中,discriminate 判定正确, 合并后的 MetalDevice 编译漏斗正确 - pack sampler 'shadowtex0' ... bound a 1x1 shadow placeholder → S6a fallback 运行期 真的被调到。「S4/S6a 运行期零次执行」这个状态结束。 崩溃:IllegalStateException: MTLRenderCommandEncoder is closed pushDescriptor → setTextureAndSampler,由 sodium 的 drawIndexedIndirect 触发。 根因是设计错误不是笔误:pushDescriptor 在渲染编码器活跃期间做惰性资源分配。 placeholders 的 createTexture/writeToTexture/clearDepthTexture,以及 IrisMetalUniformValues.slice 首次调用的 allocate+upload,都会 endEncoder() 去开 blit 编码器,把 pushDescriptor 正在写的 render 编码器关掉。惰性分配放在绘制路径上, 在这个后端里等于自毁编码器。 修法写进 handoff §6 迭代 5(四步,含一条防回归断言)。核心缺口:目前没有任何途径从 RenderSystem.getDevice()(返回 GpuDevice)拿到 MetalDevice,需要给 MetalDevice 加静态 当前设备引用,才能在 beginLevelRendering(编码器之外)完成预热。 本提交同时含与集成分支 wip/uncommitted-snapshot-2026-07-27 的第二次对齐(M4 present 走 MTL4 队列 + core/block motion family)。逐行确认:那 5 个提交未触碰 MetalRenderPass, pushDescriptor 的 fallback 语义未受影响。回归 metalComputeBackendIntegrationTest / metalIrisShaderTranslationTest 全绿。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The task could report BUILD SUCCESSFUL having asserted nothing. If the client window opens unfocused, the pause screen opens on the same frame the player joins, the compositor throttles the paused unfocused client to roughly zero frames and MetalValidationClient.beforeFrame never reaches a frame with a non-null level. The scripted timeline never starts, no GPU readbacks are captured, no run-state.json is written, and runClient still exits 0. Three runs on 2026-07-27 did exactly that: run/logs/2026-07-27-6.log.gz and -7.log.gz have no "GPU readback" lines yet the build succeeded. c32f04b cleared pauseOnLostFocus before the level guard, which narrows the race but cannot distinguish "validated and passed" from "never ran". Close that by asserting on the artifact instead. finishRunState is the only writer of run-state.json and the client calls minecraft.stop() only on its success path, so an absent file is exactly the vacuous case. runClient gains a doLast, active only under the existing minecraftMetalFxClientValidation start-parameter guard, that requires the file to exist, parse, report status "passed", and have completedGpuCaptures equal expectedGpuCaptures. The expected count is read from the file rather than hard-coded so the gate follows the client: the recorded golden baseline still carries expectedGpuCaptures 10, so a literal 12 would have rejected it. doLast rather than a finalizer because runClient is Loom's RunGameTask with ignoreExitValue false, so the genuine-failure and 220-frame-timeout paths already fail the task on a nonzero exit. The doLast is the net for the exit-0 cases, and also catches an exit-0 run that wrote status "failed". Verified by extracting the doLast body into a standalone Gradle harness and running it against ten cases, two of them real run-state.json files from the tree: missing file, status failed, 8-of-12, the iris 9-of-8 failure, passed-but-short-count, truncated JSON, missing fields, null status, 12/12, and the 10/10 golden baseline. Task-graph attachment confirmed (three actions on RunGameTask); both the guard-active and guard-inactive configuration paths evaluate clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The core/block family existed but nothing reached it. The sample carrier is made current in ModelFeatureRenderer.prepareModel and ItemFeatureRenderer.prepareSubmit, and moving blocks go through MovingBlockFeatureRenderer, so MODEL_BUILD was null for every moving-block draw, shouldSplitEntityDraw declined it and blockMotionDrawsEncoded stayed zero. These are the two hooks that close that. MovingBlockFeatureRenderer.Submit is constructed inside FallingBlockRenderer.submit, which runs between beginEntitySubmission and endEntitySubmission, so the owner is recorded there while it is still on the stack. The build-time bracket needed a different shape from the entity and item families. Those hook a private per-submit method; buildGroup inlines its per-submit work in the loop body, so the submit exists only as a local. Rather than capture locals, which pins the hook to a local variable table, the bracket wraps the one call that emits a whole submit's geometry — ModelBlockRenderer.tesselateBlock — whose level argument is the submit's own MovingBlockRenderState. That is also the key the submit constructor recorded the owner under, so the two ends meet without either needing the submit record. The wrap spans exactly the window in which Group.getVertexBuilder and getOrAddDraw run, which are the points that split a draw out and bind it to the sample. A redirect rather than paired injections, because the sample has to be cleared even if tesselation throws. A HEAD/RETURN pair around a call cannot express that, and a leaked sample would attach this block's motion to whatever geometry is built next — silently wrong motion vectors rather than none. Moving blocks are keyed by render state and the owner is retained rather than consumed: one block model can tesselate into the solid, cutout and translucent render types, and beginFrame clears the map every frame so retaining cannot leak across frames. Verified statically, since this cannot be reached from a unit test: the redirect's descriptor is byte-for-byte the invokevirtual in buildGroup's bytecode and occurs there exactly once, the Submit constructor is (Matrix4fc, MovingBlockRenderState, int) as the injection declares, and both mixins are registered — with defaultRequire 1 a descriptor that stopped matching would fail at class load rather than degrade quietly. Still unproven at runtime: that blockMotionDrawsEncoded goes non-zero with a falling block in view. The automated client validation drives an armour stand, and adding a falling-block scenario means editing MetalValidationClient, which belongs to the cutout-shimmer line. Recorded rather than done. 100 tests pass. MetallumMixinRegistrationTest now guards both directions of the mixin config, the failure this commit would otherwise have been one json line away from: a hook that compiles, ships and does nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Known limits said falling blocks and block entities "would need a second motion pipeline family" and reached the interpolator with no object motion. Half of that is no longer true and the other half was imprecise, and this file is the truth source the attended QA checklist sends a human to before judging the gate — so an overstated gap costs QA time on something already covered, and a vague one hides what is actually missing. Object-motion coverage now states the family concept and both families in a table with their format, clip position and replay shader, and names the two renderers that emit core/block geometry: FallingBlockRenderer, which is covered, and PistonHeadRenderer, which is not. Known limits now says why the piston case is still open, which is not the shader side: the BLOCK family replays it fine, but beginEntitySubmission is called only from EntityRenderDispatcher.submit — verified, it has exactly one caller — and BlockEntityRenderDispatcher is not hooked at all, so no submission window is open when a block entity's submits are constructed. That single missing piece is what blocks every block entity, not just the piston: a chest rendering entity-format models through ModelFeatureRenderer is in a family that can replay it and has no sample to replay it with. It also records why the fix cannot be done from this line alone. The current/previous pair has to come from the manager's own MetalMotionStateStore, which commits only once a frame's output has been encoded; a second store kept outside would commit on frames the manager discarded and then hand out a previous transform that was never presented, which is the failure that store's design comment exists to prevent. So it needs an entry point next to MetalFxManager.captureEntityMotion, in a file the cutout-shimmer line owns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Migrate renderer and MetalFX production paths to Metal 4
21Z121Z1
marked this pull request as ready for review
August 2, 2026 01:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Merge the pushed
21Z121Z1/MetalUniversal:iris-on-metalimplementation withEternityQwQ/MetalUniversal:masterusing Iris as the conflict authority.The merge commit is
14ea4e774fde7ae100118dca470c9ffc4877c4c9with parents695bd7079e783c1433168f916216e56e414771e3(Iris) and3bf3011aedf204c5a57cd14c047e09adea277ccf(upstream master).Conflict resolution
MetalDevicedevice-info initialization so the merged max-allocation check is valid before Iris creates its generic vertex buffer.Verification
Passed on the final source tree and Apple Silicon GPU:
./gradlew test verifyProductionJarIsolationmetalMrtBackendIntegrationTestmetalComputeBackendIntegrationTestmetalIrisTargetsIntegrationTestmetalMrtSmokeTestmetal4PipelineSmokeTestmetal4PipelinePathTestProduction JAR isolation passed. Final artifact identity:
14ea4e774fde7ae100118dca470c9ffc4877c4c9b47dd0d72d219ab2daa1a748741f474aa77843ae9c1592f609d7514c7197167498d20a1b0615e532d5abca7c353a2676545e1ff7aaa2790ec4ae4225bad637682d3647d9dde207f9bf65ab89ced2214266bcbde1The real shader-pack translation gate was run and failed closed because this clean worktree has no non-redistributable
run/shaderpacks/*.zipfixtures. No old JAR, worktree, or evidence was substituted. Therefore this PR does not claim complete Iris conformance.