mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
From #61, without the default "AMD Radeon Pro 5500M" / 4 GB identity: the host GPU's name and Vulkan working set stay the default for every app, and the existing DARLING_METAL_GPU_NAME and DARLING_METAL_VRAM_MB overrides remain the opt-in. Reporting 4 GB on a 2 GB card invites allocation failures, and under-reports larger cards. Kept from #61: - registryID returns MTLDeviceInfo::kDefaultRegistryID instead of 0; a real Mac never reports 0 and apps key devices by it. - Vertex buffer layout and attribute arrays release the old item after retiring it; the retire step read the item after the release could free it. - newDepthStencilState, newSamplerState and the device name return nil (or the fallback name) when Indium throws, instead of unwinding through Objective-C. - MTLDepthStencilGetIndium is declared only with DARLING_METAL_ENABLED. - The replaceRegion trace probes at most `width` bytes, which stays inside the first row for every pixel format. metal.patch is regenerated in its existing file order, so the diff is these changes only. tests/test_metal_device_info.py compiles the host check against MTLDeviceInfo.h as shipped in the patch and runs in CI. Split from #61. Co-authored-by: DevWebeloper <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]>
3861 lines
174 KiB
Diff
3861 lines
174 KiB
Diff
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
|
index 431f29a..eb3eec8 100644
|
|
--- a/CMakeLists.txt
|
|
+++ b/CMakeLists.txt
|
|
@@ -42,8 +42,10 @@ add_framework(Metal
|
|
SOURCES
|
|
src/Metal/Metal.mm
|
|
src/Metal/MTLBuffer.mm
|
|
+ src/Metal/MTLArgument.mm
|
|
src/Metal/MTLCaptureManager.mm
|
|
src/Metal/MTLCommandBuffer.mm
|
|
+ src/Metal/MTLBlitCommandEncoder.mm
|
|
src/Metal/MTLCommandQueue.mm
|
|
src/Metal/MTLCompileOptions.mm
|
|
src/Metal/MTLComputeCommandEncoder.mm
|
|
@@ -97,6 +99,10 @@ if (BUILD_METAL)
|
|
target_compile_definitions(Metal PUBLIC
|
|
DARLING_METAL_ENABLED=1
|
|
)
|
|
+ # MTLDevice.mm reads the Vulkan device properties from Indium's private headers.
|
|
+ target_link_libraries(Metal PRIVATE
|
|
+ indium_private
|
|
+ )
|
|
endif()
|
|
|
|
#
|
|
diff --git a/include/Metal/MTLCommandBuffer.h b/include/Metal/MTLCommandBuffer.h
|
|
index ef43df8..9045a84 100644
|
|
--- a/include/Metal/MTLCommandBuffer.h
|
|
+++ b/include/Metal/MTLCommandBuffer.h
|
|
@@ -16,6 +16,7 @@ METAL_DECLARATIONS_BEGIN
|
|
@protocol MTLComputeCommandEncoder;
|
|
@protocol MTLCommandQueue;
|
|
@protocol MTLRenderCommandEncoder;
|
|
+@protocol MTLBlitCommandEncoder;
|
|
|
|
@class MTLComputePassDescriptor;
|
|
@class MTLRenderPassDescriptor;
|
|
@@ -36,9 +37,11 @@ typedef void (^MTLCommandBufferHandler)(id<MTLCommandBuffer>);
|
|
- (id<MTLComputeCommandEncoder>)computeCommandEncoderWithDescriptor: (MTLComputePassDescriptor*)computePassDescriptor;
|
|
- (id<MTLComputeCommandEncoder>)computeCommandEncoderWithDispatchType: (MTLDispatchType)dispatchType;
|
|
- (id<MTLComputeCommandEncoder>)computeCommandEncoder;
|
|
+- (id<MTLBlitCommandEncoder>)blitCommandEncoder;
|
|
|
|
- (id<MTLRenderCommandEncoder>)renderCommandEncoderWithDescriptor: (MTLRenderPassDescriptor*)renderPassDescriptor;
|
|
|
|
+- (void)addScheduledHandler:(MTLCommandBufferHandler)block;
|
|
- (void)addCompletedHandler: (MTLCommandBufferHandler)block;
|
|
- (void)waitUntilCompleted;
|
|
- (void)presentDrawable: (id<MTLDrawable>)drawable;
|
|
diff --git a/include/Metal/MTLComputeCommandEncoder.h b/include/Metal/MTLComputeCommandEncoder.h
|
|
index 06f4e51..a8e7436 100644
|
|
--- a/include/Metal/MTLComputeCommandEncoder.h
|
|
+++ b/include/Metal/MTLComputeCommandEncoder.h
|
|
@@ -13,6 +13,9 @@ METAL_DECLARATIONS_BEGIN
|
|
@protocol MTLComputeCommandEncoder;
|
|
@protocol MTLCounterSampleBuffer;
|
|
@protocol MTLBuffer;
|
|
+@protocol MTLTexture;
|
|
+@protocol MTLSamplerState;
|
|
+@protocol MTLDepthStencilState;
|
|
|
|
@class MTLComputePassDescriptor;
|
|
@class MTLComputePassSampleBufferAttachmentDescriptorArray;
|
|
@@ -53,6 +56,9 @@ MTL_EXPORT
|
|
|
|
@property(readonly) MTLDispatchType dispatchType;
|
|
|
|
+- (void)setTexture:(id<MTLTexture>)texture atIndex:(NSUInteger)index;
|
|
+- (void)setSamplerState:(id<MTLSamplerState>)sampler atIndex:(NSUInteger)index;
|
|
+- (void)setSamplerState:(id<MTLSamplerState>)sampler lodMinClamp:(float)lodMinClamp lodMaxClamp:(float)lodMaxClamp atIndex:(NSUInteger)index;
|
|
- (void)setComputePipelineState:(id<MTLComputePipelineState>)state;
|
|
|
|
- (void)setBuffer: (id<MTLBuffer>)buffer
|
|
diff --git a/include/Metal/MTLDepthStencilDescriptor.h b/include/Metal/MTLDepthStencilDescriptor.h
|
|
index f22a57d..6b6715d 100644
|
|
--- a/include/Metal/MTLDepthStencilDescriptor.h
|
|
+++ b/include/Metal/MTLDepthStencilDescriptor.h
|
|
@@ -3,10 +3,17 @@
|
|
|
|
#ifndef _METAL_MTLDEPTHSTENCILDESCRIPTOR_H_
|
|
#define _METAL_MTLDEPTHSTENCILDESCRIPTOR_H_
|
|
-
|
|
-#import <Foundation/Foundation.h>
|
|
-
|
|
-@interface MTLDepthStencilDescriptor : NSObject
|
|
+#import <Metal/MTLStencilDescriptor.h>
|
|
+@protocol MTLDevice;
|
|
+@interface MTLDepthStencilDescriptor : NSObject <NSCopying>
|
|
+@property(nonatomic) MTLCompareFunction depthCompareFunction;
|
|
+@property(nonatomic) BOOL depthWriteEnabled;
|
|
+@property(nonatomic, copy) MTLStencilDescriptor* frontFaceStencil;
|
|
+@property(nonatomic, copy) MTLStencilDescriptor* backFaceStencil;
|
|
+@property(nonatomic, copy) NSString* label;
|
|
@end
|
|
-
|
|
-#endif // _METAL_MTLDEPTHSTENCILDESCRIPTOR_H_
|
|
+@protocol MTLDepthStencilState <NSObject>
|
|
+@property(readonly) id<MTLDevice> device;
|
|
+@property(readonly) NSString* label;
|
|
+@end
|
|
+#endif
|
|
diff --git a/include/Metal/MTLDevice.h b/include/Metal/MTLDevice.h
|
|
index a3efc96..299c038 100644
|
|
--- a/include/Metal/MTLDevice.h
|
|
+++ b/include/Metal/MTLDevice.h
|
|
@@ -18,9 +18,18 @@ METAL_DECLARATIONS_BEGIN
|
|
@protocol MTLBuffer;
|
|
@protocol MTLLibrary;
|
|
@protocol MTLRenderPipelineState;
|
|
+@protocol MTLSamplerState;
|
|
+@protocol MTLDepthStencilState;
|
|
+@class MTLDepthStencilDescriptor;
|
|
+@class MTLSamplerDescriptor;
|
|
+@class MTLTextureDescriptor;
|
|
+@protocol MTLTexture;
|
|
|
|
@class MTLComputePipelineDescriptor;
|
|
-@class MTLAutoreleasedComputePipelineReflection;
|
|
+@class MTLComputePipelineReflection;
|
|
+@class MTLRenderPipelineReflection;
|
|
+typedef MTLComputePipelineReflection* MTLAutoreleasedComputePipelineReflection;
|
|
+typedef MTLRenderPipelineReflection* MTLAutoreleasedRenderPipelineReflection;
|
|
@class MTLRenderPipelineDescriptor;
|
|
|
|
typedef NS_OPTIONS(NSUInteger, MTLPipelineOption) {
|
|
@@ -42,8 +51,31 @@ MTL_EXPORT NSArray<id<MTLDevice>>* MTLCopyAllDevices(void);
|
|
MTL_EXPORT NSArray<id<MTLDevice>>* MTLCopyAllDevicesWithObserver(id<NSObject>* observer, MTLDeviceNotificationHandler handler);
|
|
MTL_EXPORT void MTLRemoveDeviceObserver(id<NSObject> observer);
|
|
|
|
+typedef NS_ENUM(NSInteger, MTLReadWriteTextureTier) {
|
|
+ MTLReadWriteTextureTierNone = 0,
|
|
+ MTLReadWriteTextureTier1 = 1,
|
|
+ MTLReadWriteTextureTier2 = 2,
|
|
+};
|
|
+
|
|
@protocol MTLDevice <NSObject>
|
|
|
|
+@property (readonly) MTLReadWriteTextureTier readWriteTextureSupport;
|
|
+
|
|
+/* darling-lol M57: additional device queries ANGLE's Metal backend probes. */
|
|
+@property (readonly) BOOL hasUnifiedMemory;
|
|
+@property (readonly) NSUInteger maxBufferLength;
|
|
+@property (readonly) NSUInteger recommendedMaxWorkingSetSize;
|
|
+@property (readonly) BOOL isLowPower;
|
|
+@property (readonly) BOOL isRemovable;
|
|
+@property (readonly) BOOL isHeadless;
|
|
+@property (readonly) NSString* name;
|
|
+@property (readonly) uint64_t registryID;
|
|
+- (BOOL)isDepth24Stencil8PixelFormatSupported;
|
|
+- (BOOL)supportsFamily: (NSUInteger)family;
|
|
+- (BOOL)supportsFeatureSet: (NSUInteger)featureSet;
|
|
+- (BOOL)supportsTextureSampleCount: (NSUInteger)sampleCount;
|
|
+- (id<MTLCommandQueue>)newCommandQueueWithMaxCommandBufferCount: (NSUInteger)maxCommandBufferCount;
|
|
+
|
|
- (id<MTLComputePipelineState>)newComputePipelineStateWithDescriptor: (MTLComputePipelineDescriptor*)descriptor
|
|
options: (MTLPipelineOption)options
|
|
reflection: (MTLAutoreleasedComputePipelineReflection*)reflection
|
|
@@ -57,10 +89,14 @@ MTL_EXPORT void MTLRemoveDeviceObserver(id<NSObject> observer);
|
|
reflection: (MTLAutoreleasedComputePipelineReflection*)reflection
|
|
error: (NSError**)error;
|
|
|
|
+- (id<MTLRenderPipelineState>)newRenderPipelineStateWithDescriptor:(MTLRenderPipelineDescriptor*)descriptor options:(MTLPipelineOption)options reflection:(MTLAutoreleasedRenderPipelineReflection*)reflection error:(NSError**)error;
|
|
- (id<MTLRenderPipelineState>)newRenderPipelineStateWithDescriptor: (MTLRenderPipelineDescriptor*)descriptor
|
|
error: (NSError**)error;
|
|
|
|
+- (id<MTLTexture>)newTextureWithDescriptor:(MTLTextureDescriptor*)descriptor;
|
|
- (id<MTLCommandQueue>)newCommandQueue;
|
|
+- (id<MTLDepthStencilState>)newDepthStencilStateWithDescriptor:(MTLDepthStencilDescriptor*)descriptor;
|
|
+- (id<MTLSamplerState>)newSamplerStateWithDescriptor:(MTLSamplerDescriptor*)descriptor;
|
|
|
|
- (id<MTLBuffer>)newBufferWithLength: (NSUInteger)length
|
|
options: (MTLResourceOptions)options;
|
|
diff --git a/include/Metal/MTLRenderCommandEncoder.h b/include/Metal/MTLRenderCommandEncoder.h
|
|
index 5a3845c..caeab7c 100644
|
|
--- a/include/Metal/MTLRenderCommandEncoder.h
|
|
+++ b/include/Metal/MTLRenderCommandEncoder.h
|
|
@@ -12,6 +12,9 @@
|
|
METAL_DECLARATIONS_BEGIN
|
|
|
|
@protocol MTLBuffer;
|
|
+@protocol MTLTexture;
|
|
+@protocol MTLSamplerState;
|
|
+@protocol MTLDepthStencilState;
|
|
@protocol MTLRasterizationRateMap;
|
|
@protocol MTLTexture;
|
|
@protocol MTLRenderPipelineState;
|
|
@@ -26,6 +29,13 @@ METAL_DECLARATIONS_BEGIN
|
|
struct MTLSamplePosition;
|
|
typedef struct MTLSamplePosition MTLSamplePosition;
|
|
|
|
+typedef NS_OPTIONS(NSUInteger, MTLBarrierScope) {
|
|
+ MTLBarrierScopeBuffers = 1, MTLBarrierScopeTextures = 2, MTLBarrierScopeRenderTargets = 4,
|
|
+};
|
|
+typedef NS_OPTIONS(NSUInteger, MTLRenderStages) {
|
|
+ MTLRenderStageVertex = 1, MTLRenderStageFragment = 2, MTLRenderStageTile = 4,
|
|
+ MTLRenderStageObject = 8, MTLRenderStageMesh = 16,
|
|
+};
|
|
typedef NS_ENUM(NSUInteger, MTLLoadAction) {
|
|
MTLLoadActionDontCare = 0,
|
|
MTLLoadActionLoad = 1,
|
|
@@ -98,10 +108,10 @@ typedef struct MTLViewport {
|
|
} MTLViewport;
|
|
|
|
typedef struct MTLScissorRect {
|
|
- size_t height;
|
|
- size_t width;
|
|
- size_t x;
|
|
- size_t y;
|
|
+ NSUInteger x;
|
|
+ NSUInteger y;
|
|
+ NSUInteger width;
|
|
+ NSUInteger height;
|
|
} MTLScissorRect;
|
|
|
|
NS_INLINE
|
|
@@ -190,6 +200,14 @@ MTL_EXPORT
|
|
|
|
@protocol MTLRenderCommandEncoder <MTLCommandEncoder>
|
|
|
|
+- (void)setVertexTexture:(id<MTLTexture>)texture atIndex:(NSUInteger)index;
|
|
+- (void)setFragmentTexture:(id<MTLTexture>)texture atIndex:(NSUInteger)index;
|
|
+- (void)setVertexSamplerState:(id<MTLSamplerState>)sampler atIndex:(NSUInteger)index;
|
|
+- (void)setFragmentSamplerState:(id<MTLSamplerState>)sampler atIndex:(NSUInteger)index;
|
|
+- (void)setDepthBias:(float)depthBias slopeScale:(float)slopeScale clamp:(float)clamp;
|
|
+- (void)setDepthStencilState:(id<MTLDepthStencilState>)state;
|
|
+- (void)memoryBarrierWithScope:(MTLBarrierScope)scope afterStages:(MTLRenderStages)after beforeStages:(MTLRenderStages)before;
|
|
+- (void)setStencilReferenceValue:(uint32_t)value;
|
|
- (void)setRenderPipelineState: (id<MTLRenderPipelineState>)pipelineState;
|
|
- (void)setTriangleFillMode: (MTLTriangleFillMode)fillMode;
|
|
- (void)setFrontFacingWinding: (MTLWinding)frontFacingWinding;
|
|
diff --git a/include/Metal/MTLSamplerDescriptor.h b/include/Metal/MTLSamplerDescriptor.h
|
|
index f049017..c8026d3 100644
|
|
--- a/include/Metal/MTLSamplerDescriptor.h
|
|
+++ b/include/Metal/MTLSamplerDescriptor.h
|
|
@@ -1,12 +1,46 @@
|
|
-// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
-
|
|
-#ifndef _METAL_MTLSAMPLERDESCRIPTOR_H_
|
|
-#define _METAL_MTLSAMPLERDESCRIPTOR_H_
|
|
-
|
|
+#pragma once
|
|
#import <Foundation/Foundation.h>
|
|
-
|
|
-@interface MTLSamplerDescriptor : NSObject
|
|
+typedef NS_ENUM(NSUInteger, MTLSamplerMinMagFilter) {
|
|
+ MTLSamplerMinMagFilterNearest = 0,
|
|
+ MTLSamplerMinMagFilterLinear = 1,
|
|
+};
|
|
+typedef NS_ENUM(NSUInteger, MTLSamplerMipFilter) {
|
|
+ MTLSamplerMipFilterNotMipmapped = 0,
|
|
+ MTLSamplerMipFilterNearest = 1,
|
|
+ MTLSamplerMipFilterLinear = 2,
|
|
+};
|
|
+typedef NS_ENUM(NSUInteger, MTLSamplerAddressMode) {
|
|
+ MTLSamplerAddressModeClampToEdge = 0,
|
|
+ MTLSamplerAddressModeMirrorClampToEdge = 1,
|
|
+ MTLSamplerAddressModeRepeat = 2,
|
|
+ MTLSamplerAddressModeMirrorRepeat = 3,
|
|
+ MTLSamplerAddressModeClampToZero = 4,
|
|
+ MTLSamplerAddressModeClampToBorderColor = 5,
|
|
+};
|
|
+typedef NS_ENUM(NSUInteger, MTLSamplerBorderColor) {
|
|
+ MTLSamplerBorderColorTransparentBlack = 0,
|
|
+ MTLSamplerBorderColorOpaqueBlack = 1,
|
|
+ MTLSamplerBorderColorOpaqueWhite = 2,
|
|
+};
|
|
+@protocol MTLDevice;
|
|
+@protocol MTLSamplerState <NSObject>
|
|
+@property(readonly) id<MTLDevice> device;
|
|
+@property(readonly) NSString* label;
|
|
+@end
|
|
+@interface MTLSamplerDescriptor : NSObject <NSCopying>
|
|
+@property MTLSamplerMinMagFilter minFilter;
|
|
+@property MTLSamplerMinMagFilter magFilter;
|
|
+@property MTLSamplerMipFilter mipFilter;
|
|
+@property NSUInteger maxAnisotropy;
|
|
+@property MTLSamplerAddressMode sAddressMode;
|
|
+@property MTLSamplerAddressMode tAddressMode;
|
|
+@property MTLSamplerAddressMode rAddressMode;
|
|
+@property MTLSamplerBorderColor borderColor;
|
|
+@property BOOL normalizedCoordinates;
|
|
+@property float lodMinClamp;
|
|
+@property float lodMaxClamp;
|
|
+@property BOOL supportArgumentBuffers;
|
|
+@property NSUInteger compareFunction;
|
|
+@property(copy) NSString* label;
|
|
@end
|
|
-
|
|
-#endif // _METAL_MTLSAMPLERDESCRIPTOR_H_
|
|
diff --git a/include/Metal/MTLStencilDescriptor.h b/include/Metal/MTLStencilDescriptor.h
|
|
index 9dd6f91..ebe0baf 100644
|
|
--- a/include/Metal/MTLStencilDescriptor.h
|
|
+++ b/include/Metal/MTLStencilDescriptor.h
|
|
@@ -3,10 +3,33 @@
|
|
|
|
#ifndef _METAL_MTLSTENCILDESCRIPTOR_H_
|
|
#define _METAL_MTLSTENCILDESCRIPTOR_H_
|
|
-
|
|
#import <Foundation/Foundation.h>
|
|
-
|
|
-@interface MTLStencilDescriptor : NSObject
|
|
+typedef NS_ENUM(NSUInteger, MTLCompareFunction) {
|
|
+ MTLCompareFunctionNever = 0,
|
|
+ MTLCompareFunctionLess = 1,
|
|
+ MTLCompareFunctionEqual = 2,
|
|
+ MTLCompareFunctionLessEqual = 3,
|
|
+ MTLCompareFunctionGreater = 4,
|
|
+ MTLCompareFunctionNotEqual = 5,
|
|
+ MTLCompareFunctionGreaterEqual = 6,
|
|
+ MTLCompareFunctionAlways = 7,
|
|
+};
|
|
+typedef NS_ENUM(NSUInteger, MTLStencilOperation) {
|
|
+ MTLStencilOperationKeep = 0,
|
|
+ MTLStencilOperationZero = 1,
|
|
+ MTLStencilOperationReplace = 2,
|
|
+ MTLStencilOperationIncrementClamp = 3,
|
|
+ MTLStencilOperationDecrementClamp = 4,
|
|
+ MTLStencilOperationInvert = 5,
|
|
+ MTLStencilOperationIncrementWrap = 6,
|
|
+ MTLStencilOperationDecrementWrap = 7,
|
|
+};
|
|
+@interface MTLStencilDescriptor : NSObject <NSCopying>
|
|
+@property(nonatomic) MTLCompareFunction stencilCompareFunction;
|
|
+@property(nonatomic) MTLStencilOperation stencilFailureOperation;
|
|
+@property(nonatomic) MTLStencilOperation depthFailureOperation;
|
|
+@property(nonatomic) MTLStencilOperation depthStencilPassOperation;
|
|
+@property(nonatomic) uint32_t readMask;
|
|
+@property(nonatomic) uint32_t writeMask;
|
|
@end
|
|
-
|
|
-#endif // _METAL_MTLSTENCILDESCRIPTOR_H_
|
|
+#endif
|
|
diff --git a/include/Metal/MTLTextureDescriptor.h b/include/Metal/MTLTextureDescriptor.h
|
|
index 959d552..02e147f 100644
|
|
--- a/include/Metal/MTLTextureDescriptor.h
|
|
+++ b/include/Metal/MTLTextureDescriptor.h
|
|
@@ -1,12 +1,22 @@
|
|
-// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
-
|
|
-#ifndef _METAL_MTLTEXTUREDESCRIPTOR_H_
|
|
-#define _METAL_MTLTEXTUREDESCRIPTOR_H_
|
|
-
|
|
-#import <Foundation/Foundation.h>
|
|
-
|
|
-@interface MTLTextureDescriptor : NSObject
|
|
+#pragma once
|
|
+#import <Metal/MTLTexture.h>
|
|
+@interface MTLTextureDescriptor : NSObject <NSCopying>
|
|
+@property MTLTextureType textureType;
|
|
+@property MTLPixelFormat pixelFormat;
|
|
+@property NSUInteger width;
|
|
+@property NSUInteger height;
|
|
+@property NSUInteger depth;
|
|
+@property NSUInteger mipmapLevelCount;
|
|
+@property NSUInteger sampleCount;
|
|
+@property NSUInteger arrayLength;
|
|
+@property MTLResourceOptions resourceOptions;
|
|
+@property BOOL allowGPUOptimizedContents;
|
|
+@property MTLTextureUsage usage;
|
|
+@property MTLTextureSwizzleChannels swizzle;
|
|
+@property MTLCPUCacheMode cpuCacheMode;
|
|
+@property MTLStorageMode storageMode;
|
|
+@property MTLHazardTrackingMode hazardTrackingMode;
|
|
++ (instancetype)texture2DDescriptorWithPixelFormat:(MTLPixelFormat)format width:(NSUInteger)width height:(NSUInteger)height mipmapped:(BOOL)mipmapped;
|
|
++ (instancetype)textureCubeDescriptorWithPixelFormat:(MTLPixelFormat)format size:(NSUInteger)size mipmapped:(BOOL)mipmapped;
|
|
@end
|
|
-
|
|
-#endif // _METAL_MTLTEXTUREDESCRIPTOR_H_
|
|
diff --git a/include/Metal/MTLVertexDescriptor.h b/include/Metal/MTLVertexDescriptor.h
|
|
index 2084d6f..366dfa7 100644
|
|
--- a/include/Metal/MTLVertexDescriptor.h
|
|
+++ b/include/Metal/MTLVertexDescriptor.h
|
|
@@ -3,10 +3,90 @@
|
|
|
|
#ifndef _METAL_MTLVERTEXDESCRIPTOR_H_
|
|
#define _METAL_MTLVERTEXDESCRIPTOR_H_
|
|
-
|
|
#import <Foundation/Foundation.h>
|
|
-
|
|
-@interface MTLVertexDescriptor : NSObject
|
|
+typedef NS_ENUM(NSUInteger, MTLVertexFormat) {
|
|
+ MTLVertexFormatInvalid = 0,
|
|
+ MTLVertexFormatUChar2 = 1,
|
|
+ MTLVertexFormatUChar3 = 2,
|
|
+ MTLVertexFormatUChar4 = 3,
|
|
+ MTLVertexFormatChar2 = 4,
|
|
+ MTLVertexFormatChar3 = 5,
|
|
+ MTLVertexFormatChar4 = 6,
|
|
+ MTLVertexFormatUChar2Normalized = 7,
|
|
+ MTLVertexFormatUChar3Normalized = 8,
|
|
+ MTLVertexFormatUChar4Normalized = 9,
|
|
+ MTLVertexFormatChar2Normalized = 10,
|
|
+ MTLVertexFormatChar3Normalized = 11,
|
|
+ MTLVertexFormatChar4Normalized = 12,
|
|
+ MTLVertexFormatUShort2 = 13,
|
|
+ MTLVertexFormatUShort3 = 14,
|
|
+ MTLVertexFormatUShort4 = 15,
|
|
+ MTLVertexFormatShort2 = 16,
|
|
+ MTLVertexFormatShort3 = 17,
|
|
+ MTLVertexFormatShort4 = 18,
|
|
+ MTLVertexFormatUShort2Normalized = 19,
|
|
+ MTLVertexFormatUShort3Normalized = 20,
|
|
+ MTLVertexFormatUShort4Normalized = 21,
|
|
+ MTLVertexFormatShort2Normalized = 22,
|
|
+ MTLVertexFormatShort3Normalized = 23,
|
|
+ MTLVertexFormatShort4Normalized = 24,
|
|
+ MTLVertexFormatHalf2 = 25,
|
|
+ MTLVertexFormatHalf3 = 26,
|
|
+ MTLVertexFormatHalf4 = 27,
|
|
+ MTLVertexFormatFloat = 28,
|
|
+ MTLVertexFormatFloat2 = 29,
|
|
+ MTLVertexFormatFloat3 = 30,
|
|
+ MTLVertexFormatFloat4 = 31,
|
|
+ MTLVertexFormatInt = 32,
|
|
+ MTLVertexFormatInt2 = 33,
|
|
+ MTLVertexFormatInt3 = 34,
|
|
+ MTLVertexFormatInt4 = 35,
|
|
+ MTLVertexFormatUInt = 36,
|
|
+ MTLVertexFormatUInt2 = 37,
|
|
+ MTLVertexFormatUInt3 = 38,
|
|
+ MTLVertexFormatUInt4 = 39,
|
|
+ MTLVertexFormatInt1010102Normalized = 40,
|
|
+ MTLVertexFormatUInt1010102Normalized = 41,
|
|
+ MTLVertexFormatUChar4Normalized_BGRA = 42,
|
|
+ MTLVertexFormatUChar = 45,
|
|
+ MTLVertexFormatChar = 46,
|
|
+ MTLVertexFormatUCharNormalized = 47,
|
|
+ MTLVertexFormatCharNormalized = 48,
|
|
+ MTLVertexFormatUShort = 49,
|
|
+ MTLVertexFormatShort = 50,
|
|
+ MTLVertexFormatUShortNormalized = 51,
|
|
+ MTLVertexFormatShortNormalized = 52,
|
|
+ MTLVertexFormatHalf = 53,
|
|
+};
|
|
+typedef NS_ENUM(NSUInteger, MTLVertexStepFunction) {
|
|
+ MTLVertexStepFunctionConstant = 0,
|
|
+ MTLVertexStepFunctionPerVertex = 1,
|
|
+ MTLVertexStepFunctionPerInstance = 2,
|
|
+ MTLVertexStepFunctionPerPatch = 3,
|
|
+ MTLVertexStepFunctionPerPatchControlPoint = 4,
|
|
+};
|
|
+@interface MTLVertexBufferLayoutDescriptor : NSObject <NSCopying>
|
|
+@property(nonatomic) NSUInteger stride;
|
|
+@property(nonatomic) MTLVertexStepFunction stepFunction;
|
|
+@property(nonatomic) NSUInteger stepRate;
|
|
@end
|
|
-
|
|
-#endif // _METAL_MTLVERTEXDESCRIPTOR_H_
|
|
+@interface MTLVertexBufferLayoutDescriptorArray : NSObject <NSCopying>
|
|
+- (MTLVertexBufferLayoutDescriptor*)objectAtIndexedSubscript:(NSUInteger)index;
|
|
+- (void)setObject:(MTLVertexBufferLayoutDescriptor*)descriptor atIndexedSubscript:(NSUInteger)index;
|
|
+@end
|
|
+@interface MTLVertexAttributeDescriptor : NSObject <NSCopying>
|
|
+@property(nonatomic) MTLVertexFormat format;
|
|
+@property(nonatomic) NSUInteger offset;
|
|
+@property(nonatomic) NSUInteger bufferIndex;
|
|
+@end
|
|
+@interface MTLVertexAttributeDescriptorArray : NSObject <NSCopying>
|
|
+- (MTLVertexAttributeDescriptor*)objectAtIndexedSubscript:(NSUInteger)index;
|
|
+- (void)setObject:(MTLVertexAttributeDescriptor*)descriptor atIndexedSubscript:(NSUInteger)index;
|
|
+@end
|
|
+@interface MTLVertexDescriptor : NSObject <NSCopying>
|
|
++ (instancetype)vertexDescriptor;
|
|
+@property(readonly) MTLVertexBufferLayoutDescriptorArray* layouts;
|
|
+@property(readonly) MTLVertexAttributeDescriptorArray* attributes;
|
|
+- (void)reset;
|
|
+@end
|
|
+#endif
|
|
diff --git a/include/Metal/Metal.h b/include/Metal/Metal.h
|
|
index 3c63a1d..a9c0dc7 100644
|
|
--- a/include/Metal/Metal.h
|
|
+++ b/include/Metal/Metal.h
|
|
@@ -5,6 +5,8 @@
|
|
#define _METAL_METAL_H_
|
|
|
|
#import <Metal/MTLBuffer.h>
|
|
+#import <Metal/MTLArgument.h>
|
|
+#import <Metal/MTLBlitCommandEncoder.h>
|
|
#import <Metal/MTLCommandBuffer.h>
|
|
#import <Metal/MTLCommandEncoder.h>
|
|
#import <Metal/MTLCommandQueue.h>
|
|
diff --git a/private-include/Metal/MTLBufferInternal.h b/private-include/Metal/MTLBufferInternal.h
|
|
index 0a620b2..79cf40a 100644
|
|
--- a/private-include/Metal/MTLBufferInternal.h
|
|
+++ b/private-include/Metal/MTLBufferInternal.h
|
|
@@ -13,7 +13,7 @@
|
|
@interface MTLBufferInternal : NSObject <MTLBuffer>
|
|
|
|
#if DARLING_METAL_ENABLED
|
|
-@property(readonly) std::shared_ptr<Indium::Buffer> buffer;
|
|
+@property(nonatomic, readonly) std::shared_ptr<Indium::Buffer> buffer;
|
|
|
|
- (instancetype)initWithBuffer: (std::shared_ptr<Indium::Buffer>)buffer
|
|
device: (id<MTLDevice>)device
|
|
@@ -21,5 +21,8 @@
|
|
#endif
|
|
|
|
@end
|
|
+// Direct access for the encoders: the property getter is a message send that returns the shared_ptr by value.
|
|
+const std::shared_ptr<Indium::Buffer>& MTLBufferGetIndium(id object);
|
|
+
|
|
|
|
#endif // _METAL_MTLBUFFERINTERNAL_H_
|
|
diff --git a/private-include/Metal/MTLRenderPipelineInternal.h b/private-include/Metal/MTLRenderPipelineInternal.h
|
|
index 2463e97..750a245 100644
|
|
--- a/private-include/Metal/MTLRenderPipelineInternal.h
|
|
+++ b/private-include/Metal/MTLRenderPipelineInternal.h
|
|
@@ -39,7 +39,7 @@ METAL_DECLARATIONS_BEGIN
|
|
@interface MTLRenderPipelineStateInternal : NSObject <MTLRenderPipelineState>
|
|
|
|
#if DARLING_METAL_ENABLED
|
|
-@property(readonly) std::shared_ptr<Indium::RenderPipelineState> state;
|
|
+@property(nonatomic, readonly) std::shared_ptr<Indium::RenderPipelineState> state;
|
|
|
|
- (instancetype)initWithState: (std::shared_ptr<Indium::RenderPipelineState>)state
|
|
device: (id<MTLDevice>)device
|
|
@@ -47,6 +47,9 @@ METAL_DECLARATIONS_BEGIN
|
|
#endif
|
|
|
|
@end
|
|
+// Direct access for the encoders: the property getter is a message send that returns the shared_ptr by value.
|
|
+const std::shared_ptr<Indium::RenderPipelineState>& MTLRenderPipelineGetIndium(id object);
|
|
+
|
|
|
|
METAL_DECLARATIONS_END
|
|
|
|
diff --git a/private-include/Metal/MTLTextureInternal.h b/private-include/Metal/MTLTextureInternal.h
|
|
index 09572bf..f8e2977 100644
|
|
--- a/private-include/Metal/MTLTextureInternal.h
|
|
+++ b/private-include/Metal/MTLTextureInternal.h
|
|
@@ -37,7 +37,7 @@ MTL_EXPORT
|
|
@interface MTLTextureInternal : NSObject <MTLTexture>
|
|
|
|
#if DARLING_METAL_ENABLED
|
|
-@property(readonly) std::shared_ptr<Indium::Texture> texture;
|
|
+@property(nonatomic, readonly) std::shared_ptr<Indium::Texture> texture;
|
|
|
|
- (instancetype)initWithTexture: (std::shared_ptr<Indium::Texture>)texture
|
|
device: (id<MTLDevice>)device
|
|
@@ -45,5 +45,8 @@ MTL_EXPORT
|
|
#endif
|
|
|
|
@end
|
|
+// Direct access for the encoders: the property getter is a message send that returns the shared_ptr by value.
|
|
+const std::shared_ptr<Indium::Texture>& MTLTextureGetIndium(id object);
|
|
+
|
|
|
|
#endif // _METAL_MTLTEXTUREINTERNAL_H_
|
|
diff --git a/src/Metal/MTLBuffer.mm b/src/Metal/MTLBuffer.mm
|
|
index e3382a8..1182427 100644
|
|
--- a/src/Metal/MTLBuffer.mm
|
|
+++ b/src/Metal/MTLBuffer.mm
|
|
@@ -11,6 +11,10 @@
|
|
#if DARLING_METAL_ENABLED
|
|
|
|
@synthesize buffer = _buffer;
|
|
+const std::shared_ptr<Indium::Buffer>& MTLBufferGetIndium(id object) {
|
|
+ static const std::shared_ptr<Indium::Buffer> none;
|
|
+ return object ? ((MTLBufferInternal*) object)->_buffer : none;
|
|
+}
|
|
@synthesize device = _device;
|
|
@synthesize resourceOptions = _resourceOptions;
|
|
@synthesize label = _label;
|
|
diff --git a/src/Metal/MTLCommandBuffer.mm b/src/Metal/MTLCommandBuffer.mm
|
|
index 7dedaa0..5eca991 100644
|
|
--- a/src/Metal/MTLCommandBuffer.mm
|
|
+++ b/src/Metal/MTLCommandBuffer.mm
|
|
@@ -1,7 +1,18 @@
|
|
// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
+#import <Metal/MTLTrace.h>
|
|
+#import <Metal/MTLTexture.h>
|
|
+#import <QuartzCore/CAMetalLayer.h>
|
|
+#include <algorithm>
|
|
+#include <atomic>
|
|
+#include <mutex>
|
|
+#include <string>
|
|
+#include <vector>
|
|
+#include <cstdio>
|
|
+#include <unistd.h>
|
|
#import <Metal/MTLCommandBufferInternal.h>
|
|
+#import <Metal/MTLBlitCommandEncoderInternal.h>
|
|
#import <Metal/MTLComputeCommandEncoderInternal.h>
|
|
#import <Metal/MTLDevice.h>
|
|
#import <Metal/MTLCommandQueue.h>
|
|
@@ -9,6 +20,199 @@
|
|
#import <Metal/stubs.h>
|
|
#import <Metal/MTLRenderCommandEncoderInternal.h>
|
|
|
|
+
|
|
+#if DARLING_METAL_ENABLED
|
|
+// Diagnostic (MTLDebugEnabled only): remember the most recent render pass of a command buffer, then once the
|
|
+// pass is finished read its first color target back and log how many draws it had and whether any pixel is lit.
|
|
+struct MTLDebugPass {
|
|
+ id<MTLCommandBuffer> commandBuffer = nil;
|
|
+ id<MTLTexture> texture = nil;
|
|
+ unsigned long drawsAtStart = 0;
|
|
+ unsigned long ordinal = 0;
|
|
+ unsigned long load = 0;
|
|
+};
|
|
+static thread_local MTLDebugPass debugPass;
|
|
+
|
|
+static void MTLDebugFlushPass(id<MTLCommandBuffer> commandBuffer)
|
|
+{
|
|
+ if (!debugPass.texture) {
|
|
+ return;
|
|
+ }
|
|
+ id<MTLTexture> texture = debugPass.texture;
|
|
+ debugPass.texture = nil;
|
|
+ if (debugPass.commandBuffer != commandBuffer) {
|
|
+ [texture release];
|
|
+ return;
|
|
+ }
|
|
+ unsigned long draws = MTLTraceDrawTotal() - debugPass.drawsAtStart;
|
|
+ unsigned long ordinal = debugPass.ordinal, load = debugPass.load;
|
|
+ NSUInteger width = texture.width, height = texture.height;
|
|
+ unsigned long format = texture.pixelFormat;
|
|
+ bool readable = (format == 80 || format == 81 || format == 70 || format == 71) && width >= 64;
|
|
+ static unsigned long logged = 0;
|
|
+ // Full-screen passes are logged only at the start; every other size keeps being logged (the 3D scene passes come later)
|
|
+ static unsigned long smallLogged = 0, fullLogged = 0;
|
|
+ bool full = width == 1920;
|
|
+ bool interestingSmall = !full && smallLogged < 400;
|
|
+ if (interestingSmall) ++smallLogged;
|
|
+ bool skipFull = full && ++fullLogged > 60;
|
|
+ if (!readable || skipFull || (ordinal > 400 && width < 1280 && !interestingSmall) || (!interestingSmall && ++logged > 600)) {
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ if (ordinal <= 400) {
|
|
+ fprintf(log, "pass-end #%lu %lux%lu fmt=%lu draws=%lu (not read)\n", ordinal, (unsigned long) width, (unsigned long) height, format, draws);
|
|
+ }
|
|
+ fclose(log);
|
|
+ }
|
|
+ [texture release];
|
|
+ return;
|
|
+ }
|
|
+ id<MTLBuffer> readback = [[commandBuffer device] newBufferWithLength: width * height * 4 options: MTLResourceStorageModeShared];
|
|
+ id<MTLBlitCommandEncoder> blit = [commandBuffer blitCommandEncoder];
|
|
+ [blit copyFromTexture: texture sourceSlice: 0 sourceLevel: 0 sourceOrigin: MTLOriginMake(0, 0, 0)
|
|
+ sourceSize: MTLSizeMake(width, height, 1) toBuffer: readback destinationOffset: 0
|
|
+ destinationBytesPerRow: width * 4 destinationBytesPerImage: width * height * 4];
|
|
+ [blit endEncoding];
|
|
+ [commandBuffer addCompletedHandler: ^(id<MTLCommandBuffer> finished) {
|
|
+ const uint8_t* bytes = (const uint8_t*) readback.contents;
|
|
+ unsigned long lit = 0, maxChannel = 0;
|
|
+ for (NSUInteger i = 0; i < width * height * 4; i += 4) {
|
|
+ unsigned char top = std::max(bytes[i], std::max(bytes[i + 1], bytes[i + 2]));
|
|
+ if (top) ++lit;
|
|
+ if (top > maxChannel) maxChannel = top;
|
|
+ }
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "pass-end #%lu %lux%lu fmt=%lu load=%lu draws=%lu litPixels=%lu max=%lu\n", ordinal,
|
|
+ (unsigned long) width, (unsigned long) height, format, load, draws, lit, maxChannel);
|
|
+ fclose(log);
|
|
+ }
|
|
+ [readback release];
|
|
+ [texture release];
|
|
+ }];
|
|
+}
|
|
+#endif
|
|
+
|
|
+#if DARLING_METAL_ENABLED
|
|
+// Diagnostic (/tmp/metal-debug-timing): remember the render passes of a command buffer and report them when it is slow.
|
|
+struct MTLTimingPass {
|
|
+ std::string description;
|
|
+ id encoder = nil;
|
|
+ unsigned long drawsAtStart = 0;
|
|
+ unsigned long draws = 0;
|
|
+};
|
|
+static thread_local std::vector<MTLTimingPass> timingPasses;
|
|
+static thread_local id timingCommandBuffer = nil;
|
|
+
|
|
+static void MTLTimingFinishPass()
|
|
+{
|
|
+ if (!timingPasses.empty() && timingPasses.back().encoder) {
|
|
+ MTLTimingPass& pass = timingPasses.back();
|
|
+ pass.draws = MTLTraceDrawTotal() - pass.drawsAtStart;
|
|
+ NSString* label = [(id<MTLRenderCommandEncoder>) pass.encoder label];
|
|
+ if (label) pass.description += std::string(" '") + [label UTF8String] + "'";
|
|
+ [pass.encoder release];
|
|
+ pass.encoder = nil;
|
|
+ }
|
|
+}
|
|
+#endif
|
|
+
|
|
+#if DARLING_METAL_ENABLED
|
|
+// Diagnostic snapshot: while /tmp/metal-snapshot exists, save the output of the render passes (as .ppm in the guest /tmp,
|
|
+// first 60 per process) so each pass of a frame can be looked at. Cheap while the flag file is absent.
|
|
+struct MTLSnapPass {
|
|
+ id<MTLCommandBuffer> commandBuffer = nil;
|
|
+ id<MTLTexture> texture = nil;
|
|
+ unsigned long drawsAtStart = 0;
|
|
+};
|
|
+static thread_local MTLSnapPass snapPass;
|
|
+
|
|
+// The first line of /tmp/metal-snapshot names the capture; a new name starts a new capture of up to 30 passes.
|
|
+static std::mutex snapTagLock;
|
|
+static std::string snapTag;
|
|
+static unsigned long snapSavedForTag = 0;
|
|
+
|
|
+static bool MTLSnapshotArmed()
|
|
+{
|
|
+ static std::atomic<unsigned long> calls { 0 };
|
|
+ static std::atomic<bool> armed { false };
|
|
+ // re-read a few times a second: this runs for every encoder and commit, and a guest fopen is several host syscalls
|
|
+ if (calls.fetch_add(1) % 2048 == 0) {
|
|
+ bool present = false;
|
|
+ if (FILE* file = fopen("/tmp/metal-snapshot", "r")) {
|
|
+ char text[48] = {};
|
|
+ if (fgets(text, sizeof(text), file)) {
|
|
+ std::string tag(text);
|
|
+ while (!tag.empty() && (tag.back() == '\n' || tag.back() == ' ')) tag.pop_back();
|
|
+ std::lock_guard<std::mutex> guard(snapTagLock);
|
|
+ if (tag != snapTag) {
|
|
+ snapTag = tag;
|
|
+ snapSavedForTag = 0;
|
|
+ }
|
|
+ }
|
|
+ present = true;
|
|
+ fclose(file);
|
|
+ }
|
|
+ armed = present;
|
|
+ }
|
|
+ return armed;
|
|
+}
|
|
+
|
|
+static void MTLSnapFlush(id<MTLCommandBuffer> commandBuffer)
|
|
+{
|
|
+ if (!snapPass.texture) {
|
|
+ return;
|
|
+ }
|
|
+ id<MTLTexture> texture = snapPass.texture;
|
|
+ snapPass.texture = nil;
|
|
+ unsigned long draws = MTLTraceDrawTotal() - snapPass.drawsAtStart;
|
|
+ NSUInteger width = texture.width, height = texture.height;
|
|
+ unsigned long format = texture.pixelFormat;
|
|
+ bool readable = (format == 80 || format == 81 || format == 70 || format == 71) && width >= 16 && width <= 2048 && snapPass.commandBuffer == commandBuffer;
|
|
+ std::string tag;
|
|
+ unsigned long number = 0;
|
|
+ {
|
|
+ std::lock_guard<std::mutex> guard(snapTagLock);
|
|
+ if (!readable || snapSavedForTag >= 30) {
|
|
+ readable = false;
|
|
+ } else {
|
|
+ number = snapSavedForTag++;
|
|
+ tag = snapTag;
|
|
+ }
|
|
+ }
|
|
+ if (!readable) {
|
|
+ [texture release];
|
|
+ return;
|
|
+ }
|
|
+ id<MTLBuffer> readback = [[commandBuffer device] newBufferWithLength: width * height * 4 options: MTLResourceStorageModeShared];
|
|
+ id<MTLBlitCommandEncoder> blit = [commandBuffer blitCommandEncoder];
|
|
+ [blit copyFromTexture: texture sourceSlice: 0 sourceLevel: 0 sourceOrigin: MTLOriginMake(0, 0, 0)
|
|
+ sourceSize: MTLSizeMake(width, height, 1) toBuffer: readback destinationOffset: 0
|
|
+ destinationBytesPerRow: width * 4 destinationBytesPerImage: width * height * 4];
|
|
+ [blit endEncoding];
|
|
+ [commandBuffer addCompletedHandler: ^(id<MTLCommandBuffer> finished) {
|
|
+ const uint8_t* bytes = (const uint8_t*) readback.contents;
|
|
+ char path[128];
|
|
+ snprintf(path, sizeof(path), "/tmp/snap-%s-%02lu-%lux%lu-draws%lu-tex%p.ppm", tag.c_str(), number, (unsigned long) width, (unsigned long) height, draws, (void*) texture);
|
|
+ if (FILE* out = fopen(path, "wb")) {
|
|
+ fprintf(out, "P6\n%lu %lu\n255\n", (unsigned long) width, (unsigned long) height);
|
|
+ bool bgra = format == 80 || format == 81;
|
|
+ std::vector<uint8_t> row(width * 3);
|
|
+ for (NSUInteger y = 0; y < height; ++y) {
|
|
+ for (NSUInteger x = 0; x < width; ++x) {
|
|
+ const uint8_t* px = bytes + (y * width + x) * 4;
|
|
+ row[x * 3 + 0] = bgra ? px[2] : px[0];
|
|
+ row[x * 3 + 1] = px[1];
|
|
+ row[x * 3 + 2] = bgra ? px[0] : px[2];
|
|
+ }
|
|
+ fwrite(row.data(), 1, row.size(), out);
|
|
+ }
|
|
+ fclose(out);
|
|
+ }
|
|
+ [readback release];
|
|
+ [texture release];
|
|
+ }];
|
|
+}
|
|
+#endif
|
|
+
|
|
#if DARLING_METAL_ENABLED
|
|
// used to take care of RR while passing the block around in C++ code
|
|
struct MTLCommandBufferHandlerWrapper {
|
|
@@ -65,7 +269,7 @@ struct MTLCommandBufferHandlerWrapper {
|
|
if (self != nil) {
|
|
_commandBuffer = commandBuffer;
|
|
_device = [commandQueue.device retain];
|
|
- _commandQueue = [_commandQueue retain];
|
|
+ _commandQueue = [commandQueue retain];
|
|
}
|
|
return self;
|
|
}
|
|
@@ -78,6 +282,13 @@ struct MTLCommandBufferHandlerWrapper {
|
|
[super dealloc];
|
|
}
|
|
|
|
+- (id<MTLBlitCommandEncoder>)blitCommandEncoder
|
|
+{
|
|
+ auto encoder = _commandBuffer->blitCommandEncoder();
|
|
+ if (!encoder) return nil;
|
|
+ return [[[MTLBlitCommandEncoderInternal alloc] initWithEncoder:encoder device:_device] autorelease];
|
|
+}
|
|
+
|
|
- (id<MTLComputeCommandEncoder>)computeCommandEncoderWithDescriptor: (MTLComputePassDescriptor*)computePassDescriptor
|
|
{
|
|
auto encoder = _commandBuffer->computeCommandEncoder(computePassDescriptor.asIndiumDescriptor);
|
|
@@ -107,11 +318,91 @@ struct MTLCommandBufferHandlerWrapper {
|
|
|
|
- (id<MTLRenderCommandEncoder>)renderCommandEncoderWithDescriptor: (MTLRenderPassDescriptor*)renderPassDescriptor
|
|
{
|
|
+ MTLTraceCount("renderPass");
|
|
+ if (MTLDebugEnabled()) {
|
|
+ MTLRenderPassColorAttachmentDescriptor* sizeColor = [renderPassDescriptor.colorAttachments objectAtIndexedSubscript: 0];
|
|
+ char name[64];
|
|
+ snprintf(name, sizeof(name), "pass_%lux%lu_f%lu", sizeColor.texture ? (unsigned long) sizeColor.texture.width : 0UL, sizeColor.texture ? (unsigned long) sizeColor.texture.height : 0UL, sizeColor.texture ? (unsigned long) sizeColor.texture.pixelFormat : 0UL);
|
|
+ MTLTraceCount(name);
|
|
+ }
|
|
+ if (MTLDebugEnabled()) {
|
|
+ MTLDebugFlushPass(self);
|
|
+ }
|
|
+ // Diagnostic: with /tmp/metal-debug-magenta present, every pass clears its first color target to magenta
|
|
+ if (MTLDebugEnabled() && access("/tmp/metal-debug-magenta", F_OK) == 0) {
|
|
+ MTLRenderPassColorAttachmentDescriptor* debugColor = [renderPassDescriptor.colorAttachments objectAtIndexedSubscript: 0];
|
|
+ if (debugColor.texture && debugColor.loadAction == MTLLoadActionClear) {
|
|
+ debugColor.clearColor = MTLClearColorMake(1, 0, 1, 1);
|
|
+ }
|
|
+ }
|
|
+ {
|
|
+ // Diagnostic: describe the first render passes (and every 500th) in the trace log
|
|
+ static unsigned long passes = 0;
|
|
+ ++passes;
|
|
+ if (MTLDebugEnabled() && (passes <= 40 || passes % 500 == 0)) {
|
|
+ MTLRenderPassColorAttachmentDescriptor* color = [renderPassDescriptor.colorAttachments objectAtIndexedSubscript: 0];
|
|
+ id<MTLTexture> texture = color.texture;
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "pass #%lu color0: tex=%s %lux%lu fmt=%lu load=%lu store=%lu clear=(%.2f %.2f %.2f %.2f) depth=%d\n", passes,
|
|
+ texture ? "yes" : "none", texture ? (unsigned long) texture.width : 0UL, texture ? (unsigned long) texture.height : 0UL,
|
|
+ texture ? (unsigned long) texture.pixelFormat : 0UL, (unsigned long) color.loadAction, (unsigned long) color.storeAction,
|
|
+ color.clearColor.red, color.clearColor.green, color.clearColor.blue, color.clearColor.alpha,
|
|
+ renderPassDescriptor.depthAttachment.texture ? 1 : 0);
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
auto encoder = _commandBuffer->renderCommandEncoder([renderPassDescriptor asIndiumDescriptor]);
|
|
if (!encoder) {
|
|
return nil;
|
|
}
|
|
- return [[[MTLRenderCommandEncoderInternal alloc] initWithEncoder: encoder device: _device] autorelease];
|
|
+ if (MTLDebugEnabled()) {
|
|
+ MTLRenderPassColorAttachmentDescriptor* color = [renderPassDescriptor.colorAttachments objectAtIndexedSubscript: 0];
|
|
+ static unsigned long ordinal = 0;
|
|
+ debugPass.commandBuffer = self;
|
|
+ debugPass.texture = [color.texture retain];
|
|
+ debugPass.drawsAtStart = MTLTraceDrawTotal();
|
|
+ debugPass.ordinal = ++ordinal;
|
|
+ debugPass.load = (unsigned long) color.loadAction;
|
|
+ }
|
|
+ id<MTLRenderCommandEncoder> result = [[[MTLRenderCommandEncoderInternal alloc] initWithEncoder: encoder device: _device] autorelease];
|
|
+ if (MTLTimingEnabled()) {
|
|
+ MTLTimingFinishPass();
|
|
+ if (timingCommandBuffer != self) {
|
|
+ timingPasses.clear();
|
|
+ timingCommandBuffer = self;
|
|
+ }
|
|
+ MTLRenderPassColorAttachmentDescriptor* color = [renderPassDescriptor.colorAttachments objectAtIndexedSubscript: 0];
|
|
+ char text[160];
|
|
+ snprintf(text, sizeof(text), "render %lux%lu fmt=%lu load=%lu depth=%d", color.texture ? (unsigned long) color.texture.width : 0UL, color.texture ? (unsigned long) color.texture.height : 0UL,
|
|
+ color.texture ? (unsigned long) color.texture.pixelFormat : 0UL, (unsigned long) color.loadAction, renderPassDescriptor.depthAttachment.texture ? 1 : 0);
|
|
+ {
|
|
+ char sizeName[64];
|
|
+ snprintf(sizeName, sizeof(sizeName), "pass_%lux%lu_f%lu_load%lu", color.texture ? (unsigned long) color.texture.width : 0UL, color.texture ? (unsigned long) color.texture.height : 0UL,
|
|
+ color.texture ? (unsigned long) color.texture.pixelFormat : 0UL, (unsigned long) color.loadAction);
|
|
+ MTLTraceTiming(sizeName, 0);
|
|
+ }
|
|
+ MTLTimingPass pass;
|
|
+ pass.description = text;
|
|
+ pass.encoder = [result retain];
|
|
+ pass.drawsAtStart = MTLTraceDrawTotal();
|
|
+ timingPasses.push_back(pass);
|
|
+ }
|
|
+ if (MTLSnapshotArmed()) {
|
|
+ MTLSnapFlush(self);
|
|
+ MTLRenderPassColorAttachmentDescriptor* snapColor = [renderPassDescriptor.colorAttachments objectAtIndexedSubscript: 0];
|
|
+ if (snapColor.texture) {
|
|
+ snapPass.commandBuffer = self;
|
|
+ snapPass.texture = [snapColor.texture retain];
|
|
+ snapPass.drawsAtStart = MTLTraceDrawTotal();
|
|
+ }
|
|
+ }
|
|
+ return result;
|
|
+}
|
|
+
|
|
+- (void)addScheduledHandler:(MTLCommandBufferHandler)block
|
|
+{
|
|
+ _commandBuffer->addScheduledHandler(MTLCommandBufferHandlerWrapper(block, self));
|
|
}
|
|
|
|
- (void)addCompletedHandler: (MTLCommandBufferHandler)block
|
|
@@ -121,17 +412,107 @@ struct MTLCommandBufferHandlerWrapper {
|
|
|
|
- (void)waitUntilCompleted
|
|
{
|
|
+ double waitStart = MTLTimingEnabled() ? MTLTraceNow() : 0;
|
|
_commandBuffer->waitUntilCompleted();
|
|
+ if (MTLTimingEnabled()) {
|
|
+ MTLTraceTiming("waitUntilCompleted", MTLTraceNow() - waitStart);
|
|
+ }
|
|
}
|
|
|
|
- (void)presentDrawable: (id<MTLDrawable>)drawable
|
|
{
|
|
+ if (MTLDebugEnabled()) {
|
|
+ static unsigned long presents = 0;
|
|
+ id drawableTexture = [(id)drawable respondsToSelector: @selector(texture)] ? [(id<CAMetalDrawable>)drawable texture] : nil;
|
|
+ if (++presents <= 40) {
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "present #%lu lastPassTexture=%p drawableTexture=%p same=%d\n", presents, debugPass.texture, drawableTexture, debugPass.texture == drawableTexture);
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+ MTLDebugFlushPass(self);
|
|
+ }
|
|
+ // Diagnostic: with /tmp/metal-debug-present-magenta present, paint the drawable magenta right before it is
|
|
+ // presented, to tell a broken presentation path from content that is genuinely black.
|
|
+ if (MTLDebugEnabled() && access("/tmp/metal-debug-present-magenta", F_OK) == 0 && [(id)drawable respondsToSelector: @selector(texture)]) {
|
|
+ MTLRenderPassDescriptor* debugPass = [MTLRenderPassDescriptor renderPassDescriptor];
|
|
+ MTLRenderPassColorAttachmentDescriptor* debugColor = [debugPass.colorAttachments objectAtIndexedSubscript: 0];
|
|
+ debugColor.texture = [(id<CAMetalDrawable>)drawable texture];
|
|
+ debugColor.loadAction = MTLLoadActionClear;
|
|
+ debugColor.storeAction = MTLStoreActionStore;
|
|
+ debugColor.clearColor = MTLClearColorMake(1, 0, 1, 1);
|
|
+ id<MTLRenderCommandEncoder> debugEncoder = [self renderCommandEncoderWithDescriptor: debugPass];
|
|
+ [debugEncoder endEncoding];
|
|
+
|
|
+ // ... and read the drawable back on the Vulkan side, to see what it holds before it crosses to GL
|
|
+ static unsigned long debugPresents = 0;
|
|
+ ++debugPresents;
|
|
+ if (debugPresents <= 5 || debugPresents % 200 == 0) {
|
|
+ id<MTLTexture> texture = debugColor.texture;
|
|
+ NSUInteger width = texture.width, height = texture.height;
|
|
+ id<MTLBuffer> readback = [[self device] newBufferWithLength: width * height * 4 options: MTLResourceStorageModeShared];
|
|
+ id<MTLBlitCommandEncoder> debugBlit = [self blitCommandEncoder];
|
|
+ [debugBlit copyFromTexture: texture sourceSlice: 0 sourceLevel: 0 sourceOrigin: MTLOriginMake(0, 0, 0)
|
|
+ sourceSize: MTLSizeMake(width, height, 1) toBuffer: readback destinationOffset: 0
|
|
+ destinationBytesPerRow: width * 4 destinationBytesPerImage: width * height * 4];
|
|
+ [debugBlit endEncoding];
|
|
+ unsigned long ordinal = debugPresents;
|
|
+ [self addCompletedHandler: ^(id<MTLCommandBuffer> finished) {
|
|
+ const uint8_t* bytes = (const uint8_t*) readback.contents;
|
|
+ unsigned long nonZero = 0;
|
|
+ for (NSUInteger i = 0; i < width * height * 4; i += 4) {
|
|
+ if (bytes[i] | bytes[i + 1] | bytes[i + 2]) ++nonZero;
|
|
+ }
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "vulkan-readback #%lu %lux%lu nonZeroPixels=%lu first=(%u %u %u %u) fmt=%lu\n", ordinal,
|
|
+ (unsigned long) width, (unsigned long) height, nonZero, bytes[0], bytes[1], bytes[2], bytes[3], (unsigned long) texture.pixelFormat);
|
|
+ fclose(log);
|
|
+ }
|
|
+ [readback release];
|
|
+ }];
|
|
+ }
|
|
+ }
|
|
_commandBuffer->presentDrawable(((id<MTLDrawableInternal>)drawable).drawable);
|
|
}
|
|
|
|
- (void)commit
|
|
{
|
|
+ MTLTraceCount("commit");
|
|
+ if (MTLSnapshotArmed()) {
|
|
+ MTLSnapFlush(self);
|
|
+ }
|
|
+ double commitStart = MTLTimingEnabled() ? MTLTraceNow() : 0;
|
|
+ if (MTLTimingEnabled()) {
|
|
+ std::string report;
|
|
+ if (timingCommandBuffer == self) {
|
|
+ MTLTimingFinishPass();
|
|
+ for (const auto& pass: timingPasses) {
|
|
+ char draws[32];
|
|
+ snprintf(draws, sizeof(draws), " draws=%lu; ", pass.draws);
|
|
+ report += pass.description + draws;
|
|
+ }
|
|
+ timingPasses.clear();
|
|
+ timingCommandBuffer = nil;
|
|
+ }
|
|
+ _commandBuffer->addCompletedHandler(MTLCommandBufferHandlerWrapper(^(id<MTLCommandBuffer> finished) {
|
|
+ double elapsed = MTLTraceNow() - commitStart;
|
|
+ MTLTraceTiming("gpuCommitToComplete", elapsed);
|
|
+ static std::atomic<int> reported { 0 };
|
|
+ if (elapsed > 0.3 && reported.fetch_add(1) < 40) {
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "slow-cmdbuf %.0fms label=%s passes: %s\n", elapsed * 1000, [[finished label] UTF8String] ? [[finished label] UTF8String] : "-", report.c_str());
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+ }, self));
|
|
+ }
|
|
+ if (MTLDebugEnabled()) {
|
|
+ MTLDebugFlushPass(self);
|
|
+ }
|
|
_commandBuffer->commit();
|
|
+ if (MTLTimingEnabled()) {
|
|
+ MTLTraceTiming("commitCall", MTLTraceNow() - commitStart);
|
|
+ }
|
|
}
|
|
|
|
#else
|
|
diff --git a/src/Metal/MTLComputeCommandEncoder.mm b/src/Metal/MTLComputeCommandEncoder.mm
|
|
index 208a0e9..b13ad20 100644
|
|
--- a/src/Metal/MTLComputeCommandEncoder.mm
|
|
+++ b/src/Metal/MTLComputeCommandEncoder.mm
|
|
@@ -1,3 +1,6 @@
|
|
+#import <Metal/MTLTrace.h>
|
|
+#import <Metal/MTLTextureInternal.h>
|
|
+#import <Metal/MTLSamplerStateInternal.h>
|
|
// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
@@ -196,6 +199,15 @@ MTL_UNSUPPORTED_CLASS
|
|
[super dealloc];
|
|
}
|
|
|
|
+- (void)setTexture:(id<MTLTexture>)texture atIndex:(NSUInteger)index {
|
|
+ _encoder->setTexture(((MTLTextureInternal*)texture).texture, index);
|
|
+}
|
|
+- (void)setSamplerState:(id<MTLSamplerState>)sampler atIndex:(NSUInteger)index {
|
|
+ _encoder->setSamplerState(((MTLSamplerStateInternal*)sampler).sampler, index);
|
|
+}
|
|
+- (void)setSamplerState:(id<MTLSamplerState>)sampler lodMinClamp:(float)lodMinClamp lodMaxClamp:(float)lodMaxClamp atIndex:(NSUInteger)index {
|
|
+ _encoder->setSamplerState(((MTLSamplerStateInternal*)sampler).sampler, lodMinClamp, lodMaxClamp, index);
|
|
+}
|
|
- (void)endEncoding
|
|
{
|
|
_encoder->endEncoding();
|
|
@@ -249,6 +261,16 @@ MTL_UNSUPPORTED_CLASS
|
|
- (void)dispatchThreadgroups: (MTLSize)threadgroupsPerGrid
|
|
threadsPerThreadgroup: (MTLSize)threadsPerThreadgroup
|
|
{
|
|
+ MTLTraceCount("dispatch");
|
|
+ if (MTLTimingEnabled() || MTLDebugEnabled()) {
|
|
+ static unsigned long logged = 0;
|
|
+ if (++logged <= 40) {
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "dispatch #%lu groups=(%lu %lu %lu) threads=(%lu %lu %lu)\n", logged, (unsigned long) threadgroupsPerGrid.width, (unsigned long) threadgroupsPerGrid.height, (unsigned long) threadgroupsPerGrid.depth, (unsigned long) threadsPerThreadgroup.width, (unsigned long) threadsPerThreadgroup.height, (unsigned long) threadsPerThreadgroup.depth);
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
_encoder->dispatchThreadgroups(MTLSizeToIndium(threadgroupsPerGrid), MTLSizeToIndium(threadsPerThreadgroup));
|
|
}
|
|
|
|
diff --git a/src/Metal/MTLDepthStencilDescriptor.mm b/src/Metal/MTLDepthStencilDescriptor.mm
|
|
index 2a57796..0cbedf2 100644
|
|
--- a/src/Metal/MTLDepthStencilDescriptor.mm
|
|
+++ b/src/Metal/MTLDepthStencilDescriptor.mm
|
|
@@ -1,24 +1,58 @@
|
|
// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
-#import <Metal/MTLDepthStencilDescriptor.h>
|
|
-#import <Foundation/NSMethodSignature.h>
|
|
-#import <Foundation/NSInvocation.h>
|
|
-
|
|
+#import <Metal/MTLDepthStencilInternal.h>
|
|
+#import <Metal/MTLDevice.h>
|
|
+#import <Metal/stubs.h>
|
|
@implementation MTLDepthStencilDescriptor
|
|
-
|
|
+@synthesize depthCompareFunction = _depthCompareFunction;
|
|
+@synthesize depthWriteEnabled = _depthWriteEnabled;
|
|
+@synthesize frontFaceStencil = _frontFaceStencil;
|
|
+@synthesize backFaceStencil = _backFaceStencil;
|
|
+@synthesize label = _label;
|
|
+- (instancetype)init {
|
|
+ self = [super init];
|
|
+ if (self) _depthCompareFunction = MTLCompareFunctionAlways;
|
|
+ return self;
|
|
+}
|
|
+- (void)dealloc { [_frontFaceStencil release]; [_backFaceStencil release]; [_label release]; [super dealloc]; }
|
|
+- (id)copyWithZone:(NSZone*)zone {
|
|
+ MTLDepthStencilDescriptor* copy = [[[self class] allocWithZone:zone] init];
|
|
+ copy.depthCompareFunction = _depthCompareFunction;
|
|
+ copy.depthWriteEnabled = _depthWriteEnabled;
|
|
+ copy.frontFaceStencil = _frontFaceStencil;
|
|
+ copy.backFaceStencil = _backFaceStencil;
|
|
+ copy.label = _label;
|
|
+ return copy;
|
|
+}
|
|
#if DARLING_METAL_ENABLED
|
|
-
|
|
-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
|
|
-{
|
|
- return [NSMethodSignature signatureWithObjCTypes: "v@:"];
|
|
+- (Indium::DepthStencilDescriptor)asIndiumDescriptor {
|
|
+ Indium::DepthStencilDescriptor result;
|
|
+ result.depthCompareFunction = static_cast<Indium::CompareFunction>(_depthCompareFunction);
|
|
+ result.depthWriteEnabled = _depthWriteEnabled;
|
|
+ if (_frontFaceStencil) result.frontFaceStencil = [_frontFaceStencil asIndiumDescriptor];
|
|
+ if (_backFaceStencil) result.backFaceStencil = [_backFaceStencil asIndiumDescriptor];
|
|
+ return result;
|
|
}
|
|
-
|
|
-- (void)forwardInvocation:(NSInvocation *)anInvocation
|
|
-{
|
|
- NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]);
|
|
+#endif
|
|
+@end
|
|
+@implementation MTLDepthStencilStateInternal
|
|
+#if DARLING_METAL_ENABLED
|
|
+@synthesize state = _state;
|
|
+const std::shared_ptr<Indium::DepthStencilState>& MTLDepthStencilGetIndium(id object) {
|
|
+ static const std::shared_ptr<Indium::DepthStencilState> none;
|
|
+ return object ? ((MTLDepthStencilStateInternal*) object)->_state : none;
|
|
}
|
|
-
|
|
+@synthesize device = _device;
|
|
+@synthesize label = _label;
|
|
+- (instancetype)initWithState:(std::shared_ptr<Indium::DepthStencilState>)state device:(id<MTLDevice>)device label:(NSString*)label {
|
|
+ self = [super init];
|
|
+ if (self) { _state = state; _device = [device retain]; _label = [label copy]; }
|
|
+ return self;
|
|
+}
|
|
+- (void)dealloc { [_device release]; [_label release]; [super dealloc]; }
|
|
+#else
|
|
+@dynamic device, label;
|
|
+MTL_UNSUPPORTED_CLASS
|
|
#endif
|
|
-
|
|
@end
|
|
diff --git a/src/Metal/MTLDevice.mm b/src/Metal/MTLDevice.mm
|
|
index f58184a..ed918ed 100644
|
|
--- a/src/Metal/MTLDevice.mm
|
|
+++ b/src/Metal/MTLDevice.mm
|
|
@@ -2,14 +2,32 @@
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
#import <Metal/MTLDeviceInternal.h>
|
|
+#import <Metal/MTLArgumentInternal.h>
|
|
+#import <Metal/MTLDepthStencilInternal.h>
|
|
#import <dispatch/dispatch.h>
|
|
#import <Metal/MTLLibraryInternal.h>
|
|
#import <Metal/MTLComputePipelineInternal.h>
|
|
#import <Metal/MTLCommandQueueInternal.h>
|
|
#import <Metal/MTLBufferInternal.h>
|
|
+#import <Metal/MTLSamplerStateInternal.h>
|
|
+#import <Metal/MTLTextureInternal.h>
|
|
+#import <Metal/MTLTextureDescriptor.h>
|
|
#import <Metal/MTLLibraryInternal.h>
|
|
#import <Metal/stubs.h>
|
|
+#import <Metal/MTLExceptionBridge.h>
|
|
+#import <Metal/MTLTrace.h>
|
|
+#include <string>
|
|
+#include <mutex>
|
|
+#include <unordered_map>
|
|
+#include <exception>
|
|
+#import <Metal/MTLVertexDescriptor.h>
|
|
+#include <type_traits>
|
|
#import <Metal/MTLRenderPipelineInternal.h>
|
|
+#import <Metal/MTLDeviceInfo.h>
|
|
+#if DARLING_METAL_ENABLED
|
|
+#include <indium/device.private.hpp>
|
|
+#include <cstdlib>
|
|
+#endif
|
|
|
|
MTL_EXTERN const MTLDeviceNotificationName MTLDeviceWasAddedNotification = @"MTLDeviceWasAdded";
|
|
MTL_EXTERN const MTLDeviceNotificationName MTLDeviceRemovalRequestedNotification = @"MTLDeviceRemovalRequested";
|
|
@@ -88,6 +106,37 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
|
[observer release];
|
|
};
|
|
|
|
+
|
|
+#if DARLING_METAL_ENABLED
|
|
+// Diagnostic: report pipeline creation failures (the game just skips the work when a pipeline is nil).
|
|
+static void MTLDebugPipelineFailure(const char* what)
|
|
+{
|
|
+ if (!(MTLDebugEnabled() || MTLTimingEnabled())) {
|
|
+ return;
|
|
+ }
|
|
+ std::string message = "unknown";
|
|
+ try { throw; } catch (const std::exception& e) { message = e.what(); } catch (...) {}
|
|
+ MTLTraceCount(what);
|
|
+ static unsigned long logged = 0;
|
|
+ if (++logged <= 80) {
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "pipeline-failure %s: %s\n", what, message.c_str());
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+}
|
|
+#endif
|
|
+
|
|
+#if DARLING_METAL_ENABLED
|
|
+std::mutex& MTLDebugReflectionLock() { static std::mutex lock; return lock; }
|
|
+std::unordered_map<void*, std::string>& MTLDebugReflectionMap() { static std::unordered_map<void*, std::string> map; return map; }
|
|
+std::string MTLDebugReflectionFor(void* pso)
|
|
+{
|
|
+ std::lock_guard<std::mutex> guard(MTLDebugReflectionLock());
|
|
+ auto it = MTLDebugReflectionMap().find(pso);
|
|
+ return it == MTLDebugReflectionMap().end() ? std::string("(none)") : it->second;
|
|
+}
|
|
+#endif
|
|
@implementation MTLDeviceInternal
|
|
|
|
#if DARLING_METAL_ENABLED
|
|
@@ -96,6 +145,9 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
|
NSThread* _pollingThread;
|
|
NSCondition* _threadExitCondition;
|
|
BOOL _threadIsRunning;
|
|
+ NSString* _name;
|
|
+ uint64_t _workingSetSize;
|
|
+ BOOL _sharesSystemMemory;
|
|
}
|
|
|
|
@synthesize device = _device;
|
|
@@ -116,6 +168,44 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
|
[_threadExitCondition unlock];
|
|
}
|
|
|
|
+// Real host GPU facts, computed once per device in -initWithDevice: (games may query them per frame).
|
|
+- (void)loadHostDeviceInfo
|
|
+{
|
|
+ // Overrides are read once per process.
|
|
+ static const char* const nameOverride = getenv("DARLING_METAL_GPU_NAME");
|
|
+ static const uint64_t vramOverride = MTLDeviceInfo::parseMegabytes(getenv("DARLING_METAL_VRAM_MB"));
|
|
+
|
|
+ std::string vulkanName;
|
|
+ try {
|
|
+ vulkanName = _device ? _device->name() : "";
|
|
+ } catch (...) {
|
|
+ vulkanName = "";
|
|
+ }
|
|
+ _name = [[NSString alloc] initWithUTF8String: MTLDeviceInfo::resolveName(nameOverride, vulkanName.c_str()).c_str()];
|
|
+ if (_name == nil) {
|
|
+ _name = [[NSString alloc] initWithUTF8String: MTLDeviceInfo::kFallbackName];
|
|
+ }
|
|
+
|
|
+ uint64_t workingSet = 0;
|
|
+ _sharesSystemMemory = NO;
|
|
+ if (auto priv = dynamic_cast<Indium::PrivateDevice*>(_device.get())) {
|
|
+ const VkPhysicalDeviceType type = priv->properties().deviceType;
|
|
+ _sharesSystemMemory = type == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU || type == VK_PHYSICAL_DEVICE_TYPE_CPU;
|
|
+ const VkPhysicalDeviceMemoryProperties memory = priv->memoryProperties();
|
|
+ MTLDeviceInfo::Heap heaps[VK_MAX_MEMORY_HEAPS];
|
|
+ const size_t count = memory.memoryHeapCount < VK_MAX_MEMORY_HEAPS ? memory.memoryHeapCount : VK_MAX_MEMORY_HEAPS;
|
|
+ for (size_t i = 0; i < count; ++i) {
|
|
+ heaps[i] = { memory.memoryHeaps[i].size, (memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 };
|
|
+ }
|
|
+ workingSet = MTLDeviceInfo::workingSetBytes(heaps, count, _sharesSystemMemory);
|
|
+ }
|
|
+ if (vramOverride != 0) {
|
|
+ workingSet = vramOverride;
|
|
+ }
|
|
+ // 1 GiB was the previous fixed value; keep it when Vulkan reports nothing usable.
|
|
+ _workingSetSize = workingSet != 0 ? workingSet : (1ull << 30);
|
|
+}
|
|
+
|
|
- (instancetype)initWithDevice: (std::shared_ptr<Indium::Device>)device
|
|
{
|
|
self = [super init];
|
|
@@ -123,6 +213,7 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
|
_device = device;
|
|
_threadExitCondition = [NSCondition new];
|
|
_threadIsRunning = NO;
|
|
+ [self loadHostDeviceInfo];
|
|
_pollingThread = [[NSThread alloc] initWithTarget: self selector: @selector(pollingLoop) object: nil];
|
|
[_pollingThread start];
|
|
}
|
|
@@ -133,6 +224,7 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
|
{
|
|
[_pollingThread release];
|
|
[_threadExitCondition release];
|
|
+ [_name release];
|
|
|
|
[super dealloc];
|
|
}
|
|
@@ -153,12 +245,60 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
|
[_threadExitCondition unlock];
|
|
}
|
|
|
|
+- (MTLReadWriteTextureTier)readWriteTextureSupport
|
|
+{
|
|
+ /* darling-lol: ANGLE queries this during GPU init; it was missing
|
|
+ * (unrecognized selector -> terminate). Report Tier2; TierNone made
|
|
+ * ANGLE refuse context creation outright (eglCreateContext failure ->
|
|
+ * GPU exit(1) -> browser FATAL). If Indium lacks something ANGLE then
|
|
+ * uses, that will surface as the next concrete gap instead. */
|
|
+ return MTLReadWriteTextureTier2;
|
|
+}
|
|
+
|
|
+/* darling-lol M57: feature queries ANGLE's Metal backend needs. */
|
|
+- (BOOL)isDepth24Stencil8PixelFormatSupported { return _device->supportsDepth24Stencil8PixelFormat(); }
|
|
+- (BOOL)hasUnifiedMemory
|
|
+{
|
|
+ // Stays YES by default: with NO, ANGLE and games take the Managed/Private paths
|
|
+ // (synchronizeResource:, Private uploads) that this Metal/Indium does not fully implement.
|
|
+ // DARLING_METAL_HOST_UNIFIED_MEMORY=1 reports the host GPU type instead
|
|
+ // (YES for integrated and CPU devices, NO for discrete ones).
|
|
+ static const bool reportHost = [] {
|
|
+ const char* value = getenv("DARLING_METAL_HOST_UNIFIED_MEMORY");
|
|
+ return value && value[0] == '1' && value[1] == '\0';
|
|
+ }();
|
|
+ return reportHost ? _sharesSystemMemory : YES;
|
|
+}
|
|
+- (NSUInteger)maxBufferLength { return 256u * 1024u * 1024u; }
|
|
+- (NSUInteger)recommendedMaxWorkingSetSize
|
|
+{
|
|
+ return _workingSetSize > NSUIntegerMax ? NSUIntegerMax : static_cast<NSUInteger>(_workingSetSize);
|
|
+}
|
|
+- (BOOL)isLowPower { return NO; }
|
|
+- (BOOL)isRemovable { return NO; }
|
|
+- (BOOL)isHeadless { return NO; }
|
|
+- (NSString*)name { return _name; }
|
|
+- (uint64_t)registryID { return MTLDeviceInfo::kDefaultRegistryID; }
|
|
+- (BOOL)supportsFamily: (NSUInteger)family { return YES; }
|
|
+- (BOOL)supportsFeatureSet: (NSUInteger)featureSet { return YES; }
|
|
+- (BOOL)supportsTextureSampleCount: (NSUInteger)sampleCount { return sampleCount <= 4; }
|
|
+- (id<MTLCommandQueue>)newCommandQueueWithMaxCommandBufferCount: (NSUInteger)maxCommandBufferCount
|
|
+{
|
|
+ return [self newCommandQueue];
|
|
+}
|
|
+
|
|
- (id<MTLComputePipelineState>)newComputePipelineStateWithDescriptor: (MTLComputePipelineDescriptor*)descriptor
|
|
options: (MTLPipelineOption)options
|
|
reflection: (MTLAutoreleasedComputePipelineReflection*)reflection
|
|
error: (NSError**)error
|
|
{
|
|
- auto pso = _device->newComputePipelineState(descriptor.asIndiumDescriptor, static_cast<Indium::PipelineOption>(options), nullptr);
|
|
+ std::decay_t<decltype(_device->newComputePipelineState(descriptor.asIndiumDescriptor, static_cast<Indium::PipelineOption>(options), nullptr))> pso;
|
|
+ try { pso = _device->newComputePipelineState(descriptor.asIndiumDescriptor, static_cast<Indium::PipelineOption>(options), nullptr); }
|
|
+ catch (...) {
|
|
+ MTLDebugPipelineFailure("computePipelineFailure");
|
|
+ if (error) *error = MTLErrorFromCurrentException("newComputePipelineStateWithDescriptor");
|
|
+ return nil;
|
|
+ }
|
|
if (!pso) {
|
|
if (error) {
|
|
// TODO: better error and/or match what the official Metal method does
|
|
@@ -166,6 +306,7 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
|
}
|
|
return nil;
|
|
}
|
|
+ if (reflection) *reflection = [[[MTLComputePipelineReflection alloc] initWithFunction:((MTLFunctionInternal*)descriptor.computeFunction).function] autorelease];
|
|
return [[MTLComputePipelineStateInternal alloc] initWithState: pso device: self label: descriptor.label];
|
|
}
|
|
|
|
@@ -183,7 +324,13 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
|
reflection: (MTLAutoreleasedComputePipelineReflection*)reflection
|
|
error: (NSError**)error
|
|
{
|
|
- auto pso = _device->newComputePipelineState(((MTLFunctionInternal*)computeFunction).function, static_cast<Indium::PipelineOption>(options), nullptr);
|
|
+ std::decay_t<decltype(_device->newComputePipelineState(((MTLFunctionInternal*)computeFunction).function, static_cast<Indium::PipelineOption>(options), nullptr))> pso;
|
|
+ try { pso = _device->newComputePipelineState(((MTLFunctionInternal*)computeFunction).function, static_cast<Indium::PipelineOption>(options), nullptr); }
|
|
+ catch (...) {
|
|
+ MTLDebugPipelineFailure("computePipelineFailure");
|
|
+ if (error) *error = MTLErrorFromCurrentException("newComputePipelineStateWithFunction");
|
|
+ return nil;
|
|
+ }
|
|
if (!pso) {
|
|
if (error) {
|
|
// TODO: better error and/or match what the official Metal method does
|
|
@@ -191,13 +338,44 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
|
}
|
|
return nil;
|
|
}
|
|
+ if (reflection) *reflection = [[[MTLComputePipelineReflection alloc] initWithFunction:((MTLFunctionInternal*)computeFunction).function] autorelease];
|
|
return [[MTLComputePipelineStateInternal alloc] initWithState: pso device: self label: nil];
|
|
}
|
|
|
|
+- (id<MTLRenderPipelineState>)newRenderPipelineStateWithDescriptor:(MTLRenderPipelineDescriptor*)descriptor options:(MTLPipelineOption)options reflection:(MTLAutoreleasedRenderPipelineReflection*)reflection error:(NSError**)error {
|
|
+ if (reflection) *reflection = nil;
|
|
+ if (options & ~(MTLPipelineOptionArgumentInfo | MTLPipelineOptionBufferTypeInfo)) {
|
|
+ if (error) *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:ENOTSUP userInfo:nil];
|
|
+ return nil;
|
|
+ }
|
|
+ id<MTLRenderPipelineState> state = [self newRenderPipelineStateWithDescriptor:descriptor error:error];
|
|
+ if (state && reflection) *reflection = [[[MTLRenderPipelineReflection alloc] initWithVertexFunction:((MTLFunctionInternal*)descriptor.vertexFunction).function fragmentFunction:((MTLFunctionInternal*)descriptor.fragmentFunction).function vertexDescriptor:descriptor.vertexDescriptor] autorelease];
|
|
+ if (state && reflection && *reflection && MTLDrawTracking()) {
|
|
+ // Diagnostic: remember what the game is told about this pipeline's resources (it binds by these names/indices)
|
|
+ std::string text;
|
|
+ char piece[200];
|
|
+ for (MTLArgument* argument in [*reflection fragmentArguments]) {
|
|
+ snprintf(piece, sizeof(piece), " %lu:%lu:%s", (unsigned long) argument.type, (unsigned long) argument.index, [argument.name UTF8String]);
|
|
+ text += piece;
|
|
+ }
|
|
+ std::lock_guard<std::mutex> guard(MTLDebugReflectionLock());
|
|
+ MTLDebugReflectionMap()[(void*) state] = text;
|
|
+ }
|
|
+ return state;
|
|
+}
|
|
+
|
|
- (id<MTLRenderPipelineState>)newRenderPipelineStateWithDescriptor: (MTLRenderPipelineDescriptor*)descriptor
|
|
error: (NSError**)error
|
|
{
|
|
- auto pso = _device->newRenderPipelineState([descriptor asIndiumDescriptor]);
|
|
+ MTLTraceCount("pipelineRequest");
|
|
+ std::decay_t<decltype(_device->newRenderPipelineState([descriptor asIndiumDescriptor]))> pso;
|
|
+ try { pso = _device->newRenderPipelineState([descriptor asIndiumDescriptor]); }
|
|
+ catch (...) {
|
|
+ MTLDebugPipelineFailure("renderPipelineFailure");
|
|
+ MTLTraceCount("pipelineException");
|
|
+ if (error) *error = MTLErrorFromCurrentException("newRenderPipelineStateWithDescriptor");
|
|
+ return nil;
|
|
+ }
|
|
if (!pso) {
|
|
if (error) {
|
|
// TODO: better error and/or match what the official Metal method does
|
|
@@ -205,7 +383,118 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
|
}
|
|
return nil;
|
|
}
|
|
- return [[MTLRenderPipelineStateInternal alloc] initWithState: pso device: self label: descriptor.label];
|
|
+ id<MTLRenderPipelineState> created = [[MTLRenderPipelineStateInternal alloc] initWithState: pso device: self label: descriptor.label];
|
|
+ if (MTLDrawTracking()) {
|
|
+ // Diagnostic: the reflection the game would get for this pipeline (it binds resources by these names/indices)
|
|
+ MTLRenderPipelineReflection* debugReflection = [[[MTLRenderPipelineReflection alloc] initWithVertexFunction:((MTLFunctionInternal*)descriptor.vertexFunction).function fragmentFunction:((MTLFunctionInternal*)descriptor.fragmentFunction).function vertexDescriptor:descriptor.vertexDescriptor] autorelease];
|
|
+ std::string text = "vs=" + std::string([descriptor.vertexFunction.name UTF8String] ?: "(nil)") +
|
|
+ " fs=" + std::string([descriptor.fragmentFunction.name UTF8String] ?: "(nil)");
|
|
+ char piece[200];
|
|
+ for (MTLArgument* argument in [debugReflection fragmentArguments]) {
|
|
+ snprintf(piece, sizeof(piece), " %lu:%lu:%s", (unsigned long) argument.type, (unsigned long) argument.index, [argument.name UTF8String]);
|
|
+ text += piece;
|
|
+ }
|
|
+ std::lock_guard<std::mutex> guard(MTLDebugReflectionLock());
|
|
+ MTLDebugReflectionMap()[(void*) created] = text;
|
|
+ }
|
|
+ static unsigned long logged = 0;
|
|
+ if (MTLDebugEnabled() && ++logged <= 300) {
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ MTLRenderPipelineColorAttachmentDescriptor* color = [descriptor.colorAttachments objectAtIndexedSubscript: 0];
|
|
+ fprintf(log, "pso %p indium=%p vs=%s fs=%s color0 fmt=%lu blend=%d src=%lu/%lu dst=%lu/%lu op=%lu/%lu mask=%lu depthFmt=%lu samples=%lu rasterDiscard=%d\n", created, (void*) pso.get(),
|
|
+ [descriptor.vertexFunction.name UTF8String], [descriptor.fragmentFunction.name UTF8String],
|
|
+ (unsigned long) color.pixelFormat, (int) color.blendingEnabled,
|
|
+ (unsigned long) color.sourceRGBBlendFactor, (unsigned long) color.sourceAlphaBlendFactor,
|
|
+ (unsigned long) color.destinationRGBBlendFactor, (unsigned long) color.destinationAlphaBlendFactor,
|
|
+ (unsigned long) color.rgbBlendOperation, (unsigned long) color.alphaBlendOperation, (unsigned long) color.writeMask,
|
|
+ (unsigned long) descriptor.depthAttachmentPixelFormat, (unsigned long) descriptor.sampleCount, (int) descriptor.rasterizationEnabled);
|
|
+ MTLVertexDescriptor* vertex = descriptor.vertexDescriptor;
|
|
+ for (NSUInteger i = 0; vertex && i < 31; ++i) {
|
|
+ MTLVertexAttributeDescriptor* attr = vertex.attributes[i];
|
|
+ if (attr.format == MTLVertexFormatInvalid) continue;
|
|
+ MTLVertexBufferLayoutDescriptor* layout = vertex.layouts[attr.bufferIndex];
|
|
+ fprintf(log, " attr[%lu] fmt=%lu off=%lu buf=%lu stride=%lu step=%lu rate=%lu\n", (unsigned long) i, (unsigned long) attr.format,
|
|
+ (unsigned long) attr.offset, (unsigned long) attr.bufferIndex, (unsigned long) layout.stride, (unsigned long) layout.stepFunction, (unsigned long) layout.stepRate);
|
|
+ }
|
|
+ if (!vertex) fprintf(log, " no vertex descriptor\n");
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+ return created;
|
|
+}
|
|
+
|
|
+- (id<MTLDepthStencilState>)newDepthStencilStateWithDescriptor:(MTLDepthStencilDescriptor*)descriptor
|
|
+{
|
|
+ if (!descriptor) return nil;
|
|
+ std::shared_ptr<Indium::DepthStencilState> state;
|
|
+ try { state = _device->newDepthStencilState([descriptor asIndiumDescriptor]); }
|
|
+ catch (...) { return nil; }
|
|
+ if (!state) return nil;
|
|
+ return [[MTLDepthStencilStateInternal alloc] initWithState:state device:self label:descriptor.label];
|
|
+}
|
|
+
|
|
+- (id<MTLSamplerState>)newSamplerStateWithDescriptor:(MTLSamplerDescriptor*)descriptor
|
|
+{
|
|
+ if (!descriptor) return nil;
|
|
+ Indium::SamplerDescriptor desc;
|
|
+ desc.minFilter = static_cast<Indium::SamplerMinMagFilter>(descriptor.minFilter);
|
|
+ desc.magFilter = static_cast<Indium::SamplerMinMagFilter>(descriptor.magFilter);
|
|
+ desc.mipFilter = static_cast<Indium::SamplerMipFilter>(descriptor.mipFilter);
|
|
+ desc.maxAnisotropy = descriptor.maxAnisotropy;
|
|
+ desc.sAddressMode = static_cast<Indium::SamplerAddressMode>(descriptor.sAddressMode);
|
|
+ desc.tAddressMode = static_cast<Indium::SamplerAddressMode>(descriptor.tAddressMode);
|
|
+ desc.rAddressMode = static_cast<Indium::SamplerAddressMode>(descriptor.rAddressMode);
|
|
+ desc.borderColor = static_cast<Indium::SamplerBorderColor>(descriptor.borderColor);
|
|
+ desc.normalizedCoordinates = descriptor.normalizedCoordinates;
|
|
+ desc.lodMinClamp = descriptor.lodMinClamp;
|
|
+ desc.lodMaxClamp = descriptor.lodMaxClamp;
|
|
+ desc.supportArgumentBuffers = descriptor.supportArgumentBuffers;
|
|
+ desc.compareFunction = static_cast<Indium::CompareFunction>(descriptor.compareFunction);
|
|
+ std::shared_ptr<Indium::SamplerState> sampler;
|
|
+ try { sampler = _device->newSamplerState(desc); }
|
|
+ catch (...) { return nil; }
|
|
+ if (!sampler) return nil;
|
|
+ return [[MTLSamplerStateInternal alloc] initWithSampler:sampler device:self label:descriptor.label];
|
|
+}
|
|
+
|
|
+- (id<MTLTexture>)newTextureWithDescriptor:(MTLTextureDescriptor*)descriptor
|
|
+{
|
|
+ MTLTraceCount("newTexture");
|
|
+ if (!descriptor || !descriptor.width || !descriptor.height || !descriptor.depth ||
|
|
+ !descriptor.mipmapLevelCount || !descriptor.sampleCount || !descriptor.arrayLength) return nil;
|
|
+ Indium::TextureDescriptor desc;
|
|
+ desc.textureType = static_cast<Indium::TextureType>(descriptor.textureType);
|
|
+ desc.pixelFormat = static_cast<Indium::PixelFormat>(descriptor.pixelFormat);
|
|
+ desc.width = descriptor.width;
|
|
+ desc.height = descriptor.height;
|
|
+ desc.depth = descriptor.depth;
|
|
+ desc.mipmapLevelCount = descriptor.mipmapLevelCount;
|
|
+ desc.sampleCount = descriptor.sampleCount;
|
|
+ desc.arrayLength = descriptor.arrayLength;
|
|
+ desc.resourceOptions = static_cast<Indium::ResourceOptions>(descriptor.resourceOptions);
|
|
+ desc.allowGPUOptimizedContents = descriptor.allowGPUOptimizedContents;
|
|
+ desc.usage = static_cast<Indium::TextureUsage>(descriptor.usage);
|
|
+ desc.swizzle = MTLTextureSwizzleChannelsToIndium(descriptor.swizzle);
|
|
+ std::shared_ptr<Indium::Texture> texture;
|
|
+ std::string failure;
|
|
+ try { texture = _device->newTexture(desc); }
|
|
+ catch (const std::exception& e) { failure = e.what(); }
|
|
+ catch (...) { failure = "unknown exception"; }
|
|
+ if (MTLDebugEnabled() || MTLTimingEnabled()) {
|
|
+ static unsigned long renderTargets = 0, failures = 0;
|
|
+ bool renderTarget = (descriptor.usage & MTLTextureUsageRenderTarget) != 0;
|
|
+ if (!texture ? ++failures <= 60 : (renderTarget && ++renderTargets <= 80)) {
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "%s %lux%lux%lu type=%lu fmt=%lu mips=%lu len=%lu samples=%lu usage=%lu storage=%lu %s\n", texture ? "renderTargetTexture" : "TEXTURE-FAILED",
|
|
+ (unsigned long) descriptor.width, (unsigned long) descriptor.height, (unsigned long) descriptor.depth, (unsigned long) descriptor.textureType,
|
|
+ (unsigned long) descriptor.pixelFormat, (unsigned long) descriptor.mipmapLevelCount, (unsigned long) descriptor.arrayLength, (unsigned long) descriptor.sampleCount,
|
|
+ (unsigned long) descriptor.usage, (unsigned long) descriptor.storageMode, failure.c_str());
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ if (!texture) return nil;
|
|
+ return [[MTLTextureInternal alloc] initWithTexture:texture device:self resourceOptions:descriptor.resourceOptions];
|
|
}
|
|
|
|
- (id<MTLCommandQueue>)newCommandQueue
|
|
@@ -275,8 +564,21 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
|
- (id<MTLLibrary>)newLibraryWithData: (dispatch_data_t)data
|
|
error: (NSError**)error
|
|
{
|
|
- NSData* nsdata = (NSData*)data;
|
|
- auto lib = _device->newLibrary(nsdata.bytes, nsdata.length);
|
|
+ const void* bytes = nullptr;
|
|
+ size_t length = 0;
|
|
+ dispatch_data_t mapped = dispatch_data_create_map(data, &bytes, &length);
|
|
+ if (!mapped) {
|
|
+ if (error) *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:ENOMEM userInfo:nil];
|
|
+ return nil;
|
|
+ }
|
|
+ std::shared_ptr<Indium::Library> lib;
|
|
+ try { lib = _device->newLibrary(bytes, length); }
|
|
+ catch (...) {
|
|
+ dispatch_release(mapped);
|
|
+ if (error) *error = MTLErrorFromCurrentException("newLibraryWithData");
|
|
+ return nil;
|
|
+ }
|
|
+ dispatch_release(mapped);
|
|
if (!lib) {
|
|
return nil;
|
|
}
|
|
diff --git a/src/Metal/MTLLibrary.mm b/src/Metal/MTLLibrary.mm
|
|
index 0feb46e..e0c0e93 100644
|
|
--- a/src/Metal/MTLLibrary.mm
|
|
+++ b/src/Metal/MTLLibrary.mm
|
|
@@ -1,6 +1,7 @@
|
|
// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
+#import <Metal/MTLExceptionBridge.h>
|
|
#import <Metal/MTLLibraryInternal.h>
|
|
#import <Metal/MTLDevice.h>
|
|
#import <Metal/stubs.h>
|
|
@@ -27,10 +28,12 @@
|
|
self = [super init];
|
|
if (self != nil) {
|
|
_function = function;
|
|
+ _functionType = static_cast<MTLFunctionType>(function->functionType());
|
|
+
|
|
_device = [device retain];
|
|
|
|
// cache the name
|
|
- _name = [NSString stringWithUTF8String: _function->name().c_str()];
|
|
+ _name = [[NSString alloc] initWithUTF8String: _function->name().c_str()];
|
|
}
|
|
return self;
|
|
}
|
|
@@ -121,7 +124,12 @@ MTL_UNSUPPORTED_CLASS
|
|
|
|
- (id<MTLFunction>)newFunctionWithName: (NSString*)functionName
|
|
{
|
|
- auto func = _library->newFunction(functionName.UTF8String);
|
|
+ std::shared_ptr<Indium::Function> func;
|
|
+ try { func = _library->newFunction(functionName.UTF8String); }
|
|
+ catch (...) {
|
|
+ MTLErrorFromCurrentException("newFunctionWithName");
|
|
+ return nil;
|
|
+ }
|
|
if (!func) {
|
|
return nil;
|
|
}
|
|
diff --git a/src/Metal/MTLRenderCommandEncoder.mm b/src/Metal/MTLRenderCommandEncoder.mm
|
|
index e931b90..b2fe0a5 100644
|
|
--- a/src/Metal/MTLRenderCommandEncoder.mm
|
|
+++ b/src/Metal/MTLRenderCommandEncoder.mm
|
|
@@ -1,3 +1,9 @@
|
|
+#import <Metal/MTLTrace.h>
|
|
+#import <Metal/MTLCommandBuffer.h>
|
|
+#import <Metal/MTLBlitCommandEncoder.h>
|
|
+#import <Metal/MTLTextureInternal.h>
|
|
+#import <Metal/MTLSamplerStateInternal.h>
|
|
+#import <Metal/MTLDepthStencilInternal.h>
|
|
// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
@@ -9,8 +15,408 @@
|
|
#import <Metal/MTLRenderPipelineInternal.h>
|
|
#import <Metal/MTLTypesInternal.h>
|
|
|
|
+#include <algorithm>
|
|
+#include <atomic>
|
|
+#include <cstring>
|
|
#include <map>
|
|
+#include <string>
|
|
+#if DARLING_METAL_ENABLED
|
|
+extern std::string MTLDebugReflectionFor(void* pso);
|
|
+#endif
|
|
+
|
|
+#if DARLING_METAL_ENABLED
|
|
+// Diagnostic (MTLDebugEnabled only): the state a draw is issued with, logged for the first few hundred draws.
|
|
+struct MTLDebugDrawState {
|
|
+ MTLViewport viewport = {};
|
|
+ MTLScissorRect scissor = {};
|
|
+ long cull = -1, winding = -1, fill = -1;
|
|
+ void* pipeline = nullptr;
|
|
+ void* depthStencil = nullptr;
|
|
+ id vertexBuffers[31] = {}; NSUInteger vertexOffsets[31] = {};
|
|
+ id fragmentBuffers[31] = {}; NSUInteger fragmentOffsets[31] = {};
|
|
+ unsigned char vertexBytes[31][64] = {}; NSUInteger vertexBytesLength[31] = {};
|
|
+ unsigned char fragmentBytes[31][64] = {}; NSUInteger fragmentBytesLength[31] = {};
|
|
+ id fragmentTextures[31] = {};
|
|
+ id vertexTextures[31] = {};
|
|
+};
|
|
+static thread_local MTLDebugDrawState debugDrawState;
|
|
+
|
|
+// Experiments, each enabled by a file in the guest /tmp present when the process starts (with metal-debug-trace)
|
|
+#define MTL_DEBUG_FLAG(name) ([]{ static const bool on = MTLDebugEnabled() && access("/tmp/" name, F_OK) == 0; return on; }())
|
|
+
|
|
+
|
|
+static void MTLDebugDumpFloats(FILE* log, const char* what, int index, const void* bytes, size_t available)
|
|
+{
|
|
+ fprintf(log, " %s[%d]:", what, index);
|
|
+ const float* floats = (const float*) bytes;
|
|
+ for (size_t i = 0; i < 16 && (i + 1) * 4 <= available; ++i) {
|
|
+ fprintf(log, " %g", floats[i]);
|
|
+ }
|
|
+ fprintf(log, "\n");
|
|
+}
|
|
+
|
|
+static void MTLDebugDumpBindings(unsigned long drawn)
|
|
+{
|
|
+ const MTLDebugDrawState& state = debugDrawState;
|
|
+ FILE* log = fopen("/tmp/metal-trace.log", "a");
|
|
+ if (!log) {
|
|
+ return;
|
|
+ }
|
|
+ fprintf(log, "bindings at draw #%lu\n", drawn);
|
|
+ for (int i = 0; i < 31; ++i) {
|
|
+ if (state.vertexBuffers[i]) {
|
|
+ id<MTLBuffer> buffer = state.vertexBuffers[i];
|
|
+ const char* contents = (const char*) buffer.contents;
|
|
+ fprintf(log, " vbuf[%d] len=%lu off=%lu contents=%s\n", i, (unsigned long) buffer.length, (unsigned long) state.vertexOffsets[i], contents ? "cpu" : "none");
|
|
+ if (contents && state.vertexOffsets[i] < buffer.length) {
|
|
+ MTLDebugDumpFloats(log, "vdata", i, contents + state.vertexOffsets[i], buffer.length - state.vertexOffsets[i]);
|
|
+ }
|
|
+ }
|
|
+ if (state.vertexBytesLength[i]) {
|
|
+ MTLDebugDumpFloats(log, "vbytes", i, state.vertexBytes[i], std::min<size_t>(64, state.vertexBytesLength[i]));
|
|
+ }
|
|
+ if (state.fragmentBuffers[i]) {
|
|
+ id<MTLBuffer> buffer = state.fragmentBuffers[i];
|
|
+ const char* contents = (const char*) buffer.contents;
|
|
+ fprintf(log, " fbuf[%d] len=%lu off=%lu contents=%s\n", i, (unsigned long) buffer.length, (unsigned long) state.fragmentOffsets[i], contents ? "cpu" : "none");
|
|
+ if (contents && state.fragmentOffsets[i] < buffer.length) {
|
|
+ MTLDebugDumpFloats(log, "fdata", i, contents + state.fragmentOffsets[i], buffer.length - state.fragmentOffsets[i]);
|
|
+ }
|
|
+ }
|
|
+ if (state.fragmentBytesLength[i]) {
|
|
+ MTLDebugDumpFloats(log, "fbytes", i, state.fragmentBytes[i], std::min<size_t>(64, state.fragmentBytesLength[i]));
|
|
+ }
|
|
+ if (state.fragmentTextures[i]) {
|
|
+ id<MTLTexture> texture = state.fragmentTextures[i];
|
|
+ unsigned long width = texture.width, height = texture.height, format = texture.pixelFormat;
|
|
+ unsigned long lit = 0, alpha = 0, total = 0;
|
|
+ bool read = MTLDebugEnabled() && (format == 80 || format == 81 || format == 70 || format == 71) && width * height <= 4096 * 4096;
|
|
+ if (read) {
|
|
+ id<MTLCommandQueue> queue = [[texture device] newCommandQueue];
|
|
+ id<MTLBuffer> readback = [[texture device] newBufferWithLength: width * height * 4 options: MTLResourceStorageModeShared];
|
|
+ id<MTLCommandBuffer> commandBuffer = [queue commandBuffer];
|
|
+ id<MTLBlitCommandEncoder> blit = [commandBuffer blitCommandEncoder];
|
|
+ [blit copyFromTexture: texture sourceSlice: 0 sourceLevel: 0 sourceOrigin: MTLOriginMake(0, 0, 0)
|
|
+ sourceSize: MTLSizeMake(width, height, 1) toBuffer: readback destinationOffset: 0
|
|
+ destinationBytesPerRow: width * 4 destinationBytesPerImage: width * height * 4];
|
|
+ [blit endEncoding];
|
|
+ [commandBuffer commit];
|
|
+ [commandBuffer waitUntilCompleted];
|
|
+ const uint8_t* bytes = (const uint8_t*) readback.contents;
|
|
+ for (unsigned long k = 0; k < width * height * 4; k += 4) {
|
|
+ ++total;
|
|
+ if (bytes[k] | bytes[k + 1] | bytes[k + 2]) ++lit;
|
|
+ if (bytes[k + 3]) ++alpha;
|
|
+ }
|
|
+ [readback release];
|
|
+ [queue release];
|
|
+ }
|
|
+ fprintf(log, " ftex[%d] ptr=%p %lux%lu type=%lu mips=%lu len=%lu samples=%lu fmt=%lu%s litRGB=%lu alphaNonZero=%lu of %lu\n", i, (void*) texture, width, height, (unsigned long) texture.textureType, (unsigned long) texture.mipmapLevelCount, (unsigned long) texture.arrayLength, (unsigned long) texture.sampleCount, format, read ? "" : " (not read)", lit, alpha, total);
|
|
+ }
|
|
+ }
|
|
+ fclose(log);
|
|
+}
|
|
+
|
|
+static void MTLDebugLogDraw(const char* kind, unsigned long count, unsigned long instances, unsigned long indexType, long baseVertex, id<MTLBuffer> indexBuffer, NSUInteger indexOffset)
|
|
+{
|
|
+ if (!MTLDebugEnabled()) {
|
|
+ // Light mode: only while /tmp/metal-snapshot exists, log up to 300 draws with their bindings
|
|
+ static unsigned long lightLogged = 0;
|
|
+ static unsigned long checks = 0;
|
|
+ static bool armed = false;
|
|
+ static std::string captureTag;
|
|
+ // re-read a few times a second: this runs for every draw, and a guest fopen is several host syscalls
|
|
+ if (checks++ % 4096 == 0) {
|
|
+ armed = false;
|
|
+ if (FILE* file = fopen("/tmp/metal-snapshot", "r")) {
|
|
+ char tag[48] = {};
|
|
+ if (fgets(tag, sizeof(tag), file)) {
|
|
+ if (captureTag != tag) { captureTag = tag; lightLogged = 0; }
|
|
+ armed = true;
|
|
+ }
|
|
+ fclose(file);
|
|
+ }
|
|
+ }
|
|
+ if (!armed || lightLogged >= 300) {
|
|
+ return;
|
|
+ }
|
|
+ ++lightLogged;
|
|
+ MTLDebugDumpBindings(2000 + lightLogged);
|
|
+ {
|
|
+ // Save the raw blocks of a few block-compressed textures (decoded offline) to check the stored image
|
|
+ static std::vector<void*> dumped;
|
|
+ for (int i = 0; i < 31; ++i) {
|
|
+ id<MTLTexture> texture = debugDrawState.fragmentTextures[i];
|
|
+ unsigned long format = texture ? (unsigned long) texture.pixelFormat : 0;
|
|
+ bool bc = format == 130 || format == 131 || format == 134 || format == 135;
|
|
+ if (!bc || texture.width < 128 || dumped.size() >= 8 || std::find(dumped.begin(), dumped.end(), (void*) texture) != dumped.end()) continue;
|
|
+ dumped.push_back((void*) texture);
|
|
+ NSUInteger level = std::min<NSUInteger>(1, texture.mipmapLevelCount - 1);
|
|
+ NSUInteger width = std::max<NSUInteger>(1, texture.width >> level), height = std::max<NSUInteger>(1, texture.height >> level);
|
|
+ NSUInteger blocksX = (width + 3) / 4, blocksY = (height + 3) / 4, blockBytes = (format == 130 || format == 131) ? 8 : 16;
|
|
+ NSUInteger rowBytes = blocksX * blockBytes, total = rowBytes * blocksY;
|
|
+ id<MTLCommandQueue> queue = [[texture device] newCommandQueue];
|
|
+ id<MTLBuffer> readback = [[texture device] newBufferWithLength: total options: MTLResourceStorageModeShared];
|
|
+ id<MTLCommandBuffer> commandBuffer = [queue commandBuffer];
|
|
+ id<MTLBlitCommandEncoder> blit = [commandBuffer blitCommandEncoder];
|
|
+ [blit copyFromTexture: texture sourceSlice: 0 sourceLevel: level sourceOrigin: MTLOriginMake(0, 0, 0) sourceSize: MTLSizeMake(width, height, 1)
|
|
+ toBuffer: readback destinationOffset: 0 destinationBytesPerRow: rowBytes destinationBytesPerImage: total];
|
|
+ [blit endEncoding];
|
|
+ [commandBuffer commit];
|
|
+ [commandBuffer waitUntilCompleted];
|
|
+ char path[128];
|
|
+ snprintf(path, sizeof(path), "/tmp/tex-%02zu-%lux%lu-f%lu-level%lu.bc", dumped.size(), (unsigned long) width, (unsigned long) height, format, (unsigned long) level);
|
|
+ if (FILE* out = fopen(path, "wb")) { fwrite(readback.contents, 1, total, out); fclose(out); }
|
|
+ [readback release];
|
|
+ [queue release];
|
|
+ }
|
|
+ }
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "draw #%lu %s count=%lu inst=%lu baseVtx=%ld pso=%p ds=%p cull=%ld wind=%ld\n", 2000 + lightLogged, kind, count, instances, baseVertex, debugDrawState.pipeline, debugDrawState.depthStencil, debugDrawState.cull, debugDrawState.winding);
|
|
+ fprintf(log, "reflect pso=%p frag:%s\n", debugDrawState.pipeline, MTLDebugReflectionFor(debugDrawState.pipeline).c_str());
|
|
+ fclose(log);
|
|
+ }
|
|
+ return;
|
|
+ }
|
|
+ static unsigned long drawn = 0;
|
|
+ static unsigned long bigDrawn = 0;
|
|
+ bool big = count >= 600 || instances > 1;
|
|
+ if (big && ++bigDrawn <= 120) {
|
|
+ MTLDebugDumpBindings(1000 + bigDrawn);
|
|
+ } else if (++drawn > 400) {
|
|
+ return;
|
|
+ }
|
|
+ const MTLDebugDrawState& state = debugDrawState;
|
|
+ if (drawn == 1 || drawn == 7 || drawn == 13 || drawn == 40) {
|
|
+ MTLDebugDumpBindings(drawn);
|
|
+ if (indexBuffer && indexBuffer.contents) {
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ const uint16_t* indices = (const uint16_t*)((const char*) indexBuffer.contents + indexOffset);
|
|
+ {
|
|
+ const unsigned char* base = (const unsigned char*) indexBuffer.contents;
|
|
+ NSUInteger from = indexOffset >= 256 ? indexOffset - 256 : 0;
|
|
+ fprintf(log, " before-index bytes from %lu:", (unsigned long) from);
|
|
+ for (NSUInteger k = from; k < indexOffset + 16; k += 4) {
|
|
+ float f; memcpy(&f, base + k, 4);
|
|
+ unsigned u; memcpy(&u, base + k, 4);
|
|
+ fprintf(log, " %g/%08x", f, u);
|
|
+ }
|
|
+ fprintf(log, "\n");
|
|
+ }
|
|
+ fprintf(log, " indices(type %lu, off %lu, len %lu):", indexType, (unsigned long) indexOffset, (unsigned long) indexBuffer.length);
|
|
+ for (int k = 0; k < 12; ++k) fprintf(log, " %u", indices[k]);
|
|
+ fprintf(log, "\n");
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "draw #%lu %s count=%lu inst=%lu idxType=%lu baseVtx=%ld pso=%p ds=%p vp=(%.0f %.0f %.0f %.0f %.2f %.2f) sc=(%lu %lu %lu %lu) cull=%ld wind=%ld fill=%ld\n",
|
|
+ drawn, kind, count, instances, indexType, baseVertex, state.pipeline, state.depthStencil,
|
|
+ state.viewport.originX, state.viewport.originY, state.viewport.width, state.viewport.height, state.viewport.znear, state.viewport.zfar,
|
|
+ (unsigned long) state.scissor.x, (unsigned long) state.scissor.y, (unsigned long) state.scissor.width, (unsigned long) state.scissor.height,
|
|
+ state.cull, state.winding, state.fill);
|
|
+ fclose(log);
|
|
+ }
|
|
+}
|
|
+#endif
|
|
+
|
|
+
|
|
+#if DARLING_METAL_ENABLED
|
|
+// Experiment: /tmp/metal-draw-limit holds N; every render pass then executes only its first N draws.
|
|
+static thread_local unsigned long passDrawIndex = 0;
|
|
+static bool MTLDrawLimited(unsigned long count)
|
|
+{
|
|
+ static std::atomic<unsigned long> calls { 0 };
|
|
+ static std::atomic<long> limit { -1 };
|
|
+ static std::atomic<long> minimum { 0 };
|
|
+ static std::atomic<long> maximum { -1 };
|
|
+ static const bool possible = access("/tmp/metal-debug-draws", F_OK) == 0 || MTLDebugEnabled();
|
|
+ if (!possible) {
|
|
+ return false;
|
|
+ }
|
|
+ if (calls.fetch_add(1) % 200 == 0) {
|
|
+ long value = -1;
|
|
+ if (FILE* file = fopen("/tmp/metal-draw-limit", "r")) {
|
|
+ if (fscanf(file, "%ld", &value) != 1) value = -1;
|
|
+ fclose(file);
|
|
+ }
|
|
+ limit = value;
|
|
+ long low = 0, high = -1;
|
|
+ if (FILE* file = fopen("/tmp/metal-draw-min", "r")) { if (fscanf(file, "%ld", &low) != 1) low = 0; fclose(file); }
|
|
+ if (FILE* file = fopen("/tmp/metal-draw-max", "r")) { if (fscanf(file, "%ld", &high) != 1) high = -1; fclose(file); }
|
|
+ minimum = low;
|
|
+ maximum = high;
|
|
+ }
|
|
+ // skip draws outside [min, max] index counts (experiment: which draws build which part of the picture)
|
|
+ if (count < (unsigned long) (long) minimum || (maximum >= 0 && count > (unsigned long) (long) maximum)) {
|
|
+ return true;
|
|
+ }
|
|
+ long n = limit;
|
|
+ return n >= 0 && ++passDrawIndex > (unsigned long) n;
|
|
+}
|
|
+#endif
|
|
+
|
|
+#if DARLING_METAL_ENABLED
|
|
+// Metal's render encoders start with every slot unbound, and a bind only replaces the value its slot
|
|
+// holds, so re-binding the identical value at the same slot cannot change what later draws see: the
|
|
+// call can be dropped before any of its work (the resource's shared_ptr copy and its atomic refcount,
|
|
+// the Indium op, and the first touch of both objects). These records are what the setters compare
|
|
+// against.
|
|
+//
|
|
+// A record holds the *Indium* object's address (`shared_ptr::get()`, see MTLBindKey), never the
|
|
+// Objective-C wrapper's address. The wrapper is not a stable identity: the game may release it while
|
|
+// the Indium encoder still keeps the resource it wrapped alive, the allocator can then hand the same
|
|
+// address to a wrapper around a different resource (transient per-draw buffers do exactly that), and a
|
|
+// comparison on the wrapper's address would drop that new resource's bind -- every later draw would
|
|
+// keep using the old one (the ABA problem). The Indium encoder holds each resource it was given until
|
|
+// the encoder ends -- `FunctionResources::textures/buffers/samplers` store the shared_ptr the setter
|
|
+// received (command-encoder.private.hpp), and the pipeline is held by `_privatePSO`/`_pipelineOwners`
|
|
+// (render-command-encoder.cpp), on both the deferred and the direct encoder path -- so an Indium
|
|
+// address cannot be reused while it is recorded. The depth-stencil state is the one kind Indium does
|
|
+// not keep (its setter reads the descriptor and issues the Vulkan commands), so its record retains
|
|
+// one itself (`_depthStencilOwner`).
|
|
+//
|
|
+// `valid` marks the slots a value was recorded for: a slot that was never bound is always forwarded,
|
|
+// and setVertexBytes/setFragmentBytes (inline data in the same slot) just clears the mark.
|
|
+struct MTLBindObjectSlots {
|
|
+ const void* keys[31] = {};
|
|
+ uint32_t valid = 0;
|
|
+};
|
|
+
|
|
+struct MTLBindBufferSlots {
|
|
+ const void* keys[31] = {};
|
|
+ NSUInteger offsets[31] = {};
|
|
+ uint32_t valid = 0;
|
|
+};
|
|
+
|
|
+// The key a bind is recorded and compared under: the Indium object's address. Reading it out of the
|
|
+// accessor's const shared_ptr reference is one load of the wrapper's payload -- no copy, no atomic.
|
|
+template<class T>
|
|
+static inline const void* MTLBindKey(const std::shared_ptr<T>& object)
|
|
+{
|
|
+ return object.get();
|
|
+}
|
|
+
|
|
+// true: the slot already holds this value, so the caller can drop the call; false: forward it, and the
|
|
+// value is recorded so the next identical call can be dropped.
|
|
+static bool MTLBindObjectSlot(MTLBindObjectSlots& slots, NSUInteger index, const void* key)
|
|
+{
|
|
+ if (index >= 31) {
|
|
+ return false;
|
|
+ }
|
|
+ const uint32_t bit = 1u << index;
|
|
+ if ((slots.valid & bit) && slots.keys[index] == key) {
|
|
+ return true;
|
|
+ }
|
|
+ slots.keys[index] = key;
|
|
+ slots.valid |= bit;
|
|
+ return false;
|
|
+}
|
|
+
|
|
+static bool MTLBindBufferSlot(MTLBindBufferSlots& slots, NSUInteger index, const void* key, NSUInteger offset)
|
|
+{
|
|
+ if (index >= 31) {
|
|
+ return false;
|
|
+ }
|
|
+ const uint32_t bit = 1u << index;
|
|
+ if ((slots.valid & bit) && slots.keys[index] == key && slots.offsets[index] == offset) {
|
|
+ return true;
|
|
+ }
|
|
+ slots.keys[index] = key;
|
|
+ slots.offsets[index] = offset;
|
|
+ slots.valid |= bit;
|
|
+ return false;
|
|
+}
|
|
+
|
|
+// The offset belongs to the buffer's slot state, so an unchanged one is dropped the same way.
|
|
+static bool MTLBindBufferOffset(MTLBindBufferSlots& slots, NSUInteger index, NSUInteger offset)
|
|
+{
|
|
+ if (index >= 31 || !(slots.valid & (1u << index))) {
|
|
+ return false;
|
|
+ }
|
|
+ if (slots.offsets[index] == offset) {
|
|
+ return true;
|
|
+ }
|
|
+ slots.offsets[index] = offset;
|
|
+ return false;
|
|
+}
|
|
+
|
|
+static void MTLBindForgetBufferSlot(MTLBindBufferSlots& slots, NSUInteger index)
|
|
+{
|
|
+ if (index < 31) {
|
|
+ slots.valid &= ~(1u << index);
|
|
+ }
|
|
+}
|
|
+
|
|
+// endEncoding: nothing may be bound after it, and the Indium encoder releases the resources it held,
|
|
+// so no record -- and no address a record kept meaningful -- survives the pass.
|
|
+static void MTLBindForgetAll(MTLBindObjectSlots& slots)
|
|
+{
|
|
+ slots = MTLBindObjectSlots {};
|
|
+}
|
|
+
|
|
+static void MTLBindForgetAll(MTLBindBufferSlots& slots)
|
|
+{
|
|
+ slots = MTLBindBufferSlots {};
|
|
+}
|
|
+
|
|
+// Diagnostic (guest flag /tmp/metal-debug-binds, or the full trace flag): count the dropped re-binds.
|
|
+// The setters test MTLBindSkipsEnabled() inline first, so a dropped call pays one load and a branch
|
|
+// rather than the counter's call frame; MTLBindSkipCount then appends the totals to the trace log on
|
|
+// the same 3 s interval as the other counters.
|
|
+static bool MTLBindSkipsEnabled()
|
|
+{
|
|
+ static const bool enabled = MTLDebugEnabled() || access("/tmp/metal-debug-binds", F_OK) == 0;
|
|
+ return enabled;
|
|
+}
|
|
+
|
|
+static void MTLBindSkipCount(const char* what)
|
|
+{
|
|
+ static const bool enabled = MTLDebugEnabled() || access("/tmp/metal-debug-binds", F_OK) == 0;
|
|
+ if (!enabled) {
|
|
+ return;
|
|
+ }
|
|
+ static std::mutex lock;
|
|
+ static std::map<std::string, unsigned long> counts;
|
|
+ static time_t last = 0;
|
|
+ std::lock_guard<std::mutex> guard(lock);
|
|
+ ++counts[what];
|
|
+ time_t now = time(nullptr);
|
|
+ if (now - last >= 3) {
|
|
+ last = now;
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "bind-skips@%ld:", (long) now);
|
|
+ for (auto& count: counts) {
|
|
+ fprintf(log, " %s=%lu", count.first.c_str(), count.second);
|
|
+ }
|
|
+ fprintf(log, "\n");
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+}
|
|
+#endif
|
|
|
|
+#if DARLING_METAL_ENABLED
|
|
+// Live experiments (guest flag files, re-read every few hundred state changes): /tmp/metal-flag-nocull, -flipwind, -nodepth
|
|
+static bool MTLLiveFlag(int which)
|
|
+{
|
|
+ static std::atomic<unsigned long> calls { 0 };
|
|
+ static std::atomic<unsigned> state { 0 };
|
|
+ static const bool possible = access("/tmp/metal-debug-draws", F_OK) == 0 || MTLDebugEnabled();
|
|
+ if (!possible) {
|
|
+ return false;
|
|
+ }
|
|
+ if (calls.fetch_add(1) % 100 == 0) {
|
|
+ unsigned value = 0;
|
|
+ if (access("/tmp/metal-flag-nocull", F_OK) == 0) value |= 1;
|
|
+ if (access("/tmp/metal-flag-flipwind", F_OK) == 0) value |= 2;
|
|
+ if (access("/tmp/metal-flag-nodepth", F_OK) == 0) value |= 4;
|
|
+ state = value;
|
|
+ }
|
|
+ return (state & (1u << which)) != 0;
|
|
+}
|
|
+#endif
|
|
@implementation MTLRenderPassAttachmentDescriptor
|
|
|
|
#if DARLING_METAL_ENABLED
|
|
@@ -96,11 +502,11 @@ MTL_UNSUPPORTED_CLASS
|
|
- (Indium::RenderPassAttachmentDescriptor)asIndiumDescriptor
|
|
{
|
|
return Indium::RenderPassAttachmentDescriptor {
|
|
- ((MTLTextureInternal*)_texture).texture,
|
|
+ MTLTextureGetIndium(_texture),
|
|
_level,
|
|
_slice,
|
|
_depthPlane,
|
|
- ((MTLTextureInternal*)_resolveTexture).texture,
|
|
+ MTLTextureGetIndium(_resolveTexture),
|
|
_resolveLevel,
|
|
_resolveSlice,
|
|
_resolveDepthPlane,
|
|
@@ -367,7 +773,11 @@ MTL_UNSUPPORTED_CLASS
|
|
std::vector<Indium::RenderPassColorAttachmentDescriptor> result;
|
|
|
|
for (const auto& [key, value]: tmp2) {
|
|
- result.push_back(value);
|
|
+ // A slot that was only looked at (or never given a texture) is not an attachment; depth-only passes rely on this
|
|
+ if (value.texture) {
|
|
+ result.resize(key + 1);
|
|
+ result[key] = value;
|
|
+ }
|
|
}
|
|
|
|
return result;
|
|
@@ -404,6 +814,10 @@ MTL_UNSUPPORTED_CLASS
|
|
- (void)_doInit
|
|
{
|
|
_colorAttachments = [MTLRenderPassColorAttachmentDescriptorArray new];
|
|
+ _depthAttachment = [MTLRenderPassDepthAttachmentDescriptor new];
|
|
+ _stencilAttachment = [MTLRenderPassStencilAttachmentDescriptor new];
|
|
+ _renderTargetArrayLength = 1;
|
|
+ _defaultRasterSampleCount = 1;
|
|
}
|
|
|
|
- (void)_doDealloc
|
|
@@ -502,9 +916,9 @@ MTL_UNSUPPORTED_CLASS
|
|
{
|
|
return Indium::RenderPassDescriptor {
|
|
[_colorAttachments asContiguousIndiumDescriptors],
|
|
- (_depthAttachment == nil) ? std::nullopt : std::make_optional([_depthAttachment asIndiumDescriptor]),
|
|
- (_stencilAttachment == nil) ? std::nullopt : std::make_optional([_stencilAttachment asIndiumDescriptor]),
|
|
- ((MTLBufferInternal*)_visibilityResultBuffer).buffer,
|
|
+ (_depthAttachment.texture == nil) ? std::nullopt : std::make_optional([_depthAttachment asIndiumDescriptor]),
|
|
+ (_stencilAttachment.texture == nil) ? std::nullopt : std::make_optional([_stencilAttachment asIndiumDescriptor]),
|
|
+ MTLBufferGetIndium(_visibilityResultBuffer),
|
|
_renderTargetArrayLength,
|
|
_imageblockSampleLength,
|
|
_threadgroupMemoryLength,
|
|
@@ -527,6 +941,22 @@ MTL_UNSUPPORTED_CLASS
|
|
|
|
#if DARLING_METAL_ENABLED
|
|
|
|
+{
|
|
+ // The bindings this encoder has been given, so an identical re-bind can be dropped (see the
|
|
+ // tracking helpers above). Every encoder object is new and starts with every slot unbound, and
|
|
+ // nothing outside this class changes an encoder's bindings, so the records are per-encoder state.
|
|
+ // `_depthStencilOwner` is the strong reference the depth-stencil record needs (see the helpers).
|
|
+ MTLBindObjectSlots _vertexTextures;
|
|
+ MTLBindObjectSlots _fragmentTextures;
|
|
+ MTLBindObjectSlots _vertexSamplers;
|
|
+ MTLBindObjectSlots _fragmentSamplers;
|
|
+ MTLBindObjectSlots _pipelineState;
|
|
+ MTLBindObjectSlots _depthStencilState;
|
|
+ MTLBindBufferSlots _vertexBuffers;
|
|
+ MTLBindBufferSlots _fragmentBuffers;
|
|
+ std::shared_ptr<Indium::DepthStencilState> _depthStencilOwner;
|
|
+}
|
|
+
|
|
@synthesize device = _device;
|
|
@synthesize encoder = _encoder;
|
|
@synthesize label = _label;
|
|
@@ -538,6 +968,7 @@ MTL_UNSUPPORTED_CLASS
|
|
if (self != nil) {
|
|
_encoder = encoder;
|
|
_device = [device retain];
|
|
+ passDrawIndex = 0;
|
|
}
|
|
return self;
|
|
}
|
|
@@ -553,8 +984,60 @@ MTL_UNSUPPORTED_CLASS
|
|
// overridden methods
|
|
//
|
|
|
|
+- (void)setVertexTexture:(id<MTLTexture>)texture atIndex:(NSUInteger)index {
|
|
+ const std::shared_ptr<Indium::Texture>& indiumTexture = MTLTextureGetIndium(texture);
|
|
+ if (MTLBindObjectSlot(_vertexTextures, index, MTLBindKey(indiumTexture))) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipVertexTexture"); } return; }
|
|
+ _encoder->setVertexTexture(indiumTexture, index);
|
|
+}
|
|
+- (void)setFragmentTexture:(id<MTLTexture>)texture atIndex:(NSUInteger)index {
|
|
+ const std::shared_ptr<Indium::Texture>& indiumTexture = MTLTextureGetIndium(texture);
|
|
+ if (MTLBindObjectSlot(_fragmentTextures, index, MTLBindKey(indiumTexture))) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipFragmentTexture"); } return; }
|
|
+ if (MTLDrawTracking() && index < 31) debugDrawState.fragmentTextures[index] = texture;
|
|
+ _encoder->setFragmentTexture(indiumTexture, index);
|
|
+}
|
|
+- (void)setVertexSamplerState:(id<MTLSamplerState>)sampler atIndex:(NSUInteger)index {
|
|
+ const std::shared_ptr<Indium::SamplerState>& indiumSampler = MTLSamplerGetIndium(sampler);
|
|
+ if (MTLBindObjectSlot(_vertexSamplers, index, MTLBindKey(indiumSampler))) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipVertexSamplerState"); } return; }
|
|
+ _encoder->setVertexSamplerState(indiumSampler, index);
|
|
+}
|
|
+- (void)setFragmentSamplerState:(id<MTLSamplerState>)sampler atIndex:(NSUInteger)index {
|
|
+ const std::shared_ptr<Indium::SamplerState>& indiumSampler = MTLSamplerGetIndium(sampler);
|
|
+ if (MTLBindObjectSlot(_fragmentSamplers, index, MTLBindKey(indiumSampler))) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipFragmentSamplerState"); } return; }
|
|
+ _encoder->setFragmentSamplerState(indiumSampler, index);
|
|
+}
|
|
+- (void)setDepthBias:(float)depthBias slopeScale:(float)slopeScale clamp:(float)clamp {
|
|
+ _encoder->setDepthBias(depthBias, slopeScale, clamp);
|
|
+}
|
|
+- (void)setDepthStencilState:(id<MTLDepthStencilState>)state {
|
|
+ const std::shared_ptr<Indium::DepthStencilState>& indiumState = MTLDepthStencilGetIndium(state);
|
|
+ // Indium's setter does not keep the state (it reads the descriptor and issues the Vulkan commands),
|
|
+ // so this record holds the reference that keeps the recorded address from being reused instead.
|
|
+ if (MTLBindObjectSlot(_depthStencilState, 0, MTLBindKey(indiumState))) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipDepthStencilState"); } return; }
|
|
+ _depthStencilOwner = indiumState;
|
|
+ if (MTLDrawTracking()) debugDrawState.depthStencil = state;
|
|
+ if (MTLLiveFlag(2)) return;
|
|
+ _encoder->setDepthStencilState(indiumState);
|
|
+}
|
|
+- (void)setStencilReferenceValue:(uint32_t)value {
|
|
+ _encoder->setStencilReferenceValue(value);
|
|
+}
|
|
+- (void)memoryBarrierWithScope:(MTLBarrierScope)scope afterStages:(MTLRenderStages)after beforeStages:(MTLRenderStages)before {
|
|
+ _encoder->memoryBarrier(scope, static_cast<Indium::RenderStages>(after), static_cast<Indium::RenderStages>(before));
|
|
+}
|
|
+
|
|
- (void)endEncoding
|
|
{
|
|
+ // Nothing may be bound after this, and the Indium encoder releases the resources it held, so the
|
|
+ // records must not survive the pass (a later bind must never be compared against them again).
|
|
+ MTLBindForgetAll(_vertexTextures);
|
|
+ MTLBindForgetAll(_fragmentTextures);
|
|
+ MTLBindForgetAll(_vertexSamplers);
|
|
+ MTLBindForgetAll(_fragmentSamplers);
|
|
+ MTLBindForgetAll(_pipelineState);
|
|
+ MTLBindForgetAll(_depthStencilState);
|
|
+ MTLBindForgetAll(_vertexBuffers);
|
|
+ MTLBindForgetAll(_fragmentBuffers);
|
|
+ _depthStencilOwner.reset();
|
|
_encoder->endEncoding();
|
|
}
|
|
|
|
@@ -564,26 +1047,37 @@ MTL_UNSUPPORTED_CLASS
|
|
|
|
- (void)setRenderPipelineState: (id<MTLRenderPipelineState>)pipelineState
|
|
{
|
|
- _encoder->setRenderPipelineState(((MTLRenderPipelineStateInternal*)pipelineState).state);
|
|
+ const std::shared_ptr<Indium::RenderPipelineState>& indiumPipelineState = MTLRenderPipelineGetIndium(pipelineState);
|
|
+ // dropped re-binds are not counted as "setPipeline" in the diagnostics, they are not calls
|
|
+ if (MTLBindObjectSlot(_pipelineState, 0, MTLBindKey(indiumPipelineState))) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipPipelineState"); } return; }
|
|
+ if (MTLDrawTracking()) debugDrawState.pipeline = pipelineState;
|
|
+ MTLTraceCount(pipelineState ? "setPipeline" : "setPipelineNil");
|
|
+ _encoder->setRenderPipelineState(indiumPipelineState);
|
|
}
|
|
|
|
- (void)setTriangleFillMode: (MTLTriangleFillMode)fillMode
|
|
{
|
|
+ if (MTLDrawTracking()) debugDrawState.fill = (long) fillMode;
|
|
_encoder->setTriangleFillMode(static_cast<Indium::TriangleFillMode>(fillMode));
|
|
}
|
|
|
|
- (void)setFrontFacingWinding: (MTLWinding)frontFacingWinding
|
|
{
|
|
+ if (MTLDrawTracking()) debugDrawState.winding = (long) frontFacingWinding;
|
|
+ if (MTLLiveFlag(1)) frontFacingWinding = (frontFacingWinding == MTLWindingClockwise) ? MTLWindingCounterClockwise : MTLWindingClockwise;
|
|
_encoder->setFrontFacingWinding(static_cast<Indium::Winding>(frontFacingWinding));
|
|
}
|
|
|
|
- (void)setCullMode: (MTLCullMode)cullMode
|
|
{
|
|
+ if (MTLDrawTracking()) debugDrawState.cull = (long) cullMode;
|
|
+ if (MTLLiveFlag(0)) cullMode = MTLCullModeNone;
|
|
_encoder->setCullMode(static_cast<Indium::CullMode>(cullMode));
|
|
}
|
|
|
|
- (void)setViewport: (MTLViewport)viewport
|
|
{
|
|
+ if (MTLDrawTracking()) debugDrawState.viewport = viewport;
|
|
_encoder->setViewport(MTLViewportToIndium(viewport));
|
|
}
|
|
|
|
@@ -599,6 +1093,8 @@ MTL_UNSUPPORTED_CLASS
|
|
|
|
- (void)setScissorRect: (MTLScissorRect)rect
|
|
{
|
|
+ if (MTLDrawTracking()) debugDrawState.scissor = rect;
|
|
+ if (MTL_DEBUG_FLAG("metal-debug-noscissor")) return;
|
|
_encoder->setScissorRect(MTLScissorRectToIndium(rect));
|
|
}
|
|
|
|
@@ -616,7 +1112,27 @@ MTL_UNSUPPORTED_CLASS
|
|
offset: (NSUInteger)offset
|
|
atIndex: (NSUInteger)index
|
|
{
|
|
- _encoder->setVertexBuffer(((MTLBufferInternal*)buffer).buffer, offset, index);
|
|
+ const std::shared_ptr<Indium::Buffer>& indiumBuffer = MTLBufferGetIndium(buffer);
|
|
+ if (MTLBindBufferSlot(_vertexBuffers, index, MTLBindKey(indiumBuffer), offset)) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipVertexBuffer"); } return; }
|
|
+ if (MTLDebugEnabled()) {
|
|
+ static unsigned long calls = 0;
|
|
+ static unsigned long unusual = 0;
|
|
+ bool other = index != 3 && index != 4 && index != 10;
|
|
+ if (other && ++unusual <= 200) {
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "setVertexBuffer-other idx=%lu buf=%p off=%lu\n", (unsigned long) index, buffer, (unsigned long) offset);
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+ if (++calls <= 120) {
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "setVertexBuffer #%lu idx=%lu buf=%p off=%lu len=%lu encoder=%p\n", calls, (unsigned long) index, buffer, (unsigned long) offset, (unsigned long) buffer.length, self);
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ if (MTLDrawTracking() && index < 31) { debugDrawState.vertexBuffers[index] = buffer; debugDrawState.vertexOffsets[index] = offset; debugDrawState.vertexBytesLength[index] = 0; }
|
|
+ _encoder->setVertexBuffer(indiumBuffer, offset, index);
|
|
}
|
|
|
|
- (void)setVertexBuffers: (const id<MTLBuffer>*)buffers
|
|
@@ -626,7 +1142,17 @@ MTL_UNSUPPORTED_CLASS
|
|
std::vector<std::shared_ptr<Indium::Buffer>> tmp;
|
|
std::vector<size_t> tmp2(offsets, offsets + range.length);
|
|
for (size_t i = 0; i < range.length; ++i) {
|
|
- tmp.push_back(((MTLBufferInternal*)buffers[i]).buffer);
|
|
+ tmp.push_back(MTLBufferGetIndium(buffers[i]));
|
|
+ }
|
|
+ if (MTLDrawTracking()) {
|
|
+ for (NSUInteger i = 0; i < range.length && range.location + i < 31; ++i) {
|
|
+ debugDrawState.vertexBuffers[range.location + i] = buffers[i];
|
|
+ debugDrawState.vertexOffsets[range.location + i] = offsets[i];
|
|
+ debugDrawState.vertexBytesLength[range.location + i] = 0;
|
|
+ }
|
|
+ }
|
|
+ for (NSUInteger i = 0; i < range.length; ++i) {
|
|
+ MTLBindBufferSlot(_vertexBuffers, range.location + i, MTLBindKey(tmp[i]), offsets[i]);
|
|
}
|
|
_encoder->setVertexBuffers(tmp, tmp2, NSRangeToIndium(range));
|
|
}
|
|
@@ -634,6 +1160,17 @@ MTL_UNSUPPORTED_CLASS
|
|
- (void)setVertexBufferOffset: (NSUInteger)offset
|
|
atIndex: (NSUInteger)index
|
|
{
|
|
+ if (MTLBindBufferOffset(_vertexBuffers, index, offset)) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipVertexBufferOffset"); } return; }
|
|
+ if (MTLDebugEnabled()) {
|
|
+ static unsigned long calls = 0;
|
|
+ if (++calls <= 120) {
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "setVertexBufferOffset #%lu idx=%lu off=%lu encoder=%p\n", calls, (unsigned long) index, (unsigned long) offset, self);
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ if (MTLDrawTracking() && index < 31) debugDrawState.vertexOffsets[index] = offset;
|
|
_encoder->setVertexBufferOffset(offset, index);
|
|
}
|
|
|
|
@@ -641,6 +1178,8 @@ MTL_UNSUPPORTED_CLASS
|
|
length: (NSUInteger)length
|
|
atIndex: (NSUInteger)index
|
|
{
|
|
+ MTLBindForgetBufferSlot(_vertexBuffers, index);
|
|
+ if (MTLDrawTracking() && index < 31) { debugDrawState.vertexBuffers[index] = nil; debugDrawState.vertexBytesLength[index] = length; memcpy(debugDrawState.vertexBytes[index], bytes, std::min<NSUInteger>(64, length)); }
|
|
_encoder->setVertexBytes(bytes, length, index);
|
|
}
|
|
|
|
@@ -648,7 +1187,10 @@ MTL_UNSUPPORTED_CLASS
|
|
offset: (NSUInteger)offset
|
|
atIndex: (NSUInteger)index
|
|
{
|
|
- _encoder->setFragmentBuffer(((MTLBufferInternal*)buffer).buffer, offset, index);
|
|
+ const std::shared_ptr<Indium::Buffer>& indiumBuffer = MTLBufferGetIndium(buffer);
|
|
+ if (MTLBindBufferSlot(_fragmentBuffers, index, MTLBindKey(indiumBuffer), offset)) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipFragmentBuffer"); } return; }
|
|
+ if (MTLDrawTracking() && index < 31) { debugDrawState.fragmentBuffers[index] = buffer; debugDrawState.fragmentOffsets[index] = offset; debugDrawState.fragmentBytesLength[index] = 0; }
|
|
+ _encoder->setFragmentBuffer(indiumBuffer, offset, index);
|
|
}
|
|
|
|
- (void)setFragmentBuffers: (const id<MTLBuffer>*)buffers
|
|
@@ -658,7 +1200,17 @@ MTL_UNSUPPORTED_CLASS
|
|
std::vector<std::shared_ptr<Indium::Buffer>> tmp;
|
|
std::vector<size_t> tmp2(offsets, offsets + range.length);
|
|
for (size_t i = 0; i < range.length; ++i) {
|
|
- tmp.push_back(((MTLBufferInternal*)buffers[i]).buffer);
|
|
+ tmp.push_back(MTLBufferGetIndium(buffers[i]));
|
|
+ }
|
|
+ if (MTLDrawTracking()) {
|
|
+ for (NSUInteger i = 0; i < range.length && range.location + i < 31; ++i) {
|
|
+ debugDrawState.fragmentBuffers[range.location + i] = buffers[i];
|
|
+ debugDrawState.fragmentOffsets[range.location + i] = offsets[i];
|
|
+ debugDrawState.fragmentBytesLength[range.location + i] = 0;
|
|
+ }
|
|
+ }
|
|
+ for (NSUInteger i = 0; i < range.length; ++i) {
|
|
+ MTLBindBufferSlot(_fragmentBuffers, range.location + i, MTLBindKey(tmp[i]), offsets[i]);
|
|
}
|
|
_encoder->setFragmentBuffers(tmp, tmp2, NSRangeToIndium(range));
|
|
}
|
|
@@ -666,6 +1218,8 @@ MTL_UNSUPPORTED_CLASS
|
|
- (void)setFragmentBufferOffset: (NSUInteger)offset
|
|
atIndex: (NSUInteger)index
|
|
{
|
|
+ if (MTLBindBufferOffset(_fragmentBuffers, index, offset)) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipFragmentBufferOffset"); } return; }
|
|
+ if (MTLDrawTracking() && index < 31) debugDrawState.fragmentOffsets[index] = offset;
|
|
_encoder->setFragmentBufferOffset(offset, index);
|
|
}
|
|
|
|
@@ -673,6 +1227,8 @@ MTL_UNSUPPORTED_CLASS
|
|
length: (NSUInteger)length
|
|
atIndex: (NSUInteger)index
|
|
{
|
|
+ MTLBindForgetBufferSlot(_fragmentBuffers, index);
|
|
+ if (MTLDrawTracking() && index < 31) { debugDrawState.fragmentBuffers[index] = nil; debugDrawState.fragmentBytesLength[index] = length; memcpy(debugDrawState.fragmentBytes[index], bytes, std::min<NSUInteger>(64, length)); }
|
|
_encoder->setFragmentBytes(bytes, length, index);
|
|
}
|
|
|
|
@@ -682,6 +1238,9 @@ MTL_UNSUPPORTED_CLASS
|
|
instanceCount: (NSUInteger)instanceCount
|
|
baseInstance: (NSUInteger)baseInstance
|
|
{
|
|
+ if (MTLDrawLimited(vertexCount)) return;
|
|
+ MTLTraceCount("draw");
|
|
+ if (MTLDrawTracking()) MTLDebugLogDraw("direct", vertexCount, 0, 0, 0, nil, 0);
|
|
_encoder->drawPrimitives(static_cast<Indium::PrimitiveType>(primitiveType), vertexStart, vertexCount, instanceCount, baseInstance);
|
|
}
|
|
|
|
@@ -690,6 +1249,9 @@ MTL_UNSUPPORTED_CLASS
|
|
vertexCount: (NSUInteger)vertexCount
|
|
instanceCount: (NSUInteger)instanceCount
|
|
{
|
|
+ if (MTLDrawLimited(vertexCount)) return;
|
|
+ MTLTraceCount("draw");
|
|
+ if (MTLDrawTracking()) MTLDebugLogDraw("direct", vertexCount, 0, 0, 0, nil, 0);
|
|
_encoder->drawPrimitives(static_cast<Indium::PrimitiveType>(primitiveType), vertexStart, vertexCount, instanceCount);
|
|
}
|
|
|
|
@@ -697,6 +1259,9 @@ MTL_UNSUPPORTED_CLASS
|
|
vertexStart: (NSUInteger)vertexStart
|
|
vertexCount: (NSUInteger)vertexCount
|
|
{
|
|
+ if (MTLDrawLimited(vertexCount)) return;
|
|
+ MTLTraceCount("draw");
|
|
+ if (MTLDrawTracking()) MTLDebugLogDraw("direct", vertexCount, 0, 0, 0, nil, 0);
|
|
_encoder->drawPrimitives(static_cast<Indium::PrimitiveType>(primitiveType), vertexStart, vertexCount);
|
|
}
|
|
|
|
@@ -709,7 +1274,10 @@ MTL_UNSUPPORTED_CLASS
|
|
baseVertex: (NSInteger)baseVertex
|
|
baseInstance: (NSUInteger)baseInstance
|
|
{
|
|
- _encoder->drawIndexedPrimitives(static_cast<Indium::PrimitiveType>(primitiveType), indexCount, static_cast<Indium::IndexType>(indexType), ((MTLBufferInternal*)indexBuffer).buffer, indexBufferOffset, instanceCount, baseVertex, baseInstance);
|
|
+ if (MTLDrawLimited(indexCount)) return;
|
|
+ MTLTraceCount("drawIndexed");
|
|
+ if (MTLDrawTracking()) MTLDebugLogDraw("indexed", indexCount, instanceCount, (unsigned long) indexType, (long) baseVertex, indexBuffer, indexBufferOffset);
|
|
+ _encoder->drawIndexedPrimitives(static_cast<Indium::PrimitiveType>(primitiveType), indexCount, static_cast<Indium::IndexType>(indexType), MTLBufferGetIndium(indexBuffer), indexBufferOffset, instanceCount, baseVertex, baseInstance);
|
|
}
|
|
|
|
- (void)drawIndexedPrimitives: (MTLPrimitiveType)primitiveType
|
|
@@ -719,7 +1287,10 @@ MTL_UNSUPPORTED_CLASS
|
|
indexBufferOffset: (NSUInteger)indexBufferOffset
|
|
instanceCount: (NSUInteger)instanceCount
|
|
{
|
|
- _encoder->drawIndexedPrimitives(static_cast<Indium::PrimitiveType>(primitiveType), indexCount, static_cast<Indium::IndexType>(indexType), ((MTLBufferInternal*)indexBuffer).buffer, indexBufferOffset, instanceCount);
|
|
+ if (MTLDrawLimited(indexCount)) return;
|
|
+ MTLTraceCount("drawIndexed");
|
|
+ if (MTLDrawTracking()) MTLDebugLogDraw("indexed", indexCount, instanceCount, (unsigned long) indexType, 0, indexBuffer, indexBufferOffset);
|
|
+ _encoder->drawIndexedPrimitives(static_cast<Indium::PrimitiveType>(primitiveType), indexCount, static_cast<Indium::IndexType>(indexType), MTLBufferGetIndium(indexBuffer), indexBufferOffset, instanceCount);
|
|
}
|
|
|
|
- (void)drawIndexedPrimitives: (MTLPrimitiveType)primitiveType
|
|
@@ -728,7 +1299,10 @@ MTL_UNSUPPORTED_CLASS
|
|
indexBuffer: (id<MTLBuffer>)indexBuffer
|
|
indexBufferOffset: (NSUInteger)indexBufferOffset
|
|
{
|
|
- _encoder->drawIndexedPrimitives(static_cast<Indium::PrimitiveType>(primitiveType), indexCount, static_cast<Indium::IndexType>(indexType), ((MTLBufferInternal*)indexBuffer).buffer, indexBufferOffset);
|
|
+ if (MTLDrawLimited(indexCount)) return;
|
|
+ MTLTraceCount("drawIndexed");
|
|
+ if (MTLDrawTracking()) MTLDebugLogDraw("indexed", indexCount, 0, (unsigned long) indexType, 0, indexBuffer, indexBufferOffset);
|
|
+ _encoder->drawIndexedPrimitives(static_cast<Indium::PrimitiveType>(primitiveType), indexCount, static_cast<Indium::IndexType>(indexType), MTLBufferGetIndium(indexBuffer), indexBufferOffset);
|
|
}
|
|
|
|
#else
|
|
diff --git a/src/Metal/MTLRenderPipeline.mm b/src/Metal/MTLRenderPipeline.mm
|
|
index 8aa709b..140d0da 100644
|
|
--- a/src/Metal/MTLRenderPipeline.mm
|
|
+++ b/src/Metal/MTLRenderPipeline.mm
|
|
@@ -6,6 +6,7 @@
|
|
#import <Metal/MTLLibraryInternal.h>
|
|
#import <Metal/MTLPipelineInternal.h>
|
|
#import <Metal/MTLDevice.h>
|
|
+#import <Metal/MTLVertexDescriptorInternal.h>
|
|
|
|
#include <map>
|
|
|
|
@@ -204,7 +205,7 @@ MTL_UNSUPPORTED_CLASS
|
|
((MTLFunctionInternal*)_fragmentFunction).function,
|
|
_maxVertexCallStackDepth,
|
|
_maxFragmentCallStackDepth,
|
|
- std::nullopt, // TODO
|
|
+ _vertexDescriptor ? std::make_optional([_vertexDescriptor asIndiumDescriptor]) : std::nullopt,
|
|
[_vertexBuffers asIndiumDescriptors],
|
|
[_fragmentBuffers asIndiumDescriptors],
|
|
[_colorAttachments asContiguousIndiumDescriptors],
|
|
@@ -317,7 +318,10 @@ MTL_UNSUPPORTED_CLASS
|
|
std::vector<Indium::RenderPipelineColorAttachmentDescriptor> result;
|
|
|
|
for (const auto& [key, value]: tmp2) {
|
|
- result.push_back(value);
|
|
+ if (value.pixelFormat != Indium::PixelFormat::Invalid) {
|
|
+ result.resize(key + 1);
|
|
+ result[key] = value;
|
|
+ }
|
|
}
|
|
|
|
return result;
|
|
@@ -420,6 +424,10 @@ MTL_UNSUPPORTED_CLASS
|
|
#if DARLING_METAL_ENABLED
|
|
|
|
@synthesize state = _state;
|
|
+const std::shared_ptr<Indium::RenderPipelineState>& MTLRenderPipelineGetIndium(id object) {
|
|
+ static const std::shared_ptr<Indium::RenderPipelineState> none;
|
|
+ return object ? ((MTLRenderPipelineStateInternal*) object)->_state : none;
|
|
+}
|
|
@synthesize device = _device;
|
|
@synthesize label = _label;
|
|
|
|
diff --git a/src/Metal/MTLSamplerDescriptor.mm b/src/Metal/MTLSamplerDescriptor.mm
|
|
index 242d285..f82d083 100644
|
|
--- a/src/Metal/MTLSamplerDescriptor.mm
|
|
+++ b/src/Metal/MTLSamplerDescriptor.mm
|
|
@@ -1,24 +1,69 @@
|
|
-// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
-
|
|
+#import <Metal/MTLSamplerStateInternal.h>
|
|
#import <Metal/MTLSamplerDescriptor.h>
|
|
-#import <Foundation/NSMethodSignature.h>
|
|
-#import <Foundation/NSInvocation.h>
|
|
-
|
|
+#include <float.h>
|
|
@implementation MTLSamplerDescriptor
|
|
-
|
|
+@synthesize minFilter = _minFilter;
|
|
+@synthesize magFilter = _magFilter;
|
|
+@synthesize mipFilter = _mipFilter;
|
|
+@synthesize maxAnisotropy = _maxAnisotropy;
|
|
+@synthesize sAddressMode = _sAddressMode;
|
|
+@synthesize tAddressMode = _tAddressMode;
|
|
+@synthesize rAddressMode = _rAddressMode;
|
|
+@synthesize borderColor = _borderColor;
|
|
+@synthesize normalizedCoordinates = _normalizedCoordinates;
|
|
+@synthesize lodMinClamp = _lodMinClamp;
|
|
+@synthesize lodMaxClamp = _lodMaxClamp;
|
|
+@synthesize supportArgumentBuffers = _supportArgumentBuffers;
|
|
+@synthesize compareFunction = _compareFunction;
|
|
+@synthesize label = _label;
|
|
+- (instancetype)init {
|
|
+ self = [super init];
|
|
+ if (self) {
|
|
+ _maxAnisotropy = 1;
|
|
+ _normalizedCoordinates = YES;
|
|
+ _lodMaxClamp = FLT_MAX;
|
|
+ }
|
|
+ return self;
|
|
+}
|
|
+- (void)dealloc { [_label release]; [super dealloc]; }
|
|
+- (instancetype)copyWithZone:(NSZone*)zone {
|
|
+ MTLSamplerDescriptor* copy = [[[self class] allocWithZone:zone] init];
|
|
+ copy.minFilter = _minFilter;
|
|
+ copy.magFilter = _magFilter;
|
|
+ copy.mipFilter = _mipFilter;
|
|
+ copy.maxAnisotropy = _maxAnisotropy;
|
|
+ copy.sAddressMode = _sAddressMode;
|
|
+ copy.tAddressMode = _tAddressMode;
|
|
+ copy.rAddressMode = _rAddressMode;
|
|
+ copy.borderColor = _borderColor;
|
|
+ copy.normalizedCoordinates = _normalizedCoordinates;
|
|
+ copy.lodMinClamp = _lodMinClamp;
|
|
+ copy.lodMaxClamp = _lodMaxClamp;
|
|
+ copy.supportArgumentBuffers = _supportArgumentBuffers;
|
|
+ copy.compareFunction = _compareFunction;
|
|
+ copy.label = _label;
|
|
+ return copy;
|
|
+}
|
|
+@end
|
|
#if DARLING_METAL_ENABLED
|
|
-
|
|
-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
|
|
-{
|
|
- return [NSMethodSignature signatureWithObjCTypes: "v@:"];
|
|
+@implementation MTLSamplerStateInternal
|
|
+@synthesize sampler = _sampler;
|
|
+const std::shared_ptr<Indium::SamplerState>& MTLSamplerGetIndium(id object) {
|
|
+ static const std::shared_ptr<Indium::SamplerState> none;
|
|
+ return object ? ((MTLSamplerStateInternal*) object)->_sampler : none;
|
|
}
|
|
-
|
|
-- (void)forwardInvocation:(NSInvocation *)anInvocation
|
|
-{
|
|
- NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]);
|
|
+@synthesize device = _device;
|
|
+@synthesize label = _label;
|
|
+- (instancetype)initWithSampler:(std::shared_ptr<Indium::SamplerState>)sampler device:(id<MTLDevice>)device label:(NSString*)label {
|
|
+ self = [super init];
|
|
+ if (self) {
|
|
+ _sampler = sampler;
|
|
+ _device = [device retain];
|
|
+ _label = [label copy];
|
|
+ }
|
|
+ return self;
|
|
}
|
|
-
|
|
-#endif
|
|
-
|
|
+- (void)dealloc { [_device release]; [_label release]; [super dealloc]; }
|
|
@end
|
|
+#endif
|
|
diff --git a/src/Metal/MTLStencilDescriptor.mm b/src/Metal/MTLStencilDescriptor.mm
|
|
index 66a9056..f28235c 100644
|
|
--- a/src/Metal/MTLStencilDescriptor.mm
|
|
+++ b/src/Metal/MTLStencilDescriptor.mm
|
|
@@ -1,24 +1,35 @@
|
|
// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
-#import <Metal/MTLStencilDescriptor.h>
|
|
-#import <Foundation/NSMethodSignature.h>
|
|
-#import <Foundation/NSInvocation.h>
|
|
-
|
|
+#import <Metal/MTLDepthStencilInternal.h>
|
|
@implementation MTLStencilDescriptor
|
|
-
|
|
-#if DARLING_METAL_ENABLED
|
|
-
|
|
-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
|
|
-{
|
|
- return [NSMethodSignature signatureWithObjCTypes: "v@:"];
|
|
+@synthesize stencilCompareFunction = _stencilCompareFunction;
|
|
+@synthesize stencilFailureOperation = _stencilFailureOperation;
|
|
+@synthesize depthFailureOperation = _depthFailureOperation;
|
|
+@synthesize depthStencilPassOperation = _depthStencilPassOperation;
|
|
+@synthesize readMask = _readMask;
|
|
+@synthesize writeMask = _writeMask;
|
|
+- (instancetype)init {
|
|
+ self = [super init];
|
|
+ if (self) { _stencilCompareFunction = MTLCompareFunctionAlways; _readMask = UINT32_MAX; _writeMask = UINT32_MAX; }
|
|
+ return self;
|
|
}
|
|
-
|
|
-- (void)forwardInvocation:(NSInvocation *)anInvocation
|
|
-{
|
|
- NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]);
|
|
+- (id)copyWithZone:(NSZone*)zone {
|
|
+ MTLStencilDescriptor* copy = [[[self class] allocWithZone:zone] init];
|
|
+ copy.stencilCompareFunction = _stencilCompareFunction;
|
|
+ copy.stencilFailureOperation = _stencilFailureOperation;
|
|
+ copy.depthFailureOperation = _depthFailureOperation;
|
|
+ copy.depthStencilPassOperation = _depthStencilPassOperation;
|
|
+ copy.readMask = _readMask;
|
|
+ copy.writeMask = _writeMask;
|
|
+ return copy;
|
|
+}
|
|
+#if DARLING_METAL_ENABLED
|
|
+- (Indium::StencilDescriptor)asIndiumDescriptor {
|
|
+ return {static_cast<Indium::CompareFunction>(_stencilCompareFunction),
|
|
+ static_cast<Indium::StencilOperation>(_stencilFailureOperation),
|
|
+ static_cast<Indium::StencilOperation>(_depthFailureOperation),
|
|
+ static_cast<Indium::StencilOperation>(_depthStencilPassOperation), _readMask, _writeMask};
|
|
}
|
|
-
|
|
#endif
|
|
-
|
|
@end
|
|
diff --git a/src/Metal/MTLTexture.mm b/src/Metal/MTLTexture.mm
|
|
index 6c71fb9..186b8c0 100644
|
|
--- a/src/Metal/MTLTexture.mm
|
|
+++ b/src/Metal/MTLTexture.mm
|
|
@@ -1,3 +1,5 @@
|
|
+#import <Metal/MTLTrace.h>
|
|
+#include <algorithm>
|
|
// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
@@ -7,11 +9,38 @@
|
|
#import <Metal/MTLPixelFormatInternal.h>
|
|
#import <Metal/MTLTypesInternal.h>
|
|
|
|
+
|
|
+#if DARLING_METAL_ENABLED
|
|
+// Diagnostic: CPU texture uploads (replaceRegion) block on the GPU queue, so count, time and sample them.
|
|
+static void MTLDebugReplaceRegion(id<MTLTexture> texture, MTLRegion region, NSUInteger level, const void* bytes, double seconds)
|
|
+{
|
|
+ MTLTraceCount("replaceRegion");
|
|
+ MTLTraceTiming("replaceRegion", seconds);
|
|
+ static unsigned long logged = 0;
|
|
+ if ((MTLDebugEnabled() || MTLTimingEnabled()) && ++logged <= 60) {
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ const unsigned char* b = (const unsigned char*) bytes;
|
|
+ unsigned long nonZero = 0;
|
|
+ // Every pixel format takes at least one byte per pixel, so the first `width` bytes lie inside the first row.
|
|
+ size_t probe = std::min<size_t>(4096, (size_t) region.size.width);
|
|
+ for (size_t i = 0; i < probe; ++i) if (b[i]) ++nonZero;
|
|
+ fprintf(log, "replaceRegion tex=%p %lux%lu fmt=%lu level=%lu region=(%lu,%lu %lux%lu) firstBytesNonZero=%lu/%zu took=%.1fms\n", texture,
|
|
+ (unsigned long) texture.width, (unsigned long) texture.height, (unsigned long) texture.pixelFormat, (unsigned long) level,
|
|
+ (unsigned long) region.origin.x, (unsigned long) region.origin.y, (unsigned long) region.size.width, (unsigned long) region.size.height, nonZero, probe, seconds * 1000);
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+}
|
|
+#endif
|
|
@implementation MTLTextureInternal
|
|
|
|
#if DARLING_METAL_ENABLED
|
|
|
|
@synthesize texture = _texture;
|
|
+const std::shared_ptr<Indium::Texture>& MTLTextureGetIndium(id object) {
|
|
+ static const std::shared_ptr<Indium::Texture> none;
|
|
+ return object ? ((MTLTextureInternal*) object)->_texture : none;
|
|
+}
|
|
@synthesize device = _device;
|
|
@synthesize resourceOptions = _resourceOptions;
|
|
@synthesize label = _label;
|
|
@@ -164,7 +193,9 @@
|
|
withBytes: (const void*)pixelBytes
|
|
bytesPerRow: (NSUInteger)bytesPerRow
|
|
{
|
|
+ double start = (MTLDebugEnabled() || MTLTimingEnabled()) ? MTLTraceNow() : 0;
|
|
_texture->replaceRegion(MTLRegionToIndium(region), level, pixelBytes, bytesPerRow);
|
|
+ if (start != 0) MTLDebugReplaceRegion(self, region, level, pixelBytes, MTLTraceNow() - start);
|
|
}
|
|
|
|
- (void)replaceRegion: (MTLRegion)region
|
|
@@ -174,7 +205,9 @@
|
|
bytesPerRow: (NSUInteger)bytesPerRow
|
|
bytesPerImage: (NSUInteger)bytesPerImage
|
|
{
|
|
+ double start = (MTLDebugEnabled() || MTLTimingEnabled()) ? MTLTraceNow() : 0;
|
|
_texture->replaceRegion(MTLRegionToIndium(region), level, slice, pixelBytes, bytesPerRow, bytesPerImage);
|
|
+ if (start != 0) MTLDebugReplaceRegion(self, region, level, pixelBytes, MTLTraceNow() - start);
|
|
}
|
|
|
|
- (void)getBytes: (void*)pixelBytes
|
|
diff --git a/src/Metal/MTLTextureDescriptor.mm b/src/Metal/MTLTextureDescriptor.mm
|
|
index 37cccb3..79b2350 100644
|
|
--- a/src/Metal/MTLTextureDescriptor.mm
|
|
+++ b/src/Metal/MTLTextureDescriptor.mm
|
|
@@ -1,24 +1,64 @@
|
|
-// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
-
|
|
#import <Metal/MTLTextureDescriptor.h>
|
|
-#import <Foundation/NSMethodSignature.h>
|
|
-#import <Foundation/NSInvocation.h>
|
|
-
|
|
@implementation MTLTextureDescriptor
|
|
-
|
|
-#if DARLING_METAL_ENABLED
|
|
-
|
|
-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
|
|
-{
|
|
- return [NSMethodSignature signatureWithObjCTypes: "v@:"];
|
|
+@synthesize textureType = _textureType;
|
|
+@synthesize pixelFormat = _pixelFormat;
|
|
+@synthesize width = _width;
|
|
+@synthesize height = _height;
|
|
+@synthesize depth = _depth;
|
|
+@synthesize mipmapLevelCount = _mipmapLevelCount;
|
|
+@synthesize sampleCount = _sampleCount;
|
|
+@synthesize arrayLength = _arrayLength;
|
|
+@synthesize resourceOptions = _resourceOptions;
|
|
+@synthesize allowGPUOptimizedContents = _allowGPUOptimizedContents;
|
|
+@synthesize usage = _usage;
|
|
+@synthesize swizzle = _swizzle;
|
|
+- (instancetype)init {
|
|
+ self = [super init];
|
|
+ if (self) {
|
|
+ _textureType = MTLTextureType2D;
|
|
+ _pixelFormat = MTLPixelFormatRGBA8Unorm;
|
|
+ _width = _height = _depth = _mipmapLevelCount = _sampleCount = _arrayLength = 1;
|
|
+ _resourceOptions = MTLResourceStorageModeManaged;
|
|
+ _allowGPUOptimizedContents = YES;
|
|
+ _swizzle = MTLTextureSwizzleChannelsDefault;
|
|
+ }
|
|
+ return self;
|
|
}
|
|
-
|
|
-- (void)forwardInvocation:(NSInvocation *)anInvocation
|
|
-{
|
|
- NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]);
|
|
+- (instancetype)copyWithZone:(NSZone*)zone {
|
|
+ MTLTextureDescriptor* copy = [[[self class] allocWithZone:zone] init];
|
|
+ copy.textureType = _textureType;
|
|
+ copy.pixelFormat = _pixelFormat;
|
|
+ copy.width = _width;
|
|
+ copy.height = _height;
|
|
+ copy.depth = _depth;
|
|
+ copy.mipmapLevelCount = _mipmapLevelCount;
|
|
+ copy.sampleCount = _sampleCount;
|
|
+ copy.arrayLength = _arrayLength;
|
|
+ copy.resourceOptions = _resourceOptions;
|
|
+ copy.allowGPUOptimizedContents = _allowGPUOptimizedContents;
|
|
+ copy.usage = _usage;
|
|
+ copy.swizzle = _swizzle;
|
|
+ return copy;
|
|
+}
|
|
+- (MTLCPUCacheMode)cpuCacheMode { return static_cast<MTLCPUCacheMode>((_resourceOptions >> 0) & 0xf); }
|
|
+- (void)setCpuCacheMode:(MTLCPUCacheMode)value { _resourceOptions = static_cast<MTLResourceOptions>((_resourceOptions & ~(0xful << 0)) | (static_cast<NSUInteger>(value) << 0)); }
|
|
+- (MTLStorageMode)storageMode { return static_cast<MTLStorageMode>((_resourceOptions >> 4) & 0xf); }
|
|
+- (void)setStorageMode:(MTLStorageMode)value { _resourceOptions = static_cast<MTLResourceOptions>((_resourceOptions & ~(0xful << 4)) | (static_cast<NSUInteger>(value) << 4)); }
|
|
+- (MTLHazardTrackingMode)hazardTrackingMode { return static_cast<MTLHazardTrackingMode>((_resourceOptions >> 8) & 0xf); }
|
|
+- (void)setHazardTrackingMode:(MTLHazardTrackingMode)value { _resourceOptions = static_cast<MTLResourceOptions>((_resourceOptions & ~(0xful << 8)) | (static_cast<NSUInteger>(value) << 8)); }
|
|
++ (instancetype)texture2DDescriptorWithPixelFormat:(MTLPixelFormat)format width:(NSUInteger)width height:(NSUInteger)height mipmapped:(BOOL)mipmapped {
|
|
+ MTLTextureDescriptor* desc = [[[self alloc] init] autorelease];
|
|
+ desc.pixelFormat = format; desc.width = width; desc.height = height;
|
|
+ if (mipmapped) {
|
|
+ NSUInteger size = width > height ? width : height;
|
|
+ while (size > 1) { ++desc->_mipmapLevelCount; size >>= 1; }
|
|
+ }
|
|
+ return desc;
|
|
+}
|
|
++ (instancetype)textureCubeDescriptorWithPixelFormat:(MTLPixelFormat)format size:(NSUInteger)size mipmapped:(BOOL)mipmapped {
|
|
+ MTLTextureDescriptor* desc = [self texture2DDescriptorWithPixelFormat:format width:size height:size mipmapped:mipmapped];
|
|
+ desc.textureType = MTLTextureTypeCube;
|
|
+ return desc;
|
|
}
|
|
-
|
|
-#endif
|
|
-
|
|
@end
|
|
diff --git a/src/Metal/MTLVertexDescriptor.mm b/src/Metal/MTLVertexDescriptor.mm
|
|
index cde968d..0a016c6 100644
|
|
--- a/src/Metal/MTLVertexDescriptor.mm
|
|
+++ b/src/Metal/MTLVertexDescriptor.mm
|
|
@@ -1,24 +1,435 @@
|
|
// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
-#import <Metal/MTLVertexDescriptor.h>
|
|
-#import <Foundation/NSMethodSignature.h>
|
|
-#import <Foundation/NSInvocation.h>
|
|
-
|
|
-@implementation MTLVertexDescriptor
|
|
-
|
|
-#if DARLING_METAL_ENABLED
|
|
-
|
|
-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
|
|
+#import <Metal/MTLVertexDescriptorInternal.h>
|
|
+#import <Metal/MTLTrace.h>
|
|
+#import <objc/runtime.h>
|
|
+// The bridge owns these concrete descriptor implementations. Keep their
|
|
+// scalar storage accessible within this translation unit so reset/conversion
|
|
+// can batch it. Subclasses continue through their public property methods.
|
|
+// Every item is created by an array for one slot, and the descriptor resets a slot by writing
|
|
+// the defaults into the item that holds it (in place, so an item a caller kept reads them back).
|
|
+// The array therefore hands each item the word and bit of the descriptor mask that reset reads
|
|
+// (`_dirtyWord`/`_dirtyBit`, see MTLVertexDescriptor below): a setter marks its own slot, and
|
|
+// reset only has to visit the slots that may differ from the defaults instead of every item in
|
|
+// the table. An item that was not handed a slot (a copy, or an array nobody owns) marks nothing,
|
|
+// which is what the "always" mask in the descriptor is for.
|
|
+@interface MTLVertexBufferLayoutDescriptor () {
|
|
+@public
|
|
+ NSUInteger _stride;
|
|
+ MTLVertexStepFunction _stepFunction;
|
|
+ NSUInteger _stepRate;
|
|
+ uint32_t* _dirtyWord; // the descriptor's layout mask, or NULL when the item has no slot
|
|
+ uint32_t _dirtyBit; // 1u << slot, meaningful while _dirtyWord is not NULL
|
|
+}
|
|
+@end
|
|
+@interface MTLVertexAttributeDescriptor () {
|
|
+@public
|
|
+ MTLVertexFormat _format;
|
|
+ NSUInteger _offset;
|
|
+ NSUInteger _bufferIndex;
|
|
+ uint32_t* _dirtyWord; // the descriptor's attribute mask, or NULL when the item has no slot
|
|
+ uint32_t _dirtyBit; // 1u << slot, meaningful while _dirtyWord is not NULL
|
|
+}
|
|
+@end
|
|
+// The masks that make -reset cheap, declared here so the two arrays (same file) can set them.
|
|
+// (darling-lol:) A descriptor's reset used to write the defaults into every item the arrays had
|
|
+// ever handed out - 16-31 attributes plus 16-31 layouts for the game's descriptors, each item a
|
|
+// separate allocation last touched a frame ago, i.e. a cache miss per item even though only the
|
|
+// few slots the game refilled in the previous pass could differ from the defaults. The game
|
|
+// profile put 1.5% of the main thread there (build/perf-research/metal-vdesc/REPORT.md).
|
|
+// `_attrDirty`/`_layoutDirty` name the slots whose values may differ from the defaults and must be
|
|
+// written by the next reset; `_attrPinned`/`_layoutPinned` name the slots whose item is not
|
|
+// tracked (a subclass instance, an item installed with setObject:, or a copy), which are written
|
|
+// every time. Over-marking is always safe - the reset writes the defaults either way - so the
|
|
+// only invariant that has to hold is that a slot whose value can differ from the defaults is
|
|
+// marked. Mutations mark through the item (see MTLVertexDescriptorItemTouched), which is why
|
|
+// every fallback pins the slot instead of tracking it.
|
|
+@interface MTLVertexDescriptor () {
|
|
+@public
|
|
+ MTLVertexBufferLayoutDescriptorArray* _layouts;
|
|
+ MTLVertexAttributeDescriptorArray* _attributes;
|
|
+ uint32_t _attrDirty; // attributes: slots to write at the next reset
|
|
+ uint32_t _attrPinned; // attributes: slots whose item cannot be tracked
|
|
+ uint32_t _layoutDirty; // layouts: slots to write at the next reset
|
|
+ uint32_t _layoutPinned; // layouts: slots whose item cannot be tracked
|
|
+}
|
|
+@end
|
|
+// "Is this item exactly the bridge class?" - the class that declares the plain ivars both the
|
|
+// reset and the marking write. A hook rather than the inline expression it replaces, so the host
|
|
+// model (tools/riot-metal-bind-model) can compile the helpers that call it.
|
|
+static inline bool MTLVertexBufferLayoutItemIsExactClass(const MTLVertexBufferLayoutDescriptor* item)
|
|
{
|
|
- return [NSMethodSignature signatureWithObjCTypes: "v@:"];
|
|
+ return object_getClass((id) item) == [MTLVertexBufferLayoutDescriptor class];
|
|
}
|
|
-
|
|
-- (void)forwardInvocation:(NSInvocation *)anInvocation
|
|
+static inline bool MTLVertexAttributeItemIsExactClass(const MTLVertexAttributeDescriptor* item)
|
|
{
|
|
- NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]);
|
|
+ return object_getClass((id) item) == [MTLVertexAttributeDescriptor class];
|
|
+}
|
|
+// Marking an untouched item is harmless (reset would write the same default value), so the
|
|
+// marking is allowed to over-approximate; it must never miss a mutation. See the model in
|
|
+// tools/riot-metal-bind-model/ for the mutation paths this covers.
|
|
+static inline void MTLVertexDescriptorItemTouched(uint32_t* word, uint32_t bit)
|
|
+{
|
|
+ if (word) *word |= bit;
|
|
+}
|
|
+// The decision a -setObject:atIndexedSubscript: makes for one slot: 1 when the slot's item is one
|
|
+// this descriptor tracks at exactly this slot (the caller marks the dirty bit), 2 when the slot
|
|
+// has to be pinned because the item is not trackable (a copy with no slot, an item of another
|
|
+// descriptor, or the same item at another slot). Only the concrete classes declare the two fields,
|
|
+// so a subclass instance is never passed here (see the caller).
|
|
+static inline int MTLVertexSlotTrackingFor(uint32_t* itemWord, uint32_t itemBit, uint32_t* ownerWord, uint32_t bit)
|
|
+{
|
|
+ if (!ownerWord) return 2;
|
|
+ if (!itemWord) return 2;
|
|
+ if (itemWord != ownerWord) return 2;
|
|
+ return itemBit == bit ? 1 : 2;
|
|
+}
|
|
+static bool MTLVertexDescriptorResetCounters()
|
|
+{
|
|
+ static const bool enabled = MTLDebugEnabled() || access("/tmp/metal-debug-binds", F_OK) == 0;
|
|
+ return enabled;
|
|
+}
|
|
+static void MTLVertexDescriptorResetCount(unsigned long written, unsigned long skipped)
|
|
+{
|
|
+ static std::mutex lock;
|
|
+ static unsigned long calls = 0, writtenTotal = 0, skippedTotal = 0;
|
|
+ static time_t last = 0;
|
|
+ std::lock_guard<std::mutex> guard(lock);
|
|
+ ++calls; writtenTotal += written; skippedTotal += skipped;
|
|
+ time_t now = time(nullptr);
|
|
+ if (now - last >= 3) {
|
|
+ last = now;
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "vdesc-reset@%ld: calls=%lu slotsWritten=%lu slotsAtDefaults=%lu\n",
|
|
+ (long) now, calls, writtenTotal, skippedTotal);
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+}
|
|
+@implementation MTLVertexBufferLayoutDescriptor
|
|
+@synthesize stride = _stride;
|
|
+@synthesize stepFunction = _stepFunction;
|
|
+@synthesize stepRate = _stepRate;
|
|
+// The setters are hand-written so they can mark their slot (over-marking never changes what a
|
|
+// reset writes); the getters stay synthesized.
|
|
+- (void)setStride:(NSUInteger)stride { MTLVertexDescriptorItemTouched(_dirtyWord, _dirtyBit); _stride = stride; }
|
|
+- (void)setStepFunction:(MTLVertexStepFunction)stepFunction { MTLVertexDescriptorItemTouched(_dirtyWord, _dirtyBit); _stepFunction = stepFunction; }
|
|
+- (void)setStepRate:(NSUInteger)stepRate { MTLVertexDescriptorItemTouched(_dirtyWord, _dirtyBit); _stepRate = stepRate; }
|
|
+- (instancetype)init { self = [super init]; if (self) { _stepFunction = MTLVertexStepFunctionPerVertex; _stepRate = 1; } return self; }
|
|
+- (id)copyWithZone:(NSZone*)zone { MTLVertexBufferLayoutDescriptor* copy = [[[self class] allocWithZone:zone] init]; copy.stride = _stride; copy.stepFunction = _stepFunction; copy.stepRate = _stepRate; return copy; }
|
|
+@end
|
|
+// The array storage is visible within this file (same pattern as the item classes above) so that -[MTLVertexDescriptor reset]
|
|
+// can run the in-place reset without a message send per array: the game resets a vertex descriptor for every draw. Both arrays
|
|
+// are only ever created by -init/-reset/-copyWithZone: below, and the two properties are readonly, so the storage a descriptor
|
|
+// holds is always the concrete class declared here.
|
|
+@interface MTLVertexBufferLayoutDescriptorArray () {
|
|
+@public
|
|
+ MTLVertexBufferLayoutDescriptor* _items[31];
|
|
+ uint32_t _present; // bit i: _items[i] exists
|
|
+ uint32_t _concrete; // bit i: _items[i] is exactly MTLVertexBufferLayoutDescriptor (ivars can be written directly)
|
|
+ MTLVertexDescriptor* _owner; // the descriptor whose mask the items mark (not retained; nil after it dies)
|
|
+}
|
|
+- (MTLVertexBufferLayoutDescriptor*)existingObjectAtIndex:(NSUInteger)index;
|
|
+- (void)resetAll;
|
|
+@end
|
|
+@interface MTLVertexAttributeDescriptorArray () {
|
|
+@public
|
|
+ MTLVertexAttributeDescriptor* _items[31];
|
|
+ uint32_t _present; // bit i: _items[i] exists
|
|
+ uint32_t _concrete; // bit i: _items[i] is exactly MTLVertexAttributeDescriptor (ivars can be written directly)
|
|
+ MTLVertexDescriptor* _owner; // the descriptor whose mask the items mark (not retained; nil after it dies)
|
|
+}
|
|
+- (MTLVertexAttributeDescriptor*)existingObjectAtIndex:(NSUInteger)index;
|
|
+- (void)resetAll;
|
|
+@end
|
|
+// A present-but-non-concrete entry is a caller-installed subclass instance (see
|
|
+// -setObject:atIndexedSubscript:): it keeps its own property setters. Kept out of
|
|
+// line so the common all-concrete loop does not carry the selector references and
|
|
+// the message-send stub in its preheader.
|
|
+__attribute__((noinline)) static void MTLVertexBufferLayoutSetDefaults(MTLVertexBufferLayoutDescriptor* item)
|
|
+{
|
|
+ item.stride = 0; item.stepFunction = MTLVertexStepFunctionPerVertex; item.stepRate = 1;
|
|
+}
|
|
+__attribute__((noinline)) static void MTLVertexAttributeSetDefaults(MTLVertexAttributeDescriptor* item)
|
|
+{
|
|
+ item.format = MTLVertexFormatInvalid; item.offset = 0; item.bufferIndex = 0;
|
|
+}
|
|
+// Reset the slots whose mask bit is set, in ascending order like the old full-table loop: an
|
|
+// object a caller still holds must read back the defaults, and absent (nil) slots stay absent.
|
|
+// Exact-class entries are written through their ivars; an entry that is not exactly the bridge
|
|
+// class (a subclass an array was handed, or a graft) keeps its own property setters.
|
|
+static void MTLVertexBufferLayoutArrayResetSlots(MTLVertexBufferLayoutDescriptorArray* array, uint32_t slots)
|
|
+{
|
|
+ const uint32_t exotic = slots & ~array->_concrete;
|
|
+ for (uint32_t bits = slots; bits; bits &= bits - 1) {
|
|
+ const int i = __builtin_ctz(bits);
|
|
+ MTLVertexBufferLayoutDescriptor* item = array->_items[i];
|
|
+ if (!item) continue;
|
|
+ if (exotic & (1u << i)) {
|
|
+ MTLVertexBufferLayoutSetDefaults(item);
|
|
+ } else {
|
|
+ item->_stride = 0; item->_stepFunction = MTLVertexStepFunctionPerVertex; item->_stepRate = 1;
|
|
+ }
|
|
+ }
|
|
+}
|
|
+static void MTLVertexAttributeArrayResetSlots(MTLVertexAttributeDescriptorArray* array, uint32_t slots)
|
|
+{
|
|
+ const uint32_t exotic = slots & ~array->_concrete;
|
|
+ for (uint32_t bits = slots; bits; bits &= bits - 1) {
|
|
+ const int i = __builtin_ctz(bits);
|
|
+ MTLVertexAttributeDescriptor* item = array->_items[i];
|
|
+ if (!item) continue;
|
|
+ if (exotic & (1u << i)) {
|
|
+ MTLVertexAttributeSetDefaults(item);
|
|
+ } else {
|
|
+ item->_format = MTLVertexFormatInvalid; item->_offset = 0; item->_bufferIndex = 0;
|
|
+ }
|
|
+ }
|
|
+}
|
|
+// An item that leaves its slot (a -setObject: replaces it, or installs nil) may still be held by
|
|
+// the caller. If no slot of this array holds it any more, its mask pointer must be cleared: the
|
|
+// descriptor's -dealloc only revokes the items its arrays still hold, so a stale pointer would
|
|
+// write into the descriptor's storage after it is gone. An item that is still in a slot must keep
|
|
+// its pointer - that slot is tracked through it.
|
|
+static void MTLVertexBufferLayoutArrayRetireItem(MTLVertexBufferLayoutDescriptorArray* array, MTLVertexBufferLayoutDescriptor* item)
|
|
+{
|
|
+ if (!item || !array->_owner) return;
|
|
+ if (!MTLVertexBufferLayoutItemIsExactClass(item)) return;
|
|
+ if (item->_dirtyWord != &array->_owner->_layoutDirty) return;
|
|
+ for (int i = 0; i < 31; ++i) if (array->_items[i] == item) return;
|
|
+ item->_dirtyWord = NULL; item->_dirtyBit = 0;
|
|
+}
|
|
+static void MTLVertexAttributeArrayRetireItem(MTLVertexAttributeDescriptorArray* array, MTLVertexAttributeDescriptor* item)
|
|
+{
|
|
+ if (!item || !array->_owner) return;
|
|
+ if (!MTLVertexAttributeItemIsExactClass(item)) return;
|
|
+ if (item->_dirtyWord != &array->_owner->_attrDirty) return;
|
|
+ for (int i = 0; i < 31; ++i) if (array->_items[i] == item) return;
|
|
+ item->_dirtyWord = NULL; item->_dirtyBit = 0;
|
|
+}
|
|
+// The arrays are released with the descriptor, but a caller may hold an array (or an item) past
|
|
+// it: the array must then not reach the dead descriptor again, and an item that still points at
|
|
+// the descriptor's mask must not mark it. Both are cleared here, while the items are alive (the
|
|
+// descriptor keeps the arrays, and they keep the items).
|
|
+static void MTLVertexBufferLayoutArrayRevokeItems(MTLVertexBufferLayoutDescriptorArray* array, uint32_t* word)
|
|
+{
|
|
+ if (!array) return;
|
|
+ array->_owner = nil;
|
|
+ for (uint32_t bits = array->_present; bits; bits &= bits - 1) {
|
|
+ const int i = __builtin_ctz(bits);
|
|
+ MTLVertexBufferLayoutDescriptor* item = array->_items[i];
|
|
+ if (item && MTLVertexBufferLayoutItemIsExactClass(item) && item->_dirtyWord == word) {
|
|
+ item->_dirtyWord = NULL; item->_dirtyBit = 0;
|
|
+ }
|
|
+ }
|
|
+}
|
|
+static void MTLVertexAttributeArrayRevokeItems(MTLVertexAttributeDescriptorArray* array, uint32_t* word)
|
|
+{
|
|
+ if (!array) return;
|
|
+ array->_owner = nil;
|
|
+ for (uint32_t bits = array->_present; bits; bits &= bits - 1) {
|
|
+ const int i = __builtin_ctz(bits);
|
|
+ MTLVertexAttributeDescriptor* item = array->_items[i];
|
|
+ if (item && MTLVertexAttributeItemIsExactClass(item) && item->_dirtyWord == word) {
|
|
+ item->_dirtyWord = NULL; item->_dirtyBit = 0;
|
|
+ }
|
|
+ }
|
|
+}
|
|
+// The arrays used to be NSMutableDictionaries keyed by boxed NSNumbers, and converting a descriptor created all 31 entries;
|
|
+// under a game that builds vertex descriptors constantly that was ~30% of the render thread. Fixed storage, entries created lazily.
|
|
+@implementation MTLVertexBufferLayoutDescriptorArray
|
|
+- (void)dealloc { for (int i = 0; i < 31; ++i) [_items[i] release]; [super dealloc]; }
|
|
+- (MTLVertexBufferLayoutDescriptor*)objectAtIndexedSubscript:(NSUInteger)index {
|
|
+ if (index >= 31) [NSException raise:NSRangeException format:@"Vertex descriptor index %lu exceeds 30", (unsigned long)index];
|
|
+ if (!_items[index]) {
|
|
+ _items[index] = [MTLVertexBufferLayoutDescriptor new]; _present |= 1u << index; _concrete |= 1u << index;
|
|
+ // a fresh item holds the defaults, so nothing is marked until it is written; the slot is
|
|
+ // what a later setter marks
|
|
+ if (_owner) { _items[index]->_dirtyWord = &_owner->_layoutDirty; _items[index]->_dirtyBit = 1u << index; }
|
|
+ }
|
|
+ return _items[index];
|
|
+}
|
|
+- (MTLVertexBufferLayoutDescriptor*)existingObjectAtIndex:(NSUInteger)index { return index < 31 ? _items[index] : nil; }
|
|
+// reset in place: the game calls -[MTLVertexDescriptor reset] for every draw and then refills it, so keep the objects
|
|
+- (void)resetAll {
|
|
+ MTLVertexBufferLayoutArrayResetSlots(self, _present);
|
|
+ if (_owner) _owner->_layoutDirty &= ~_present;
|
|
+}
|
|
+- (void)setObject:(MTLVertexBufferLayoutDescriptor*)descriptor atIndexedSubscript:(NSUInteger)index {
|
|
+ if (index >= 31) [NSException raise:NSRangeException format:@"Vertex descriptor index %lu exceeds 30", (unsigned long)index];
|
|
+ MTLVertexBufferLayoutDescriptor* old = _items[index];
|
|
+ _items[index] = [descriptor retain];
|
|
+ MTLVertexBufferLayoutArrayRetireItem(self, old);
|
|
+ [old release];
|
|
+ const uint32_t bit = 1u << index;
|
|
+ if (descriptor) _present |= bit; else _present &= ~bit;
|
|
+ if (descriptor && object_getClass(descriptor) == [MTLVertexBufferLayoutDescriptor class]) _concrete |= bit; else _concrete &= ~bit;
|
|
+ // The next reset must write this slot unless its item is one this descriptor tracks at exactly
|
|
+ // this slot: a subclass with its own setters, an item that belongs to another descriptor or
|
|
+ // another slot, or an item with no slot at all is marked "always" instead, which keeps the
|
|
+ // reset exact (over-marking never changes what a reset writes).
|
|
+ if (_owner) {
|
|
+ if (!descriptor) {
|
|
+ _owner->_layoutDirty &= ~bit;
|
|
+ _owner->_layoutPinned &= ~bit;
|
|
+ } else {
|
|
+ const int tracking = MTLVertexBufferLayoutItemIsExactClass(descriptor)
|
|
+ ? MTLVertexSlotTrackingFor(descriptor->_dirtyWord, descriptor->_dirtyBit, &_owner->_layoutDirty, bit)
|
|
+ : 2;
|
|
+ if (tracking == 1) _owner->_layoutDirty |= bit; else _owner->_layoutPinned |= bit;
|
|
+ }
|
|
+ }
|
|
+}
|
|
+- (id)copyWithZone:(NSZone*)zone {
|
|
+ MTLVertexBufferLayoutDescriptorArray* copy = [[[self class] allocWithZone:zone] init];
|
|
+ for (int i = 0; i < 31; ++i) if (_items[i]) copy->_items[i] = [_items[i] copy];
|
|
+ copy->_present = _present;
|
|
+ copy->_concrete = _concrete; // -copy of a concrete item returns the same class
|
|
+ // the copied items are new objects and are not tracked; the descriptor that adopts this array
|
|
+ // binds them and marks their slots (see MTLVertexDescriptor -copyWithZone:)
|
|
+ return copy;
|
|
+}
|
|
+@end
|
|
+@implementation MTLVertexAttributeDescriptor
|
|
+@synthesize format = _format;
|
|
+@synthesize offset = _offset;
|
|
+@synthesize bufferIndex = _bufferIndex;
|
|
+// Hand-written for the same reason as MTLVertexBufferLayoutDescriptor's: the setter marks the
|
|
+// descriptor slot this item fills.
|
|
+- (void)setFormat:(MTLVertexFormat)format { MTLVertexDescriptorItemTouched(_dirtyWord, _dirtyBit); _format = format; }
|
|
+- (void)setOffset:(NSUInteger)offset { MTLVertexDescriptorItemTouched(_dirtyWord, _dirtyBit); _offset = offset; }
|
|
+- (void)setBufferIndex:(NSUInteger)bufferIndex { MTLVertexDescriptorItemTouched(_dirtyWord, _dirtyBit); _bufferIndex = bufferIndex; }
|
|
+- (id)copyWithZone:(NSZone*)zone { MTLVertexAttributeDescriptor* copy = [[[self class] allocWithZone:zone] init]; copy.format = _format; copy.offset = _offset; copy.bufferIndex = _bufferIndex; return copy; }
|
|
+@end
|
|
+@implementation MTLVertexAttributeDescriptorArray
|
|
+- (void)dealloc { for (int i = 0; i < 31; ++i) [_items[i] release]; [super dealloc]; }
|
|
+- (MTLVertexAttributeDescriptor*)objectAtIndexedSubscript:(NSUInteger)index {
|
|
+ if (index >= 31) [NSException raise:NSRangeException format:@"Vertex descriptor index %lu exceeds 30", (unsigned long)index];
|
|
+ if (!_items[index]) {
|
|
+ _items[index] = [MTLVertexAttributeDescriptor new]; _present |= 1u << index; _concrete |= 1u << index;
|
|
+ // a fresh item holds the defaults; the slot is what a later setter marks
|
|
+ if (_owner) { _items[index]->_dirtyWord = &_owner->_attrDirty; _items[index]->_dirtyBit = 1u << index; }
|
|
+ }
|
|
+ return _items[index];
|
|
+}
|
|
+- (MTLVertexAttributeDescriptor*)existingObjectAtIndex:(NSUInteger)index { return index < 31 ? _items[index] : nil; }
|
|
+- (void)resetAll {
|
|
+ MTLVertexAttributeArrayResetSlots(self, _present);
|
|
+ if (_owner) _owner->_attrDirty &= ~_present;
|
|
+}
|
|
+- (void)setObject:(MTLVertexAttributeDescriptor*)descriptor atIndexedSubscript:(NSUInteger)index {
|
|
+ if (index >= 31) [NSException raise:NSRangeException format:@"Vertex descriptor index %lu exceeds 30", (unsigned long)index];
|
|
+ MTLVertexAttributeDescriptor* old = _items[index];
|
|
+ _items[index] = [descriptor retain];
|
|
+ MTLVertexAttributeArrayRetireItem(self, old);
|
|
+ [old release];
|
|
+ const uint32_t bit = 1u << index;
|
|
+ if (descriptor) _present |= bit; else _present &= ~bit;
|
|
+ if (descriptor && object_getClass(descriptor) == [MTLVertexAttributeDescriptor class]) _concrete |= bit; else _concrete &= ~bit;
|
|
+ // see MTLVertexBufferLayoutDescriptorArray -setObject:atIndexedSubscript: for the rule
|
|
+ if (_owner) {
|
|
+ if (!descriptor) {
|
|
+ _owner->_attrDirty &= ~bit;
|
|
+ _owner->_attrPinned &= ~bit;
|
|
+ } else {
|
|
+ const int tracking = MTLVertexAttributeItemIsExactClass(descriptor)
|
|
+ ? MTLVertexSlotTrackingFor(descriptor->_dirtyWord, descriptor->_dirtyBit, &_owner->_attrDirty, bit)
|
|
+ : 2;
|
|
+ if (tracking == 1) _owner->_attrDirty |= bit; else _owner->_attrPinned |= bit;
|
|
+ }
|
|
+ }
|
|
+}
|
|
+- (id)copyWithZone:(NSZone*)zone {
|
|
+ MTLVertexAttributeDescriptorArray* copy = [[[self class] allocWithZone:zone] init];
|
|
+ for (int i = 0; i < 31; ++i) if (_items[i]) copy->_items[i] = [_items[i] copy];
|
|
+ copy->_present = _present;
|
|
+ copy->_concrete = _concrete; // -copy of a concrete item returns the same class
|
|
+ return copy;
|
|
+}
|
|
+@end
|
|
+// The reset work itself, kept in a static so the host model can run the shipping code (see
|
|
+// tools/riot-metal-bind-model/extract-vertex-reset.py): the mask words name the slots whose values
|
|
+// may differ from the defaults, and the helpers write exactly those, in ascending slot order.
|
|
+static void MTLVertexDescriptorResetMarks(MTLVertexDescriptor* descriptor)
|
|
+{
|
|
+ const uint32_t layouts = descriptor->_layoutDirty | descriptor->_layoutPinned;
|
|
+ const uint32_t attributes = descriptor->_attrDirty | descriptor->_attrPinned;
|
|
+ descriptor->_layoutDirty = descriptor->_layoutPinned;
|
|
+ descriptor->_attrDirty = descriptor->_attrPinned;
|
|
+ MTLVertexBufferLayoutArrayResetSlots(descriptor->_layouts, layouts);
|
|
+ MTLVertexAttributeArrayResetSlots(descriptor->_attributes, attributes);
|
|
+ if (MTLVertexDescriptorResetCounters()) {
|
|
+ MTLVertexDescriptorResetCount(__builtin_popcount(layouts) + __builtin_popcount(attributes),
|
|
+ __builtin_popcount(descriptor->_layouts->_present & ~layouts) + __builtin_popcount(descriptor->_attributes->_present & ~attributes));
|
|
+ }
|
|
+}
|
|
+// Guest diagnosis, same shape as the bind counters in MTLRenderCommandEncoder.mm: with
|
|
+// /tmp/metal-debug-binds (or the trace flag) present every reset reports how many slots it had to
|
|
+// write and how many it left at the defaults, so the next match measures what the marks save.
|
|
+// The flag is tested inline at the call site, so an ordinary match pays one load and a branch.
|
|
+@implementation MTLVertexDescriptor
|
|
+@synthesize layouts = _layouts;
|
|
+@synthesize attributes = _attributes;
|
|
++ (instancetype)vertexDescriptor { return [[[self alloc] init] autorelease]; }
|
|
+- (instancetype)init { self = [super init]; if (self) [self reset]; return self; }
|
|
+- (void)dealloc {
|
|
+ // an item (or an array) a caller kept must not reach this descriptor's storage after this
|
|
+ MTLVertexBufferLayoutArrayRevokeItems(_layouts, &_layoutDirty);
|
|
+ MTLVertexAttributeArrayRevokeItems(_attributes, &_attrDirty);
|
|
+ [_layouts release]; [_attributes release]; [super dealloc];
|
|
+}
|
|
+- (void)reset {
|
|
+ if (_layouts && _attributes) {
|
|
+ // Only the slots whose values may differ from the defaults are written, in the same
|
|
+ // ascending order as before; the rest were left at the defaults by the previous reset (or
|
|
+ // hold a fresh item), so writing them would change nothing. The dirty marks are cleared as
|
|
+ // the slots are written; the pinned marks stay (their items are written every time).
|
|
+ MTLVertexDescriptorResetMarks(self);
|
|
+ return;
|
|
+ }
|
|
+ [_layouts release]; [_attributes release];
|
|
+ _layouts = [MTLVertexBufferLayoutDescriptorArray new];
|
|
+ _attributes = [MTLVertexAttributeDescriptorArray new];
|
|
+ ((MTLVertexBufferLayoutDescriptorArray*)_layouts)->_owner = self;
|
|
+ ((MTLVertexAttributeDescriptorArray*)_attributes)->_owner = self;
|
|
+ _attrDirty = 0; _attrPinned = 0; _layoutDirty = 0; _layoutPinned = 0;
|
|
+}
|
|
+- (id)copyWithZone:(NSZone*)zone {
|
|
+ MTLVertexDescriptor* copy = [[[self class] allocWithZone:zone] init];
|
|
+ [copy->_layouts release]; [copy->_attributes release];
|
|
+ copy->_layouts = [_layouts copy]; copy->_attributes = [_attributes copy];
|
|
+ // the copied items are new objects nothing tracks; bind them to the copy and let its resets
|
|
+ // write every slot they occupy (a copy is rare, and this keeps it exact without tracking it)
|
|
+ MTLVertexBufferLayoutDescriptorArray* layouts = copy->_layouts;
|
|
+ MTLVertexAttributeDescriptorArray* attributes = copy->_attributes;
|
|
+ layouts->_owner = copy;
|
|
+ attributes->_owner = copy;
|
|
+ copy->_layoutPinned = layouts->_present;
|
|
+ copy->_attrPinned = attributes->_present;
|
|
+ return copy;
|
|
+}
|
|
+#if DARLING_METAL_ENABLED
|
|
+- (Indium::VertexDescriptor)asIndiumDescriptor {
|
|
+ static Class attributeClass = [MTLVertexAttributeDescriptor class];
|
|
+ static Class layoutClass = [MTLVertexBufferLayoutDescriptor class];
|
|
+ Indium::VertexDescriptor result;
|
|
+ for (NSUInteger i = 0; i < 31; ++i) {
|
|
+ MTLVertexAttributeDescriptor* attr = [_attributes existingObjectAtIndex: i];
|
|
+ if (!attr) continue;
|
|
+ const bool concrete = object_getClass(attr) == attributeClass;
|
|
+ const auto format = concrete ? attr->_format : attr.format;
|
|
+ if (format == MTLVertexFormatInvalid) continue;
|
|
+ const auto offset = concrete ? attr->_offset : attr.offset;
|
|
+ const auto index = concrete ? attr->_bufferIndex : attr.bufferIndex;
|
|
+ result.attributes[i] = {static_cast<Indium::VertexFormat>(format), offset, index};
|
|
+ MTLVertexBufferLayoutDescriptor* layout = _layouts[index];
|
|
+ if (object_getClass(layout) == layoutClass)
|
|
+ result.layouts[index] = {layout->_stride, static_cast<Indium::VertexStepFunction>(layout->_stepFunction), layout->_stepRate};
|
|
+ else
|
|
+ result.layouts[index] = {layout.stride, static_cast<Indium::VertexStepFunction>(layout.stepFunction), layout.stepRate};
|
|
+ }
|
|
+ return result;
|
|
}
|
|
-
|
|
#endif
|
|
-
|
|
@end
|
|
diff --git a/include/Metal/MTLArgument.h b/include/Metal/MTLArgument.h
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/include/Metal/MTLArgument.h
|
|
@@ -0,0 +1,29 @@
|
|
+#ifndef _METAL_MTLARGUMENT_H_
|
|
+#define _METAL_MTLARGUMENT_H_
|
|
+#import <Foundation/Foundation.h>
|
|
+typedef NS_ENUM(NSUInteger, MTLArgumentType) { MTLArgumentTypeBuffer=0, MTLArgumentTypeThreadgroupMemory=1, MTLArgumentTypeTexture=2, MTLArgumentTypeSampler=3 };
|
|
+@interface MTLStructMember : NSObject
|
|
+@property(readonly) NSString* name;
|
|
+@property(readonly) NSUInteger offset;
|
|
+@end
|
|
+@interface MTLStructType : NSObject
|
|
+@property(readonly) NSArray<MTLStructMember*>* members;
|
|
+- (MTLStructMember*)memberByName:(NSString*)name;
|
|
+@end
|
|
+@interface MTLArgument : NSObject
|
|
+@property(readonly) NSString* name;
|
|
+@property(readonly) MTLArgumentType type;
|
|
+@property(readonly) NSUInteger index;
|
|
+@property(readonly, getter=isActive) BOOL active;
|
|
+@property(readonly) NSUInteger bufferAlignment;
|
|
+@property(readonly) NSUInteger bufferDataSize;
|
|
+@property(readonly) MTLStructType* bufferStructType;
|
|
+@end
|
|
+@interface MTLComputePipelineReflection : NSObject
|
|
+@property(readonly) NSArray<MTLArgument*>* arguments;
|
|
+@end
|
|
+@interface MTLRenderPipelineReflection : NSObject
|
|
+@property(readonly) NSArray<MTLArgument*>* vertexArguments;
|
|
+@property(readonly) NSArray<MTLArgument*>* fragmentArguments;
|
|
+@end
|
|
+#endif
|
|
diff --git a/include/Metal/MTLBlitCommandEncoder.h b/include/Metal/MTLBlitCommandEncoder.h
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/include/Metal/MTLBlitCommandEncoder.h
|
|
@@ -0,0 +1,28 @@
|
|
+// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
+// SPDX-License-Identifier: MPL-2.0
|
|
+
|
|
+#ifndef _METAL_MTLBLITCOMMANDENCODER_H_
|
|
+#define _METAL_MTLBLITCOMMANDENCODER_H_
|
|
+#import <Metal/MTLCommandEncoder.h>
|
|
+#import <Metal/MTLTypes.h>
|
|
+@protocol MTLBuffer;
|
|
+@protocol MTLTexture;
|
|
+typedef NS_OPTIONS(NSUInteger, MTLBlitOption) {
|
|
+ MTLBlitOptionNone = 0,
|
|
+ MTLBlitOptionDepthFromDepthStencil = 1,
|
|
+ MTLBlitOptionStencilFromDepthStencil = 2,
|
|
+ MTLBlitOptionRowLinearPVRTC = 4,
|
|
+};
|
|
+@protocol MTLBlitCommandEncoder <MTLCommandEncoder>
|
|
+- (void)copyFromBuffer:(id<MTLBuffer>)source sourceOffset:(NSUInteger)sourceOffset toBuffer:(id<MTLBuffer>)destination destinationOffset:(NSUInteger)destinationOffset size:(NSUInteger)size;
|
|
+- (void)copyFromBuffer:(id<MTLBuffer>)source sourceOffset:(NSUInteger)sourceOffset sourceBytesPerRow:(NSUInteger)sourceBytesPerRow sourceBytesPerImage:(NSUInteger)sourceBytesPerImage sourceSize:(MTLSize)sourceSize toTexture:(id<MTLTexture>)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel destinationOrigin:(MTLOrigin)destinationOrigin;
|
|
+- (void)copyFromTexture:(id<MTLTexture>)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toBuffer:(id<MTLBuffer>)destination destinationOffset:(NSUInteger)destinationOffset destinationBytesPerRow:(NSUInteger)destinationBytesPerRow destinationBytesPerImage:(NSUInteger)destinationBytesPerImage;
|
|
+- (void)copyFromTexture:(id<MTLTexture>)source toTexture:(id<MTLTexture>)destination;
|
|
+- (void)copyFromTexture:(id<MTLTexture>)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel toTexture:(id<MTLTexture>)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel sliceCount:(NSUInteger)sliceCount levelCount:(NSUInteger)levelCount;
|
|
+- (void)copyFromTexture:(id<MTLTexture>)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toTexture:(id<MTLTexture>)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel destinationOrigin:(MTLOrigin)destinationOrigin;
|
|
+- (void)fillBuffer:(id<MTLBuffer>)buffer range:(NSRange)range value:(uint8_t)value;
|
|
+- (void)generateMipmapsForTexture:(id<MTLTexture>)texture;
|
|
+- (void)copyFromBuffer:(id<MTLBuffer>)source sourceOffset:(NSUInteger)sourceOffset sourceBytesPerRow:(NSUInteger)sourceBytesPerRow sourceBytesPerImage:(NSUInteger)sourceBytesPerImage sourceSize:(MTLSize)sourceSize toTexture:(id<MTLTexture>)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel destinationOrigin:(MTLOrigin)destinationOrigin options:(MTLBlitOption)options;
|
|
+- (void)copyFromTexture:(id<MTLTexture>)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toBuffer:(id<MTLBuffer>)destination destinationOffset:(NSUInteger)destinationOffset destinationBytesPerRow:(NSUInteger)destinationBytesPerRow destinationBytesPerImage:(NSUInteger)destinationBytesPerImage options:(MTLBlitOption)options;
|
|
+@end
|
|
+#endif
|
|
diff --git a/include/Metal/MTLExceptionBridge.h b/include/Metal/MTLExceptionBridge.h
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/include/Metal/MTLExceptionBridge.h
|
|
@@ -0,0 +1,23 @@
|
|
+#pragma once
|
|
+#import <Foundation/Foundation.h>
|
|
+#include <cstdio>
|
|
+#include <exception>
|
|
+#include <string>
|
|
+
|
|
+// Metal reports failures with nil and an NSError, while the Indium backend reports them with C++
|
|
+// exceptions, which the callers of the Metal API do not expect. Call from a catch block: builds the
|
|
+// NSError from the active exception and records it in the guest's /tmp/metal-errors.log.
|
|
+static inline NSError* MTLErrorFromCurrentException(const char* where)
|
|
+{
|
|
+ std::string message = "unknown error";
|
|
+ try { throw; }
|
|
+ catch (const std::exception& e) { message = e.what(); }
|
|
+ catch (...) {}
|
|
+
|
|
+ if (FILE* log = fopen("/tmp/metal-errors.log", "a")) {
|
|
+ fprintf(log, "%s: %s\n", where, message.c_str());
|
|
+ fclose(log);
|
|
+ }
|
|
+ return [NSError errorWithDomain: @"MTLLibraryErrorDomain" code: 3
|
|
+ userInfo: @{NSLocalizedDescriptionKey: [NSString stringWithUTF8String: message.c_str()]}];
|
|
+}
|
|
diff --git a/include/Metal/MTLTrace.h b/include/Metal/MTLTrace.h
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/include/Metal/MTLTrace.h
|
|
@@ -0,0 +1,106 @@
|
|
+#pragma once
|
|
+#include <atomic>
|
|
+#include <cstdio>
|
|
+#include <unistd.h>
|
|
+#include <ctime>
|
|
+#include <map>
|
|
+#include <mutex>
|
|
+#include <string>
|
|
+
|
|
+// All the Metal diagnostics (counters, render-pass descriptions, the magenta overrides, drawable readbacks) are
|
|
+// off unless /tmp/metal-debug-trace exists when the process starts.
|
|
+inline bool MTLDebugEnabled()
|
|
+{
|
|
+ static const bool enabled = access("/tmp/metal-debug-trace", F_OK) == 0;
|
|
+ return enabled;
|
|
+}
|
|
+
|
|
+// Total draw calls seen so far, used to attribute draws to render passes.
|
|
+inline std::atomic<unsigned long>& MTLTraceDrawTotal()
|
|
+{
|
|
+ static std::atomic<unsigned long> total { 0 };
|
|
+ return total;
|
|
+}
|
|
+
|
|
+// Diagnostic event counters, appended to the guest's /tmp/metal-trace.log every few seconds.
|
|
+inline void MTLTraceCount(const char* name)
|
|
+{
|
|
+ static const bool countDraws = MTLDebugEnabled() || access("/tmp/metal-debug-timing", F_OK) == 0 || access("/tmp/metal-debug-snapshotdraws", F_OK) == 0;
|
|
+ if (countDraws && name[0] == 'd' && name[1] == 'r' && name[2] == 'a' && name[3] == 'w') {
|
|
+ ++MTLTraceDrawTotal();
|
|
+ }
|
|
+ if (!MTLDebugEnabled()) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ static std::mutex lock;
|
|
+ static std::map<std::string, unsigned long> counts;
|
|
+ static time_t last = 0;
|
|
+
|
|
+ std::lock_guard<std::mutex> guard(lock);
|
|
+ ++counts[name];
|
|
+
|
|
+ time_t now = time(nullptr);
|
|
+ if (now - last >= 3) {
|
|
+ last = now;
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "counts@%ld:", (long) now);
|
|
+ for (auto& count: counts) {
|
|
+ fprintf(log, " %s=%lu", count.first.c_str(), count.second);
|
|
+ }
|
|
+ fprintf(log, "\n");
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+// Cheap aggregated timings (flag file /tmp/metal-debug-timing in the guest when the process starts), printed every 5 s.
|
|
+inline void MTLTraceTiming(const char* what, double seconds)
|
|
+{
|
|
+ static const bool enabled = access("/tmp/metal-debug-timing", F_OK) == 0;
|
|
+ if (!enabled) {
|
|
+ return;
|
|
+ }
|
|
+ struct Stat { double sum = 0, max = 0; unsigned long count = 0; };
|
|
+ static std::mutex lock;
|
|
+ static std::map<std::string, Stat> stats;
|
|
+ static time_t last = 0;
|
|
+ std::lock_guard<std::mutex> guard(lock);
|
|
+ Stat& stat = stats[what];
|
|
+ stat.sum += seconds;
|
|
+ stat.count++;
|
|
+ if (seconds > stat.max) stat.max = seconds;
|
|
+ time_t now = time(nullptr);
|
|
+ if (now - last >= 5) {
|
|
+ last = now;
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "mtl-timing@%ld:", (long) now);
|
|
+ for (auto& entry: stats) {
|
|
+ fprintf(log, " %s[n=%lu avg=%.1fms max=%.1fms]", entry.first.c_str(), entry.second.count, entry.second.sum / entry.second.count * 1000, entry.second.max * 1000);
|
|
+ }
|
|
+ fprintf(log, "\n");
|
|
+ fclose(log);
|
|
+ }
|
|
+ stats.clear();
|
|
+ }
|
|
+}
|
|
+
|
|
+inline bool MTLTimingEnabled()
|
|
+{
|
|
+ static const bool enabled = access("/tmp/metal-debug-timing", F_OK) == 0;
|
|
+ return enabled;
|
|
+}
|
|
+
|
|
+inline double MTLTraceNow()
|
|
+{
|
|
+ struct timespec ts;
|
|
+ clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
+ return ts.tv_sec + ts.tv_nsec / 1e9;
|
|
+}
|
|
+
|
|
+// Draw-state tracking (cheap bookkeeping for the draw log): on with the trace flag, or with /tmp/metal-debug-draws.
|
|
+inline bool MTLDrawTracking()
|
|
+{
|
|
+ static const bool enabled = MTLDebugEnabled() || access("/tmp/metal-debug-draws", F_OK) == 0;
|
|
+ return enabled;
|
|
+}
|
|
diff --git a/private-include/Metal/MTLArgumentInternal.h b/private-include/Metal/MTLArgumentInternal.h
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/Metal/MTLArgumentInternal.h
|
|
@@ -0,0 +1,15 @@
|
|
+#ifndef _METAL_MTLARGUMENTINTERNAL_H_
|
|
+#define _METAL_MTLARGUMENTINTERNAL_H_
|
|
+#import <Metal/MTLArgument.h>
|
|
+#import <Metal/MTLVertexDescriptor.h>
|
|
+#if DARLING_METAL_ENABLED
|
|
+#include <indium/indium.hpp>
|
|
+@interface MTLComputePipelineReflection (Internal)
|
|
+- (instancetype)initWithFunction:(std::shared_ptr<Indium::Function>)function;
|
|
+@end
|
|
+@interface MTLRenderPipelineReflection (Internal)
|
|
+- (instancetype)initWithVertexFunction:(std::shared_ptr<Indium::Function>)vertex fragmentFunction:(std::shared_ptr<Indium::Function>)fragment;
|
|
+- (instancetype)initWithVertexFunction:(std::shared_ptr<Indium::Function>)vertex fragmentFunction:(std::shared_ptr<Indium::Function>)fragment vertexDescriptor:(MTLVertexDescriptor*)vertexDescriptor;
|
|
+@end
|
|
+#endif
|
|
+#endif
|
|
diff --git a/private-include/Metal/MTLBlitCommandEncoderInternal.h b/private-include/Metal/MTLBlitCommandEncoderInternal.h
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/Metal/MTLBlitCommandEncoderInternal.h
|
|
@@ -0,0 +1,15 @@
|
|
+// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
+// SPDX-License-Identifier: MPL-2.0
|
|
+
|
|
+#ifndef _METAL_MTLBLITCOMMANDENCODERINTERNAL_H_
|
|
+#define _METAL_MTLBLITCOMMANDENCODERINTERNAL_H_
|
|
+#import <Metal/MTLBlitCommandEncoder.h>
|
|
+#if DARLING_METAL_ENABLED
|
|
+#include <indium/indium.hpp>
|
|
+#endif
|
|
+@interface MTLBlitCommandEncoderInternal : NSObject <MTLBlitCommandEncoder>
|
|
+#if DARLING_METAL_ENABLED
|
|
+- (instancetype)initWithEncoder:(std::shared_ptr<Indium::BlitCommandEncoder>)encoder device:(id<MTLDevice>)device;
|
|
+#endif
|
|
+@end
|
|
+#endif
|
|
diff --git a/private-include/Metal/MTLDepthStencilInternal.h b/private-include/Metal/MTLDepthStencilInternal.h
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/Metal/MTLDepthStencilInternal.h
|
|
@@ -0,0 +1,27 @@
|
|
+// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
+// SPDX-License-Identifier: MPL-2.0
|
|
+
|
|
+#ifndef _METAL_MTLDEPTHSTENCILINTERNAL_H_
|
|
+#define _METAL_MTLDEPTHSTENCILINTERNAL_H_
|
|
+#import <Metal/MTLDepthStencilDescriptor.h>
|
|
+#if DARLING_METAL_ENABLED
|
|
+#include <indium/indium.hpp>
|
|
+@interface MTLStencilDescriptor (Internal)
|
|
+- (Indium::StencilDescriptor)asIndiumDescriptor;
|
|
+@end
|
|
+@interface MTLDepthStencilDescriptor (Internal)
|
|
+- (Indium::DepthStencilDescriptor)asIndiumDescriptor;
|
|
+@end
|
|
+#endif
|
|
+@interface MTLDepthStencilStateInternal : NSObject <MTLDepthStencilState>
|
|
+#if DARLING_METAL_ENABLED
|
|
+@property(nonatomic, readonly) std::shared_ptr<Indium::DepthStencilState> state;
|
|
+- (instancetype)initWithState:(std::shared_ptr<Indium::DepthStencilState>)state device:(id<MTLDevice>)device label:(NSString*)label;
|
|
+#endif
|
|
+@end
|
|
+#if DARLING_METAL_ENABLED
|
|
+// Direct access for the encoders: the property getter is a message send that returns the shared_ptr by value.
|
|
+const std::shared_ptr<Indium::DepthStencilState>& MTLDepthStencilGetIndium(id object);
|
|
+#endif
|
|
+
|
|
+#endif
|
|
diff --git a/private-include/Metal/MTLDeviceInfo.h b/private-include/Metal/MTLDeviceInfo.h
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/Metal/MTLDeviceInfo.h
|
|
@@ -0,0 +1,150 @@
|
|
+// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
+// SPDX-License-Identifier: MPL-2.0
|
|
+
|
|
+#ifndef _METAL_MTLDEVICEINFO_H_
|
|
+#define _METAL_MTLDEVICEINFO_H_
|
|
+
|
|
+// Host-testable helpers for the values MTLDevice reports about the GPU.
|
|
+// Plain C++ only (no Objective-C, Vulkan or Indium) so tools/test-metal-device-info.cpp
|
|
+// can compile this header on the host.
|
|
+
|
|
+#include <cstddef>
|
|
+#include <cstdint>
|
|
+#include <string>
|
|
+
|
|
+namespace MTLDeviceInfo {
|
|
+
|
|
+// Used when neither the cleaned nor the raw Vulkan name has any characters.
|
|
+static constexpr const char* kFallbackName = "Darling Indium Device";
|
|
+// A real Mac never reports registry ID 0, and apps use it as a key for the device.
|
|
+static constexpr uint64_t kDefaultRegistryID = 0x100000439ULL;
|
|
+
|
|
+static inline bool isSpace(char c) {
|
|
+ return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
|
|
+}
|
|
+
|
|
+static inline std::string collapseWhitespace(const std::string& text) {
|
|
+ std::string out;
|
|
+ bool pendingSpace = false;
|
|
+ for (char c: text) {
|
|
+ if (isSpace(c)) {
|
|
+ pendingSpace = !out.empty();
|
|
+ continue;
|
|
+ }
|
|
+ if (pendingSpace) {
|
|
+ out.push_back(' ');
|
|
+ pendingSpace = false;
|
|
+ }
|
|
+ out.push_back(c);
|
|
+ }
|
|
+ return out;
|
|
+}
|
|
+
|
|
+static inline char lower(char c) {
|
|
+ return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
|
|
+}
|
|
+
|
|
+// Removes "(R)" and "(TM)" in any letter case (Intel's Vulkan names use both).
|
|
+static inline std::string removeTrademarks(const std::string& text) {
|
|
+ std::string out;
|
|
+ for (size_t i = 0; i < text.size();) {
|
|
+ if (text[i] == '(') {
|
|
+ if (i + 2 < text.size() && lower(text[i + 1]) == 'r' && text[i + 2] == ')') {
|
|
+ i += 3;
|
|
+ continue;
|
|
+ }
|
|
+ if (i + 3 < text.size() && lower(text[i + 1]) == 't' && lower(text[i + 2]) == 'm' && text[i + 3] == ')') {
|
|
+ i += 4;
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+ out.push_back(text[i++]);
|
|
+ }
|
|
+ return out;
|
|
+}
|
|
+
|
|
+// Drops one trailing parenthetical group (Mesa/driver annotation such as
|
|
+// " (RADV NAVI21)", " (CML GT2)" or " (LLVM 22.1.0, 256 bits)"), but never when
|
|
+// the group is the whole name. Expects whitespace already collapsed and trimmed.
|
|
+static inline std::string removeTrailingGroup(const std::string& text) {
|
|
+ if (text.empty() || text.back() != ')') {
|
|
+ return text;
|
|
+ }
|
|
+ int depth = 0;
|
|
+ for (size_t i = text.size(); i-- > 0;) {
|
|
+ if (text[i] == ')') {
|
|
+ ++depth;
|
|
+ } else if (text[i] == '(' && --depth == 0) {
|
|
+ std::string prefix = collapseWhitespace(text.substr(0, i));
|
|
+ return prefix.empty() ? text : prefix;
|
|
+ }
|
|
+ }
|
|
+ return text; // unbalanced: leave it alone
|
|
+}
|
|
+
|
|
+// Converts a VkPhysicalDeviceProperties::deviceName into macOS Metal style,
|
|
+// e.g. "AMD Radeon RX 6800 XT (RADV NAVI21)" -> "AMD Radeon RX 6800 XT" and
|
|
+// "Intel(R) UHD Graphics 630 (CML GT2)" -> "Intel UHD Graphics 630".
|
|
+// This only cleans the real name; it never substitutes another GPU.
|
|
+static inline std::string metalNameFromVulkanName(const char* vulkanName) {
|
|
+ std::string raw = vulkanName ? vulkanName : "";
|
|
+ std::string cleaned = removeTrailingGroup(collapseWhitespace(removeTrademarks(raw)));
|
|
+ if (!cleaned.empty()) {
|
|
+ return cleaned;
|
|
+ }
|
|
+ std::string rawTrimmed = collapseWhitespace(raw);
|
|
+ return rawTrimmed.empty() ? std::string(kFallbackName) : rawTrimmed;
|
|
+}
|
|
+
|
|
+// DARLING_METAL_GPU_NAME (used verbatim when non-empty) wins over the Vulkan name.
|
|
+static inline std::string resolveName(const char* overrideName, const char* vulkanName) {
|
|
+ if (overrideName && overrideName[0] != '\0') {
|
|
+ return overrideName;
|
|
+ }
|
|
+ return metalNameFromVulkanName(vulkanName);
|
|
+}
|
|
+
|
|
+// Parses DARLING_METAL_VRAM_MB: a positive decimal integer of mebibytes, digits only.
|
|
+// Returns 0 (meaning "ignore") for anything else, including overflow.
|
|
+static inline uint64_t parseMegabytes(const char* text) {
|
|
+ if (!text || text[0] == '\0') {
|
|
+ return 0;
|
|
+ }
|
|
+ uint64_t megabytes = 0;
|
|
+ for (const char* p = text; *p; ++p) {
|
|
+ if (*p < '0' || *p > '9') {
|
|
+ return 0;
|
|
+ }
|
|
+ uint64_t digit = static_cast<uint64_t>(*p - '0');
|
|
+ if (megabytes > (UINT64_MAX / (1024ull * 1024ull) - digit) / 10) {
|
|
+ return 0;
|
|
+ }
|
|
+ megabytes = megabytes * 10 + digit;
|
|
+ }
|
|
+ return megabytes * 1024ull * 1024ull;
|
|
+}
|
|
+
|
|
+struct Heap {
|
|
+ uint64_t size;
|
|
+ bool deviceLocal;
|
|
+};
|
|
+
|
|
+// Working-set size from the Vulkan memory heaps; 0 when nothing usable is reported.
|
|
+// Discrete GPUs report their device-local (VRAM) heaps. GPUs sharing system RAM
|
|
+// (integrated/CPU device types) report every heap: their device-local heap may be
|
|
+// only the firmware carve-out (RADV APUs) while the GPU can also use the system heap,
|
|
+// which matches macOS reporting a large share of system RAM for unified-memory GPUs.
|
|
+static inline uint64_t workingSetBytes(const Heap* heaps, size_t count, bool sharesSystemMemory) {
|
|
+ uint64_t total = 0;
|
|
+ for (size_t i = 0; i < count; ++i) {
|
|
+ if (!sharesSystemMemory && !heaps[i].deviceLocal) {
|
|
+ continue;
|
|
+ }
|
|
+ total = heaps[i].size > UINT64_MAX - total ? UINT64_MAX : total + heaps[i].size;
|
|
+ }
|
|
+ return total;
|
|
+}
|
|
+
|
|
+} // namespace MTLDeviceInfo
|
|
+
|
|
+#endif // _METAL_MTLDEVICEINFO_H_
|
|
diff --git a/private-include/Metal/MTLSamplerStateInternal.h b/private-include/Metal/MTLSamplerStateInternal.h
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/Metal/MTLSamplerStateInternal.h
|
|
@@ -0,0 +1,13 @@
|
|
+// SPDX-License-Identifier: MPL-2.0
|
|
+#pragma once
|
|
+#import <Metal/MTLSamplerDescriptor.h>
|
|
+#if DARLING_METAL_ENABLED
|
|
+#include <indium/indium.hpp>
|
|
+@interface MTLSamplerStateInternal : NSObject <MTLSamplerState>
|
|
+@property(nonatomic, readonly) std::shared_ptr<Indium::SamplerState> sampler;
|
|
+- (instancetype)initWithSampler:(std::shared_ptr<Indium::SamplerState>)sampler device:(id<MTLDevice>)device label:(NSString*)label;
|
|
+@end
|
|
+// Direct access for the encoders: the property getter is a message send that returns the shared_ptr by value.
|
|
+const std::shared_ptr<Indium::SamplerState>& MTLSamplerGetIndium(id object);
|
|
+
|
|
+#endif
|
|
diff --git a/private-include/Metal/MTLVertexDescriptorInternal.h b/private-include/Metal/MTLVertexDescriptorInternal.h
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/Metal/MTLVertexDescriptorInternal.h
|
|
@@ -0,0 +1,13 @@
|
|
+// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
+// SPDX-License-Identifier: MPL-2.0
|
|
+
|
|
+#ifndef _METAL_MTLVERTEXDESCRIPTORINTERNAL_H_
|
|
+#define _METAL_MTLVERTEXDESCRIPTORINTERNAL_H_
|
|
+#import <Metal/MTLVertexDescriptor.h>
|
|
+#if DARLING_METAL_ENABLED
|
|
+#include <indium/indium.hpp>
|
|
+@interface MTLVertexDescriptor (Internal)
|
|
+- (Indium::VertexDescriptor)asIndiumDescriptor;
|
|
+@end
|
|
+#endif
|
|
+#endif
|
|
diff --git a/src/Metal/MTLArgument.mm b/src/Metal/MTLArgument.mm
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/src/Metal/MTLArgument.mm
|
|
@@ -0,0 +1,78 @@
|
|
+#import <Metal/MTLArgumentInternal.h>
|
|
+@implementation MTLStructMember
|
|
+@synthesize name=_name, offset=_offset;
|
|
+- (instancetype)initWithName:(NSString*)name offset:(NSUInteger)offset { self=[super init]; if(self){_name=[name copy];_offset=offset;}return self; }
|
|
+- (void)dealloc { [_name release];[super dealloc]; }
|
|
+@end
|
|
+@implementation MTLStructType
|
|
+@synthesize members=_members;
|
|
+- (instancetype)initWithMembers:(NSArray*)members { self=[super init];if(self)_members=[members copy];return self; }
|
|
+- (MTLStructMember*)memberByName:(NSString*)name { for(MTLStructMember* member in _members) if([member.name isEqualToString:name])return member; return nil; }
|
|
+- (void)dealloc { [_members release];[super dealloc]; }
|
|
+@end
|
|
+@implementation MTLArgument
|
|
+@synthesize name=_name, type=_type, index=_index, bufferAlignment=_bufferAlignment, bufferDataSize=_bufferDataSize, bufferStructType=_bufferStructType;
|
|
+- (BOOL)isActive { return YES; }
|
|
+#if DARLING_METAL_ENABLED
|
|
+- (instancetype)initWithInfo:(const Iridium::ArgumentInfo&)info {
|
|
+ self=[super init];if(self){
|
|
+ _name=[[NSString alloc] initWithUTF8String:info.name.c_str()];_index=info.index;
|
|
+ _type=info.type==Iridium::BindingType::Buffer ? MTLArgumentTypeBuffer : (info.type==Iridium::BindingType::Texture ? MTLArgumentTypeTexture : MTLArgumentTypeSampler);
|
|
+ _bufferAlignment=info.alignment;_bufferDataSize=info.size;
|
|
+ if(!info.members.empty()){
|
|
+ NSMutableArray* members=[NSMutableArray new];
|
|
+ for(const auto& member:info.members){ MTLStructMember* object=[[MTLStructMember alloc] initWithName:[NSString stringWithUTF8String:member.name.c_str()] offset:member.offset];[members addObject:object];[object release]; }
|
|
+ _bufferStructType=[[MTLStructType alloc] initWithMembers:members];[members release];
|
|
+ }
|
|
+ }return self;
|
|
+}
|
|
+#endif
|
|
+- (void)dealloc { [_name release];[_bufferStructType release];[super dealloc]; }
|
|
+@end
|
|
+#if DARLING_METAL_ENABLED
|
|
+static NSArray* argumentsForFunction(std::shared_ptr<Indium::Function> function) {
|
|
+ NSMutableArray* result=[NSMutableArray array];
|
|
+ if(function)for(const auto& info:function->argumentInfo()){
|
|
+ MTLArgument* argument=[[MTLArgument alloc] initWithInfo:info];[result addObject:argument];[argument release];
|
|
+ }
|
|
+ return result;
|
|
+}
|
|
+#endif
|
|
+@implementation MTLComputePipelineReflection
|
|
+@synthesize arguments=_arguments;
|
|
+#if DARLING_METAL_ENABLED
|
|
+- (instancetype)initWithFunction:(std::shared_ptr<Indium::Function>)function { self=[super init];if(self)_arguments=[argumentsForFunction(function) copy];return self; }
|
|
+#endif
|
|
+- (void)dealloc { [_arguments release];[super dealloc]; }
|
|
+@end
|
|
+@implementation MTLRenderPipelineReflection
|
|
+@synthesize vertexArguments=_vertexArguments, fragmentArguments=_fragmentArguments;
|
|
+#if DARLING_METAL_ENABLED
|
|
+- (instancetype)initWithVertexFunction:(std::shared_ptr<Indium::Function>)vertex fragmentFunction:(std::shared_ptr<Indium::Function>)fragment {
|
|
+ return [self initWithVertexFunction:vertex fragmentFunction:fragment vertexDescriptor:nil];
|
|
+}
|
|
+// Metal lists the vertex buffers that feed [[stage_in]] among the vertex arguments, as buffers named "vertexBuffer.<index>"
|
|
+// (the game derives which vertex streams to bind from them).
|
|
+- (instancetype)initWithVertexFunction:(std::shared_ptr<Indium::Function>)vertex fragmentFunction:(std::shared_ptr<Indium::Function>)fragment vertexDescriptor:(MTLVertexDescriptor*)vertexDescriptor {
|
|
+ self=[super init];if(self){
|
|
+ NSMutableArray* vertexArguments=[argumentsForFunction(vertex) mutableCopy];
|
|
+ NSMutableIndexSet* seen=[NSMutableIndexSet indexSet];
|
|
+ for(NSUInteger attribute=0;vertexDescriptor&&attribute<31;++attribute){
|
|
+ MTLVertexAttributeDescriptor* description=vertexDescriptor.attributes[attribute];
|
|
+ if(description.format==MTLVertexFormatInvalid||[seen containsIndex:description.bufferIndex])continue;
|
|
+ [seen addIndex:description.bufferIndex];
|
|
+ Iridium::ArgumentInfo info;
|
|
+ info.type=Iridium::BindingType::Buffer;
|
|
+ info.index=description.bufferIndex;
|
|
+ info.name="vertexBuffer."+std::to_string(description.bufferIndex);
|
|
+ info.size=vertexDescriptor.layouts[description.bufferIndex].stride;
|
|
+ info.alignment=4;
|
|
+ MTLArgument* argument=[[MTLArgument alloc] initWithInfo:info];[vertexArguments addObject:argument];[argument release];
|
|
+ }
|
|
+ _vertexArguments=vertexArguments;
|
|
+ _fragmentArguments=[argumentsForFunction(fragment) copy];
|
|
+ }return self;
|
|
+}
|
|
+#endif
|
|
+- (void)dealloc { [_vertexArguments release];[_fragmentArguments release];[super dealloc]; }
|
|
+@end
|
|
diff --git a/src/Metal/MTLBlitCommandEncoder.mm b/src/Metal/MTLBlitCommandEncoder.mm
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/src/Metal/MTLBlitCommandEncoder.mm
|
|
@@ -0,0 +1,101 @@
|
|
+// SPDX-FileCopyrightText: 2026 Darling Developers
|
|
+// SPDX-License-Identifier: MPL-2.0
|
|
+
|
|
+#import <Metal/MTLTrace.h>
|
|
+#include <algorithm>
|
|
+#import <Metal/MTLBlitCommandEncoderInternal.h>
|
|
+#import <Metal/MTLBufferInternal.h>
|
|
+#import <Metal/MTLTextureInternal.h>
|
|
+#import <Metal/MTLTypesInternal.h>
|
|
+#import <Metal/stubs.h>
|
|
+
|
|
+#if DARLING_METAL_ENABLED
|
|
+// Diagnostic (/tmp/metal-debug-timing): are the texture uploads carrying data? Sample the start of each source.
|
|
+static void MTLDebugUpload(id<MTLBuffer> source, NSUInteger offset, id<MTLTexture> destination, NSUInteger level, MTLSize size)
|
|
+{
|
|
+ if (!MTLTimingEnabled()) {
|
|
+ return;
|
|
+ }
|
|
+ const uint8_t* bytes = (const uint8_t*) source.contents;
|
|
+ if (!bytes) {
|
|
+ MTLTraceTiming("upload_sourceNotCPUVisible", 0);
|
|
+ return;
|
|
+ }
|
|
+ NSUInteger available = source.length > offset ? source.length - offset : 0;
|
|
+ NSUInteger probe = std::min<NSUInteger>(available, 4096);
|
|
+ NSUInteger nonZero = 0;
|
|
+ for (NSUInteger i = 0; i < probe; ++i) if (bytes[offset + i]) ++nonZero;
|
|
+ char name[64];
|
|
+ snprintf(name, sizeof(name), "upload_%s_f%lu", nonZero ? "data" : "ZERO", (unsigned long) destination.pixelFormat);
|
|
+ MTLTraceTiming(name, 0);
|
|
+ static unsigned long logged = 0;
|
|
+ if (!nonZero && destination.width >= 256 && ++logged <= 60) {
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "zero-upload tex=%lux%lu fmt=%lu mips=%lu level=%lu size=%lux%lu srcOffset=%lu\n", (unsigned long) destination.width, (unsigned long) destination.height, (unsigned long) destination.pixelFormat,
|
|
+ (unsigned long) destination.mipmapLevelCount, (unsigned long) level, (unsigned long) size.width, (unsigned long) size.height, (unsigned long) offset);
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+}
|
|
+#endif
|
|
+@implementation MTLBlitCommandEncoderInternal
|
|
+#if DARLING_METAL_ENABLED
|
|
+{
|
|
+ std::shared_ptr<Indium::BlitCommandEncoder> _encoder;
|
|
+}
|
|
+@synthesize device = _device;
|
|
+@synthesize label = _label;
|
|
+- (instancetype)initWithEncoder:(std::shared_ptr<Indium::BlitCommandEncoder>)encoder device:(id<MTLDevice>)device {
|
|
+ self = [super init];
|
|
+ if (self) { _encoder = encoder; _device = [device retain]; }
|
|
+ return self;
|
|
+}
|
|
+- (void)dealloc {
|
|
+ [_device release]; [_label release]; [super dealloc];
|
|
+}
|
|
+- (void)endEncoding { _encoder->endEncoding(); }
|
|
+- (void)copyFromBuffer:(id<MTLBuffer>)source sourceOffset:(NSUInteger)sourceOffset toBuffer:(id<MTLBuffer>)destination destinationOffset:(NSUInteger)destinationOffset size:(NSUInteger)size {
|
|
+ MTLTraceCount("blitBufferToBuffer");
|
|
+ _encoder->copy(MTLBufferGetIndium(source), sourceOffset, MTLBufferGetIndium(destination), destinationOffset, size);
|
|
+}
|
|
+- (void)copyFromBuffer:(id<MTLBuffer>)source sourceOffset:(NSUInteger)sourceOffset sourceBytesPerRow:(NSUInteger)sourceBytesPerRow sourceBytesPerImage:(NSUInteger)sourceBytesPerImage sourceSize:(MTLSize)sourceSize toTexture:(id<MTLTexture>)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel destinationOrigin:(MTLOrigin)destinationOrigin {
|
|
+ MTLTraceCount("blitBufferToTexture");
|
|
+ MTLDebugUpload(source, sourceOffset, destination, destinationLevel, sourceSize);
|
|
+ _encoder->copy(MTLBufferGetIndium(source), sourceOffset, sourceBytesPerRow, sourceBytesPerImage, MTLSizeToIndium(sourceSize), MTLTextureGetIndium(destination), destinationSlice, destinationLevel, (Indium::Origin {destinationOrigin.x, destinationOrigin.y, destinationOrigin.z}));
|
|
+}
|
|
+- (void)copyFromTexture:(id<MTLTexture>)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toBuffer:(id<MTLBuffer>)destination destinationOffset:(NSUInteger)destinationOffset destinationBytesPerRow:(NSUInteger)destinationBytesPerRow destinationBytesPerImage:(NSUInteger)destinationBytesPerImage {
|
|
+ MTLTraceCount("blitTextureToBuffer");
|
|
+ _encoder->copy(MTLTextureGetIndium(source), sourceSlice, sourceLevel, (Indium::Origin {sourceOrigin.x, sourceOrigin.y, sourceOrigin.z}), MTLSizeToIndium(sourceSize), MTLBufferGetIndium(destination), destinationOffset, destinationBytesPerRow, destinationBytesPerImage);
|
|
+}
|
|
+- (void)copyFromTexture:(id<MTLTexture>)source toTexture:(id<MTLTexture>)destination {
|
|
+ MTLTraceCount("blitTextureToTexture");
|
|
+ _encoder->copy(MTLTextureGetIndium(source), MTLTextureGetIndium(destination));
|
|
+}
|
|
+- (void)copyFromTexture:(id<MTLTexture>)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel toTexture:(id<MTLTexture>)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel sliceCount:(NSUInteger)sliceCount levelCount:(NSUInteger)levelCount {
|
|
+ MTLTraceCount("blitTextureToTexture");
|
|
+ _encoder->copy(MTLTextureGetIndium(source), sourceSlice, sourceLevel, MTLTextureGetIndium(destination), destinationSlice, destinationLevel, sliceCount, levelCount);
|
|
+}
|
|
+- (void)copyFromTexture:(id<MTLTexture>)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toTexture:(id<MTLTexture>)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel destinationOrigin:(MTLOrigin)destinationOrigin {
|
|
+ MTLTraceCount("blitTextureToTexture");
|
|
+ _encoder->copy(MTLTextureGetIndium(source), sourceSlice, sourceLevel, (Indium::Origin {sourceOrigin.x, sourceOrigin.y, sourceOrigin.z}), MTLSizeToIndium(sourceSize), MTLTextureGetIndium(destination), destinationSlice, destinationLevel, (Indium::Origin {destinationOrigin.x, destinationOrigin.y, destinationOrigin.z}));
|
|
+}
|
|
+- (void)fillBuffer:(id<MTLBuffer>)buffer range:(NSRange)range value:(uint8_t)value {
|
|
+ _encoder->fillBuffer(MTLBufferGetIndium(buffer), NSRangeToIndium(range), value);
|
|
+}
|
|
+- (void)generateMipmapsForTexture:(id<MTLTexture>)texture {
|
|
+ MTLTraceCount("generateMipmaps");
|
|
+ _encoder->generateMipmapsForTexture(MTLTextureGetIndium(texture));
|
|
+}
|
|
+- (void)copyFromBuffer:(id<MTLBuffer>)source sourceOffset:(NSUInteger)sourceOffset sourceBytesPerRow:(NSUInteger)sourceBytesPerRow sourceBytesPerImage:(NSUInteger)sourceBytesPerImage sourceSize:(MTLSize)sourceSize toTexture:(id<MTLTexture>)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel destinationOrigin:(MTLOrigin)destinationOrigin options:(MTLBlitOption)options {
|
|
+ MTLTraceCount("blitBufferToTexture");
|
|
+ _encoder->copy(MTLBufferGetIndium(source), sourceOffset, sourceBytesPerRow, sourceBytesPerImage, MTLSizeToIndium(sourceSize), MTLTextureGetIndium(destination), destinationSlice, destinationLevel, (Indium::Origin {destinationOrigin.x, destinationOrigin.y, destinationOrigin.z}), static_cast<Indium::BlitOption>(options));
|
|
+}
|
|
+- (void)copyFromTexture:(id<MTLTexture>)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toBuffer:(id<MTLBuffer>)destination destinationOffset:(NSUInteger)destinationOffset destinationBytesPerRow:(NSUInteger)destinationBytesPerRow destinationBytesPerImage:(NSUInteger)destinationBytesPerImage options:(MTLBlitOption)options {
|
|
+ MTLTraceCount("blitTextureToBuffer");
|
|
+ _encoder->copy(MTLTextureGetIndium(source), sourceSlice, sourceLevel, (Indium::Origin {sourceOrigin.x, sourceOrigin.y, sourceOrigin.z}), MTLSizeToIndium(sourceSize), MTLBufferGetIndium(destination), destinationOffset, destinationBytesPerRow, destinationBytesPerImage, static_cast<Indium::BlitOption>(options));
|
|
+}
|
|
+#else
|
|
+@dynamic device, label;
|
|
+MTL_UNSUPPORTED_CLASS
|
|
+#endif
|
|
+@end
|