Skip to content

使用 AI Agent 适配了 Minecraft 1.21.11 的 Metal 渲染后端 - #14

Open
PigeonCoders wants to merge 40 commits into
EternityQwQ:masterfrom
PigeonCoders:mc/1.21.11
Open

使用 AI Agent 适配了 Minecraft 1.21.11 的 Metal 渲染后端#14
PigeonCoders wants to merge 40 commits into
EternityQwQ:masterfrom
PigeonCoders:mc/1.21.11

Conversation

@PigeonCoders

Copy link
Copy Markdown

我全程使用 OpenCode + DeepSeek-V4-Flash-0731 的开发方式为 MetalUniversal 添加了 Minecraft 1.21.11 (Fabric) 支持

我做了什么

  • 将Metal渲染后端适配到了 MC 1.21.11 的绘图接口
  • 经过了部分的单设备测试

测试情况

  • 使用 iPad Air 3(M3) 进行测试,启动器使用catsruledogs/Amethyst-iOS-25的 Release v1.1.0-experimenta l发布版本+ Java 25 测试通过Minecraft 的 GUI 显示/区块渲染

注意事项

  • 由于Sodium v0.8.x 没有开放的渲染端口供 Metal 使用,即该版本与 Sodium 不兼容
  • 该分支存在未验证的问题,需要更加完备的测试

希望您有空看一下,请您不要直接合并该请求,请求您新建分支并合并该请求

PigeonCoders and others added 30 commits August 2, 2026 01:10
- Retarget gradle.properties: MC 1.21.11 / loader 0.19.3 / loom 1.14.10 / Sodium mc1.21.11-0.8.13
- Adopt buildscript classpath for loom (stable-version plugin markers unpublished), Gradle wrapper 9.5.1
- Add explicit mojang mappings, modImplementation, lwjgl-vulkan/shaderc/spvc 3.3.3 + mixin compile deps
- Rewrite device/encoder/renderpass against 1.21.11 interfaces (GpuDevice/CommandEncoder/RenderPass)
- Self-host GLSL->SPIR-V->MSL pipeline (ShaderSource interface + LWJGL shaderc/spvc)
- Add TransientBlockAllocator/ShaderCompileException replacements for removed MC classes
- Replace PreferredGraphicsApi/Sodium mixins with RenderSystemDeviceMixin (@reDIrect GlDevice ctor)
- Add metallum_NSWindow_contentView bridge export (GLFW 3.5 glfwGetCocoaView missing in 3.3.3)
- Sodium 0.8.x: no backend abstraction, mutually exclusive with Metal; startup warning
- mod_version 1.1.0
… inject

Mixin does not support redirecting constructors (InvalidInjectionException:
Illegal @reDIrect of constructor). Switch to @Inject at HEAD with
cancellable, manually replicating skipped initRenderer side effects
(apiDescription, dynamicUniforms, samplerCache.initialize) via @shadow.
…cations

1.21.11 GLSL lacks explicit layout(binding/location) decorators (injected
by the GL backend at compile time); shaderc rejects them in strict mode.
Match 26.2 GlslCompiler options: auto_bind_uniforms + auto_map_locations.
Existing spvc rebindResourceType renumbers bindings per BindingPlan.
…cated blocks

A 1MB texture upload (mojangstudios.png 512x512 RGBA) exceeded the 512KB
BLOCK_SIZE, overflowing the cursor and producing an out-of-range slice
(newLimit > capacity crash in MetalGpuBuffer.sliceStorage). Match MC's
original TransientBlockAllocator semantics: oversized requests allocate a
dedicated block (offset 0) released on the next rotate().
Metal presents via CommandEncoder.presentTexture; the leftover GL swap in
flipFrame crashes mobileglues (SIGSEGV in gl_swap_buffers). Redirect the
GLFW call to a no-op, keeping pollEvents/callback/dynamicUniforms logic.
…cting flipFrame

