Skip to content
Merged

. #10

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -45,6 +46,7 @@ final class MetalCommandEncoder implements CommandEncoderBackend {
private MemorySegment renderColorAttachment = MemorySegment.NULL;
private MemorySegment renderDepthAttachment = MemorySegment.NULL;
private final Long2ObjectOpenHashMap<java.util.ArrayDeque<MemorySegment>> dynamicBackingPool = new Long2ObjectOpenHashMap<>();
private static final int MAX_POOLED_DYNAMIC_BACKINGS_PER_SIZE = 8;

MetalCommandEncoder(final MetalDevice device) {
this.device = device;
Expand Down Expand Up @@ -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());

Expand All @@ -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<MemorySegment> bucket = dynamicBackingPool.get(size);
final long key = MetalDevice.composePoolKey(size, resourceOptions);
final java.util.ArrayDeque<MemorySegment> 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<MemorySegment> 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
Expand Down
28 changes: 25 additions & 3 deletions src/main/java/com/metallum/client/metal/render/MetalDevice.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,20 @@ final class MetalDevice implements GpuDeviceBackend {
private final Map<RenderPipeline, MetalCompiledRenderPipeline> compiledPipelines = new IdentityHashMap<>();
private final Map<ShaderCompilationKey, IntermediaryShaderModule> shaderCache = new HashMap<>();
private final Map<MslFunctionKey, MemorySegment> functionCache = new HashMap<>();
private final Map<Long, Deque<MemorySegment>> 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<Long, Deque<MemorySegment>> bufferPool = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(final Map.Entry<Long, Deque<MemorySegment>> 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(
Expand Down Expand Up @@ -126,11 +138,17 @@ final class MetalDevice implements GpuDeviceBackend {

@Override
public @NonNull GpuBuffer createBuffer(@Nullable final Supplier<String> 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<String> 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;
Expand Down Expand Up @@ -207,6 +225,10 @@ MemorySegment metalDeviceHandle() {
return this.metalDeviceHandle;
}

long maxBufferAllocationSize() {
return this.deviceInfo.limits().maxMemoryAllocationSize();
}

void waitForSubmittedGpuWork() {
this.commandEncoder.waitForSubmittedGpuWork();
}
Expand Down Expand Up @@ -236,7 +258,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);
}

Expand Down
23 changes: 17 additions & 6 deletions src/main/java/com/metallum/client/metal/render/MetalGpuBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -45,24 +52,28 @@ 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) {
MemorySegment contents = MetalNativeBridge.metallum_get_buffer_contents(this.nativeHandle);
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());
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();
Expand Down
11 changes: 10 additions & 1 deletion src/main/native/MetallumNative.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down Expand Up @@ -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..<drawCount {
encoder.drawIndexedPrimitives(
Expand Down