From c0068c4b3e184eccd4c0458ddc77bb3f2d2a5c77 Mon Sep 17 00:00:00 2001 From: ai-fix-bot Date: Tue, 28 Jul 2026 01:01:57 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix(metal):=20=E4=BF=AE=E5=A4=8D=E7=BC=93?= =?UTF-8?q?=E5=86=B2=E5=88=9B=E5=BB=BA=E5=A4=B1=E8=B4=A5=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E7=9A=84=20SIGSEGV=20=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:MetalGpuBuffer. 对 size=0 无下限 clamp,allocationSize=(size+15)&~15 在 size=0 时为 0,经 metallum_create_buffer 传给 makeBuffer(length:0) 按 Apple 契约 返回 nil,抛裸 "Failed to create Metal buffer";叠加 nativeHandle() 只查 Java null 不查 address()==0,MemorySegment.NULL 静默穿透到 drawIndexedPrimitivesIndirect, AGXMetal 解引用 0x0 触发 SIGSEGV。 修复: - MetalGpuBuffer.: 入口校验 size<=0 / 对齐溢出 / 超 maxBufferAllocationSize; 失败异常携带 size/resourceOptions/device 诊断(非裸消息) - MetalDevice: createBuffer 两重载入口拦截 size<=0 与空 ByteBuffer; 新增包级 maxBufferAllocationSize() 暴露 device 上限 - MetalGpuBuffer.nativeHandle(): 增加 address()==0L 检查,阻断 NULL 段穿透 - MetalRenderPass: drawIndexedIndirect 派发前校验 drawCount/indexBuffer/ indirect buffer isClosed/容量,失败 warn+return;drawIndexed 补 indexBuffer null 防御 - MetallumNative.swift: metallum_create_buffer 加 guard length>0; drawIndexedPrimitivesIndirect 加越界/溢出校验(defense in depth) --- .../client/metal/render/MetalDevice.java | 10 ++++++++ .../client/metal/render/MetalGpuBuffer.java | 23 ++++++++++++++----- .../client/metal/render/MetalRenderPass.java | 21 +++++++++++++++++ src/main/native/MetallumNative.swift | 11 ++++++++- 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/metallum/client/metal/render/MetalDevice.java b/src/main/java/com/metallum/client/metal/render/MetalDevice.java index 765f46bbe..8a1f3bc92 100644 --- a/src/main/java/com/metallum/client/metal/render/MetalDevice.java +++ b/src/main/java/com/metallum/client/metal/render/MetalDevice.java @@ -126,11 +126,17 @@ final class MetalDevice implements GpuDeviceBackend { @Override public @NonNull GpuBuffer createBuffer(@Nullable final Supplier label, @GpuBuffer.Usage final int usage, final long size) { + if (size <= 0L) { + throw new IllegalArgumentException("Metal buffer size must be > 0 (got " + size + ")"); + } return new MetalGpuBuffer(this, usage, size); } @Override public @NonNull GpuBuffer createBuffer(@Nullable final Supplier label, @GpuBuffer.Usage final int usage, final ByteBuffer data) { + if (data == null || data.remaining() <= 0) { + throw new IllegalArgumentException("Cannot create buffer from empty ByteBuffer"); + } MetalGpuBuffer buffer = (MetalGpuBuffer) this.createBuffer(label, usage | GpuBuffer.USAGE_COPY_DST, data.remaining()); this.commandEncoder.writeToBuffer(buffer.slice(), data.duplicate()); return buffer; @@ -207,6 +213,10 @@ MemorySegment metalDeviceHandle() { return this.metalDeviceHandle; } + long maxBufferAllocationSize() { + return this.deviceInfo.limits().maxMemoryAllocationSize(); + } + void waitForSubmittedGpuWork() { this.commandEncoder.waitForSubmittedGpuWork(); } diff --git a/src/main/java/com/metallum/client/metal/render/MetalGpuBuffer.java b/src/main/java/com/metallum/client/metal/render/MetalGpuBuffer.java index 24d3d187c..cb6fe79cb 100644 --- a/src/main/java/com/metallum/client/metal/render/MetalGpuBuffer.java +++ b/src/main/java/com/metallum/client/metal/render/MetalGpuBuffer.java @@ -35,7 +35,14 @@ class MetalGpuBuffer extends GpuBuffer { this.dynamic = isDynamic(usage); this.cpuAccessible = isCpuAccessible(usage) || this.dynamic; this.resourceOptions = toMtlResourceOptions(usage); - this.allocationSize = (size + 15L) & ~15L; + if (size <= 0L) { + throw new IllegalArgumentException("Metal buffer size must be > 0 (got " + size + ")"); + } + long aligned = (size + 15L) & ~15L; + if (aligned <= 0L) { + throw new IllegalArgumentException("Metal buffer size overflow after alignment: " + size); + } + this.allocationSize = aligned; MemorySegment pooled = device.tryAcquirePooledBuffer(this.allocationSize, this.resourceOptions); if (!MetalNativeBridge.isNullHandle(pooled)) { @@ -45,16 +52,20 @@ class MetalGpuBuffer extends GpuBuffer { if (MetalNativeBridge.isNullHandle(contents)) { MetalNativeBridge.metallum_release_object(pooled); this.nativeHandle = null; - throw new IllegalStateException("MTLBuffer.contents returned null for pooled buffer"); + throw new IllegalStateException("MTLBuffer.contents returned null for pooled buffer (size=" + this.allocationSize + ", resourceOptions=" + this.resourceOptions + ")"); } this.storage = MetalNativeBridge.nativeByteBufferView(contents, this.allocationSize).order(ByteOrder.nativeOrder()); } return; } + long max = device.maxBufferAllocationSize(); + if (max > 0L && this.allocationSize > max) { + throw new IllegalArgumentException("Metal buffer size " + this.allocationSize + " exceeds device max " + max); + } this.nativeHandle = MetalNativeBridge.metallum_create_buffer(device.metalDeviceHandle(), this.allocationSize, this.resourceOptions); if (MetalNativeBridge.isNullHandle(this.nativeHandle)) { - throw new IllegalStateException("Failed to create Metal buffer"); + throw new IllegalStateException("Failed to create Metal buffer (size=" + this.allocationSize + ", resourceOptions=" + this.resourceOptions + ", device=" + this.device.getClass().getSimpleName() + ")"); } if (this.cpuAccessible) { @@ -62,7 +73,7 @@ class MetalGpuBuffer extends GpuBuffer { if (MetalNativeBridge.isNullHandle(contents)) { MetalNativeBridge.metallum_release_object(this.nativeHandle); this.nativeHandle = null; - throw new IllegalStateException("MTLBuffer.contents returned null"); + throw new IllegalStateException("MTLBuffer.contents returned null (size=" + this.allocationSize + ", resourceOptions=" + this.resourceOptions + ")"); } this.storage = MetalNativeBridge.nativeByteBufferView(contents, this.allocationSize).order(ByteOrder.nativeOrder()); @@ -92,8 +103,8 @@ ByteBuffer sliceStorage(final long offset, final long length) { } MemorySegment nativeHandle() { - if (this.nativeHandle == null) { - throw new IllegalStateException("Native Metal buffer is closed"); + if (this.nativeHandle == null || this.nativeHandle.address() == 0L) { + throw new IllegalStateException("Native Metal buffer is closed or null"); } return this.nativeHandle; } diff --git a/src/main/java/com/metallum/client/metal/render/MetalRenderPass.java b/src/main/java/com/metallum/client/metal/render/MetalRenderPass.java index 0d356a1d3..e99749e76 100644 --- a/src/main/java/com/metallum/client/metal/render/MetalRenderPass.java +++ b/src/main/java/com/metallum/client/metal/render/MetalRenderPass.java @@ -1,5 +1,6 @@ package com.metallum.client.metal.render; +import com.metallum.Metallum; import com.metallum.client.metal.render.bridge.MetalNativeBridge; import com.metallum.client.metal.render.mtl.*; import com.mojang.blaze3d.GpuFormat; @@ -183,6 +184,10 @@ private void setIndexBuffer(@Nullable final GpuBuffer indexBuffer, final MTLInde @Override public void drawIndexed(final int indexCount, final int instanceCount, final int firstIndex, final int vertexOffset, final int firstInstance) { + if (this.indexBuffer == null) { + Metallum.LOGGER.warn("[metallum] drawIndexed called with null index buffer, skipping draw"); + return; + } MetalGpuBuffer nativeIndexBuffer = (MetalGpuBuffer) indexBuffer; MTLRenderCommandEncoder enc = renderEncoder(); @@ -233,10 +238,26 @@ public void multiDrawIndexed(@NonNull PointerBuffer firstIndexOffsets, @NonNull @Override public void drawIndexedIndirect(final @NonNull GpuBufferSlice commands, final int drawCount) { + if (drawCount <= 0) { + return; + } MTLPrimitiveType primitiveType = primitiveTopology(); if (primitiveType == MTLPrimitiveType.TriangleFan) { throw new UnsupportedOperationException("Metal backend does not support triangle fan indirect draws"); } + if (this.indexBuffer == null) { + Metallum.LOGGER.warn("[metallum] drawIndexedIndirect called with null index buffer, skipping draw"); + return; + } + if (commands.buffer().isClosed()) { + Metallum.LOGGER.warn("[metallum] drawIndexedIndirect called with closed indirect command buffer, skipping draw"); + return; + } + long needed = (long) drawCount * VkDrawIndexedIndirectCommand.SIZEOF; + if (commands.length() < needed) { + Metallum.LOGGER.warn("[metallum] drawIndexedIndirect command buffer too small: need {} bytes, have {} (drawCount={})", needed, commands.length(), drawCount); + return; + } MetalGpuBuffer nativeIndexBuffer = (MetalGpuBuffer) indexBuffer; MTLRenderCommandEncoder enc = renderEncoder(); diff --git a/src/main/native/MetallumNative.swift b/src/main/native/MetallumNative.swift index 0cec1d417..52a596c1d 100644 --- a/src/main/native/MetallumNative.swift +++ b/src/main/native/MetallumNative.swift @@ -839,7 +839,8 @@ public func metallum_create_buffer( _ options: MTLResourceOptions ) -> UnsafeMutableRawPointer? { return autoreleasepool { - retainedPointer(device.makeBuffer(length: length, options: options)) + guard length > 0 else { return nil } + return retainedPointer(device.makeBuffer(length: length, options: options)) } } @@ -1223,6 +1224,14 @@ public func metallum_MTLRenderCommandEncoder_drawIndexedPrimitivesIndirect( _ drawCount: Int, _ stride: UInt64 ) { + if drawCount <= 0 { return } + let mul = Int(stride) * drawCount + if mul < 0 { return } + let needed = Int(indirectBufferOffset) + mul + if needed < 0 || needed > indirectBuffer.length { + return + } + if Int(indirectBufferOffset) < 0 { return } var offset = Int(indirectBufferOffset) for _ in 0.. Date: Tue, 28 Jul 2026 23:50:47 +0000 Subject: [PATCH 2/3] fix(metal): bound dynamic backing pool and degrade on OOM for iOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iOS 3-5 分钟延迟闪退根因:MetalCommandEncoder.dynamicBackingPool 是全仓 唯一无上限对象池,recycleDynamicBacking 永远 push 无 eviction;所有 backing 为 MTLStorageMode.Shared(iOS 计入 RSS)。叠加 acquire 立即/recycle 延迟 2-3 帧 不对称,稳态 live backing 数被延迟系数放大,iOS recommendedMaxWorkingSetSize 耗尽后 jetsam 杀进程或 makeBuffer 返回 nil 抛 IllegalStateException 崩渲染线程。 macOS 内存大+swap 容忍故未报告。 修复: - MetalDevice.composePoolKey 去 private 改包级静态,MetalCommandEncoder 复用 - MetalCommandEncoder 加 MAX_POOLED_DYNAMIC_BACKINGS_PER_SIZE=8;acquire/recycle 改用 composePoolKey(size, resourceOptions) 复合键(替代 size-only,维度一致) - recycleDynamicBacking 桶满时 metallum_release_object 释放(替代无界 push) - acquireDynamicBacking 在 makeBuffer nil(OOM)时 return MemorySegment.NULL + warn, 不抛 IllegalStateException;orphanWrite 检测 fresh.address()==0L 早退 return (在 recycle 之前,无双持),old backing 保持合法,GPU 读上一帧 uniform, 下一帧 destroyQueue.rotate 后池命中自动恢复(方案 A:跳过+log) 不实现 staging blit(核查不可行:hazard+transient 生命周期不匹配);不实现 mid-frame flush(架构侵入高)。 --- .../metal/render/MetalCommandEncoder.java | 27 ++++++++++++++----- .../client/metal/render/MetalDevice.java | 2 +- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/metallum/client/metal/render/MetalCommandEncoder.java b/src/main/java/com/metallum/client/metal/render/MetalCommandEncoder.java index 40f5d4327..9b0492d5f 100644 --- a/src/main/java/com/metallum/client/metal/render/MetalCommandEncoder.java +++ b/src/main/java/com/metallum/client/metal/render/MetalCommandEncoder.java @@ -1,5 +1,6 @@ package com.metallum.client.metal.render; +import com.metallum.Metallum; import com.metallum.client.metal.render.bridge.MetalNativeBridge; import com.metallum.client.metal.render.mtl.*; import com.mojang.blaze3d.buffers.GpuBuffer; @@ -45,6 +46,7 @@ final class MetalCommandEncoder implements CommandEncoderBackend { private MemorySegment renderColorAttachment = MemorySegment.NULL; private MemorySegment renderDepthAttachment = MemorySegment.NULL; private final Long2ObjectOpenHashMap> dynamicBackingPool = new Long2ObjectOpenHashMap<>(); + private static final int MAX_POOLED_DYNAMIC_BACKINGS_PER_SIZE = 8; MetalCommandEncoder(final MetalDevice device) { this.device = device; @@ -339,6 +341,9 @@ private void orphanWrite(final MetalGpuBuffer buffer, final long offset, final B long size = buffer.allocationSize(); MemorySegment old = buffer.nativeHandle(); MemorySegment fresh = acquireDynamicBacking(size, buffer.resourceOptions()); + if (fresh.address() == 0L) { + return; + } ByteBuffer freshStorage = MetalNativeBridge.nativeByteBufferView( MetalNativeBridge.metallum_get_buffer_contents(fresh), size).order(ByteOrder.nativeOrder()); @@ -353,23 +358,33 @@ private void orphanWrite(final MetalGpuBuffer buffer, final long offset, final B dst.put(data.duplicate()); buffer.swapBacking(fresh, freshStorage); - recycleDynamicBacking(old, size); + recycleDynamicBacking(old, size, buffer.resourceOptions()); } private MemorySegment acquireDynamicBacking(final long size, final long resourceOptions) { - java.util.ArrayDeque bucket = dynamicBackingPool.get(size); + final long key = MetalDevice.composePoolKey(size, resourceOptions); + final java.util.ArrayDeque bucket = dynamicBackingPool.get(key); if (bucket != null && !bucket.isEmpty()) { return bucket.pop(); } - MemorySegment handle = MetalNativeBridge.metallum_create_buffer(device.metalDeviceHandle(), size, resourceOptions); + final MemorySegment handle = MetalNativeBridge.metallum_create_buffer(device.metalDeviceHandle(), size, resourceOptions); if (MetalNativeBridge.isNullHandle(handle)) { - throw new IllegalStateException("Failed to create dynamic backing buffer"); + Metallum.LOGGER.warn("dynamic backing OOM, skipping uniform update this frame"); + return MemorySegment.NULL; } return handle; } - private void recycleDynamicBacking(final MemorySegment handle, final long size) { - queueForDestroy(() -> dynamicBackingPool.computeIfAbsent(size, k -> new java.util.ArrayDeque<>()).push(handle)); + private void recycleDynamicBacking(final MemorySegment handle, final long size, final long resourceOptions) { + queueForDestroy(() -> { + final long key = MetalDevice.composePoolKey(size, resourceOptions); + java.util.ArrayDeque bucket = dynamicBackingPool.computeIfAbsent(key, k -> new java.util.ArrayDeque<>()); + if (bucket.size() < MAX_POOLED_DYNAMIC_BACKINGS_PER_SIZE) { + bucket.push(handle); + } else { + MetalNativeBridge.metallum_release_object(handle); + } + }); } @Override diff --git a/src/main/java/com/metallum/client/metal/render/MetalDevice.java b/src/main/java/com/metallum/client/metal/render/MetalDevice.java index 8a1f3bc92..01a889944 100644 --- a/src/main/java/com/metallum/client/metal/render/MetalDevice.java +++ b/src/main/java/com/metallum/client/metal/render/MetalDevice.java @@ -246,7 +246,7 @@ void queueBufferRelease(final MemorySegment handle, final long size, final long }); } - private static long composePoolKey(final long size, final long resourceOptions) { + static long composePoolKey(final long size, final long resourceOptions) { return (size << 12) | (resourceOptions & 0xFFFL); } From 772f1cc8512d04c65ed3f17ce51b2b1bf1a61785 Mon Sep 17 00:00:00 2001 From: Trae Bot Date: Wed, 29 Jul 2026 03:47:55 +0000 Subject: [PATCH 3/3] fix(metal): bound bufferPool buckets with LRU eviction for iOS chunk loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iOS 跑图 3-6 分钟延迟闪退根因(前两轮修复 c0068c4/aee788d 后仍复现): MetalDevice.bufferPool 为 HashMap,每桶 16 上限但 distinct (size, resourceOptions) 桶数无上限,drainBufferPool 仅 device.close() 调用。 跑图时新区块加载产生各异 size 的 vertex/index buffer(每 region 几何不同 → allocationSize 不同 → 新桶),桶数单调增长,每桶最多 16 个 MTLBuffer 滞留。 iOS 集成 GPU 无独立显存,Private buffer 也计入 jetsam footprint(修正 "Private 不占 RSS"的 macOS 心智模型错误),累积到阈值后 jetsam 杀进程。 挂机不崩证明 uniform 路径(dynamicBackingPool,aee788d 已修)非本次主因。 修复: - bufferPool 从 HashMap 改为 LinkedHashMap(accessOrder=true),override removeEldestEntry,桶数超 MAX_POOLED_BUFFER_BUCKETS=32 时淘汰最旧桶, 遍历释放其中所有 MTLBuffer - MAX_POOLED_BUFFERS_PER_SIZE 16→8(防御纵深) - tryAcquirePooledBuffer/queueBufferRelease/drainBufferPool 逻辑不变 (LinkedHashMap.get/computeIfAbsent 自动维护 access-order, removeEldestEntry 自动触发;drainBufferPool 遍历释放逻辑不变) 不引入 dynamicBackingPool LRU(挂机不崩证明非主因)、storage mode 特判 (iOS 上 Private 也占 footprint,特判无效且破坏 macOS 语义)、周期性 drain (无 iOS memory-warning 钩子)、字节上限(Stats.java 死代码,过度工程)。 --- .../client/metal/render/MetalDevice.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/metallum/client/metal/render/MetalDevice.java b/src/main/java/com/metallum/client/metal/render/MetalDevice.java index 01a889944..091d16c5f 100644 --- a/src/main/java/com/metallum/client/metal/render/MetalDevice.java +++ b/src/main/java/com/metallum/client/metal/render/MetalDevice.java @@ -42,8 +42,20 @@ final class MetalDevice implements GpuDeviceBackend { private final Map compiledPipelines = new IdentityHashMap<>(); private final Map shaderCache = new HashMap<>(); private final Map functionCache = new HashMap<>(); - private final Map> bufferPool = new HashMap<>(); - private static final int MAX_POOLED_BUFFERS_PER_SIZE = 16; + private static final int MAX_POOLED_BUFFER_BUCKETS = 32; + private static final int MAX_POOLED_BUFFERS_PER_SIZE = 8; + private final Map> bufferPool = new LinkedHashMap<>(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(final Map.Entry> eldest) { + if (size() <= MAX_POOLED_BUFFER_BUCKETS) { + return false; + } + for (MemorySegment handle : eldest.getValue()) { + MetalNativeBridge.metallum_release_object(handle); + } + return true; + } + }; private ShaderSource activeShaderSource; MetalDevice(