The previous @reDIrect on RenderSystem.flipFrame never took effect: mixin 0.8.7
requires the redirect handler to cover ALL target method parameters (fyk/fwf are
unmapped types, impossible to declare). hs_err confirmed flipFrame still called
glfwSwapBuffers -> SIGSEGV in mobileglues gl_swap_buffers.

Replace with a direct @Inject HEAD cancellable on GLFW.glfwSwapBuffers itself,
whose (long) signature is known and whose class is loaded by Knot only at window
creation (mixin system is ready at preLaunch, so the transform applies in time).
…egion upload)

rowStart wrongly used (sourceY + row) * rowBytes where rowBytes is the
destination region stride (width*4) instead of the source image stride
(imageWidth*4). Atlas sub-region uploads (sourceY > 0 or imageWidth >
width) exceeded the region buffer capacity, crashing with
IndexOutOfBoundsException in DirectByteBuffer.put.

Fix: target rows are laid out from 0 in the region buffer; source
coordinates are only used for getPixel sampling.
…t() returns null

The ShaderSource passed to initRenderer is a capturing lambda from the
Minecraft ctor whose get() returns null for pipelines that were never
precompiled (e.g. gui), causing 'Couldn't find shader source' when the
pipeline cache misses. Fall back to reading the GLSL directly from
assets/minecraft/shaders/<path>.vsh/.fsh (1.21.11 core shader layout).
Uses Identifier.parse (no two-arg of() factory in 1.21.11).
1.21.11 shaders are GL dialect (gl_VertexID etc); the Vulkan target env
rejects them ('gl_VertexID undeclared, did you mean gl_VertexIndex?').
Switch shaderc to GL target (opengl/4.5, only env constant in LWJGL 3.3.3);
SPIR-V output is dialect-neutral so spvc->MSL is unaffected.

Also expand #moj_import directives when reading raw shader files
(Minecraft's ShaderSource.get normally does this internally): resolve
ns:shaders/include/<path>.glsl, strip the include's own #version line,
inline recursively with cycle protection.
…r type

terrain.fsh declares sampleNearest(sampler2D sampler, ...); SPIRV-Cross keeps
the GLSL parameter name, producing MSL 'texture2d<float> sampler' which shadows
the built-in sampler type and breaks MTL compilation (failed 5 terrain
pipelines -> 'Failed to load required shader programs').

Sanitize generated MSL: rename only the texture2d<...> sampler parameter form
to samplerTex (type-name occurrences are untouched by the anchored pattern).
sanitizeMsl only renamed the texture2d<...> sampler parameter declaration;
the sampleNearest body still referenced 'sampler.sample(...)' which then
resolves to the MSL type name ('sampler' does not refer to a value).

Rename at the GLSL layer instead: \bsampler\b -> samplerTex in
prepareShaderSource (after comment stripping and define injection). GLSL has
no 'sampler' keyword (\b never matches sampler2D/Cube; Sampler0 unaffected),
so declaration and all references stay consistent through SPIR-V -> MSL.
sanitizeMsl is kept as a defensive fallback.
…n launch

- build.gradle: write Implementation-Build manifest attr (GITHUB_REF_NAME tag
  in CI, 'dev' locally) so fix iterations are distinguishable
- MetallumSelfUpdater: daemon thread on onInitialize; reads own manifest build
  tag + GH_TOKEN env var; queries artifacts API (per_page=5, skip expired),
  compares workflow_run.head_branch (tag) with local build tag; on mismatch
  downloads artifact zip, extracts the non-sources metallum-*.jar, size-checks
  against API size_in_bytes, deletes old metallum-*.jar in mods dir and lands
  the new jar (restart to apply). All failures silent (debug log), never
  blocks the render thread. No token or dev build -> skip.
- Works with the existing v*-tag CI flow; no Release or public URL needed.
…ocations

- shaderc auto_map_locations remaps vertex/fragment locations independently,
  causing fragment inputs (user(locn0)) to mismatch vertex outputs and fail
  MTLRenderPipelineState creation ('Fragment input(s) mismatching vertex
  shader output type(s) or not written by vertex shader', e.g. animate_sprite_blit)
- align with 26.2's IntermediaryShaderModule.rebind(): rebindStageOutputs
  renumbers stage outputs 0..n-1 (skip gl_* builtins), rebindStageInputs
  matches inputs by name against vertex outputs (extra inputs appended),
  vertex inputs are numbered per vertexAttributeNames order matching
  MTLVertexDescriptor attribute indices
- registerIntegerInputConversions now runs after location rebind
…c/uvec inputs

- 1.21.11 rendertype_text.vsh declares ivec2 UV2 (int) while the
  VertexFormat element is SHORT: GL allows the mixed usage (driver converts),
  Metal rejects it ('Cannot convert attribute from MTLAttributeFormatShort2Normalized
  to int2 or uint2')
- collect SPIR-V stage inputs whose base type is int/uint in the cross shader
  compiler and pass to the compiled pipeline; buildVertexDescriptor then uses
  non-normalized MTLVertexFormat (fromInteger) for those attributes
- self-updater: also check for updates when the build tag is null (legacy jars
  without Implementation-Build manifest could never self-update)
- delete MetallumSelfUpdater.java
- drop startIfNeeded() call in onInitialize; Sodium warning kept
- Implementation-Build manifest attr kept as build metadata
… upload byte order

- Java [diag] logs at ERROR level (log4j config fails to load on Amethyst,
  only ERROR reaches console): createTexture usage/format/label,
  createRenderPass target info, presentTexture source info, bindPipeline
  location - next iOS run will show whether the main framebuffer is ever
  written and whether present source matches the render target
- writeToTexture(NativeImage): getPixel returns ARGB (javap-verified),
  previous ABGR byte order swapped R/B
- Swift: NSLog when present sampler creation fails
- fix-diag logged createRenderPass/presentTexture on every frame (error level),
  flooding the iOS console and freezing the game
- new Diagnostics utility: ConcurrentHashMap-based per-key dedup, each texture/
  pass/present source logged at most once; toggleable via -Dmetallum.diag=false
- keys: tex:<label>, pass:<label|format>, present:<label>, pipe:<pipeline|useDepth>
- Diagnostics.shouldRun(key, intervalMs): time-window throttling for sampled diagnostics
- MetalCommandEncoder.diagReadback: copy 4x4 region of a texture back to CPU,
  log average RGBA + first pixel (every 3s); called from presentTexture (main-target)
  and writeToTexture NativeImage path (upload) to tell 'explicitly magenta' vs
  'never written / texture upload dead' apart
- MetalRenderPass.countDraw: cumulative draw counter logged every 5s to verify
  draws are actually encoded
Root cause (verified in code): createRenderPass dropped the pending color
clear recorded by clearColorTexture() — MC 1.21.11 clears the main target via
the pending path (clearColor param is empty, see diag log 'clear=false'),
but the color branch removed pendingColorClears without using it, unlike the
depth branch which carries pendingDepth into effectiveDepthClear.

Consequence: main target color never cleared → uninitialized MTLTexture →
iOS shows solid magenta (readback verified avg=255,0,255 constant), and with
uncleared depth most draws fail, keeping the screen magenta.

Fix: assign colorClear = pendingColor in the full-view + empty-param branch,
symmetric with the depth branch. flushPendingClear (partial region) and
param-priority paths unchanged.
- local spvc probe proved binding numbering is correct (DynamicTransforms->buffer(0),
  Projection->buffer(1), Sampler0->texture(0), stage_in locn alignment OK)
- remaining suspects: uniform buffer contents (matrices -> vertex clip / GUI discard),
  vertex data, runtime texture/sampler handles
- v4 diag (once per target): pushVertexBuffers (slot/handle/offset/closed/storage first16),
  pushDescriptor SAMPLED_IMAGE (handles), UNIFORM_BUFFER (storage first64 hex),
  drawIndexedNative (5s throttle), MSL dump head (binding lines)
- 1.21.11 RenderPass.drawIndexed(int,int,int,int) is (baseVertex, indexBufferOffset,
  indexCount, instanceCount), not (indexCount, instanceCount, firstIndex, vertexOffset)
  as previously assumed from MCP param names; verified by disassembling
  GlCommandEncoder's GL calls (glDrawElementsBaseVertex(basevertex=p2, count=p4))
- drawIndexed was drawing 0 indices every frame (MC passes (0,0,30,1) -> old code
  read indexCount=0), leaving the main target with only the clear color
- RenderPass.draw(int,int) is (firstVertex, vertexCount), not (vertexCount, instanceCount)
- drawMultipleIndexed unchanged (already correct: Draw.firstIndex=index offset,
  Draw.indexCount=index count, baseVertex=0, instanceCount=1)
- GLFWTerminateMixin: no-op glfwTerminate on Metal hosts (MobileGlues
  gl_terminate SIGSEGV when GL context never made current, hs_err confirmed)
- bindPipeline diag: + cullMode/winding/depthCompareOp/depthWrite
  (MetalCompiledRenderPipeline exposes depthCompareOp()/depthWrite())
- createRenderPass diag (3s throttle): pendingColor/pendingDepth handoff
  state - distinguishes depth-clear failure (blocks invisible) from other causes
- drawMultipleIndexed diag (5s): draw count + first draw firstIndex/indexCount/slot
- bindPipeline once key now includes depthOp/depthWrite/cull so the world
  pass depth state is visible (GUI pass used to occupy the dedup key)
- pushVertexBuffers key now uses metalSlot so chunk vertex buffers (slot 3)
  are logged instead of only the GUI buffer (slot 2)
- drawIndexedNative: 5s window stats (draw count/max idxCount/UInt32 count/
  baseVertex & indexOffset ranges) replacing first-entry-only logging
- write passclear diag (previous patch left the snapshot vars without the
  log call): color/depth pending takeover results
- readbackDepth: read back main depth texture 4x4 after present - uniform
  value = only clear (blocks not writing depth), non-uniform = blocks rendered
- drop redundant readback 'upload' (panorama upload already verified)
- draw(firstVertex, vertexCount) is the only geometry path 1.21.11 chunks use
  (drawIndexedNative never called per drawIdxStat absence, countDraw rises)
- drawStat log (5s): draws/firstVertex min-max/vertexCount min-max/raw mode
- multiDraw log (5s): draw count + first draw's firstIndex/indexCount/slot
  (was missing since v5)
- MetalCompiledRenderPipeline.getVertexFormatMode() for raw topology in logs
- multiDraw log now includes vb/ib handle+size; first chunk draw triggers
  readbackBuffer(chunkVb, 128B) and readbackBuffer(chunkIb, 64B) via GPU
  staging copy (chunk buffers are Private, not CPU-readable)
- bindPipeline log appends vertex format dump (stride + name(type x count @offset)
  per element) to cross-check against MSL stage_in and buffer layout
- MetalCompiledRenderPipeline.getVertexFormat()
- temporarily force setCullMode(None) for all pipelines to test whether
  cull/winding is the root cause of invisible chunks (one round)
- pushDescriptor UNIFORM_BUFFER: log on content change (5s throttle) instead
  of once, capturing world pass Projection/ChunkSection matrices
- storageHex 64->192 reveals ChunkVisibility@64/TextureSize@72/ChunkPosition@80
  (ChunkSection) and CameraBlockPos@0/CameraOffset@16 (Globals)
- vertex translation relies on these vec3/ivec3 members (terrain.vsh:
  pos = Position + (ChunkPosition - CameraBlockPos) + CameraOffset);
  spvc MSL struct layout already proven == MC Std140Builder layout via probe
- bindPipeline log now calls describeVertexFormat() (v8 replacement had
  failed silently) - actual stride + per-attribute offset vs chunk data
  layout (stride=28B: Position@0 float3 + Color@12 ubyte4 + UV0@16 float2
  + UV2@24 short2)
- Swift drawPrimitives/drawIndexedPrimitives: throttled (5s) NSLog with
  call total, proving Metal encoder actually receives draw commands
- UBO/matrix path fully exonerated (v10): ChunkVisibility=1.0,
  TextureSize=(1024,2), ChunkPosition=(96,208,-1), CameraBlockPos/CameraOffset
  all correct
- bindPipeline once key now includes describeVertexFormat() so terrain's
  real stride/attribute offsets are visible (clouds shared the old key and
  swallowed it) - cross-check vs chunkVb layout (stride=28: Position@0
  float3 + Color@12 ubyte4 + UV0@16 float2 + UV2@24 short2)
- buildIOSNative/buildMacNative doLast verifies dylib exists non-empty and
  logs size (CI dylibs frozen since v10 - v10/v11 artifact dylibs are
  byte-identical md5, Swift changes never entered the jar)
…unks)

- spvc FLIP_VERTEX_Y flips Y only; GL NDC z[-1,1] passes through,
  so front-facing triangles (z<0) get clipped by Metal NDC z[0,1]
  -> zero rasterization (readbackDepth stays 1.0, main-target only sky)
- GUI was fine because UI z=0 sits exactly on the Metal NDC boundary
- LWJGL 3.3.3 does not expose fixup_clipspace, so apply the exact
  SPIRV-Cross spirv_msl.cpp formula: out.gl_Position.z = (z + w) * 0.5
  inserted right before the existing Y-flip line (same location/format)
- clipspace fix now logs hit ('applied') or miss ('NOT applied' + first
  gl_Position line) to verify whether the z remap actually reached the
  pipeline (metal-mc-terrain PR#2 used identical formula successfully)
- pushDescriptor records Globals slice; multidraw window triggers
  readbackBuffer(globals, 64B) to verify CameraBlockPos@0/CameraOffset@16
…ring

Root cause (mcmodding + javap): MC 1.21.11 passes the Globals UBO via
RenderSystem.setGlobalSettingsUniform (independent entry, not RenderPass.setUniform)
- our Metal backend never received it, so terrain.vsh's
pos = Position + (ChunkPosition - CameraBlockPos) + CameraOffset read 0
for CameraBlockPos/CameraOffset -> absolute world coords (~1549,3328) ->
outside perspective far plane -> everything clipped (readbackDepth=1.0).

Fix:
- RenderSystemGlobalsMixin: @Inject setGlobalSettingsUniform HEAD -> MetalBackend stores buffer
- MetalCrossShaderCompiler: extract Globals [[buffer(N)]] per stage from MSL
  (terrain vertex 0 / fragment 1; glint fragment 3)
- MetalCompiledRenderPipeline: store globalsBindings, getGlobalsBinding(stage)
- MetalRenderPass.bindDrawState: bind Globals buffer per stage after descriptors
- Z clipspace remap (v13) kept: necessary GL->Metal depth range fix (verified applied)
…eline

- MetalRenderPass: drop drawStat/drawIdxStat/multiDraw/vbuf/texbind/ubind/
  pipe logs, draw-call counter, diag fields, describeVertexFormat/storageHex
- MetalCommandEncoder: drop pass/passclear/present logs, diagReadback/
  diagReadbackDepth/readbackBuffer (GPU sync points), lastDepthTexture
- MetalCrossShaderCompiler: keep clipspace fix, drop applied/NOT logs
- MetalDevice: drop createTexture log
- MetalCompiledRenderPipeline: drop MSL dump log
- Diagnostics class kept for future debugging
- When all draws share indexBuffer/indexType/vertexBuffer/slot and have no
  per-draw uniform uploader, encode once via multiDrawIndexed (N FFM
  boundary calls -> 1) instead of per-draw drawIndexedPrimitives
- firstIndexOffsets in bytes (element * index width), vertexOffsets all 0
  (1.21.11 Draw has no baseVertex); instanceCount=1 baseInstance=0
- TriangleFan path (transient buffer expansion) excluded from batching
- Fallback to per-draw loop when conditions not met
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants