metaco 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -1
- data/README.md +69 -12
- data/Rakefile +16 -1
- data/docs/index.html +38 -0
- data/ext/metaco/metaco.m +927 -288
- data/lib/metaco/version.rb +1 -1
- data/lib/metaco.rb +63 -4
- metadata +10 -7
- data/metaco.gemspec +0 -28
data/ext/metaco/metaco.m
CHANGED
|
@@ -6,221 +6,276 @@
|
|
|
6
6
|
#import <Metal/Metal.h>
|
|
7
7
|
#import <QuartzCore/CAMetalLayer.h>
|
|
8
8
|
#import <ruby.h>
|
|
9
|
+
#import <ruby/thread.h>
|
|
10
|
+
#include <limits.h>
|
|
11
|
+
#include <math.h>
|
|
12
|
+
#include <pthread.h>
|
|
13
|
+
#include <stdint.h>
|
|
14
|
+
|
|
15
|
+
static void native_error(NSError **error, NSString *message) {
|
|
16
|
+
if (error) *error = [NSError errorWithDomain:@"Metaco" code:1
|
|
17
|
+
userInfo:@{NSLocalizedDescriptionKey: message}];
|
|
18
|
+
}
|
|
9
19
|
|
|
10
20
|
@interface MetacoMetalView : NSView
|
|
11
21
|
@property (nonatomic, strong) CAMetalLayer *metalLayer;
|
|
12
22
|
@property (nonatomic, strong) id<MTLDevice> device;
|
|
13
23
|
@property (nonatomic, strong) id<MTLCommandQueue> commandQueue;
|
|
14
24
|
@property (nonatomic, strong) id<MTLTexture> texture;
|
|
25
|
+
@property (nonatomic, strong) id<MTLRenderPipelineState> renderPipeline;
|
|
15
26
|
@property (nonatomic, assign) int texWidth;
|
|
16
27
|
@property (nonatomic, assign) int texHeight;
|
|
17
|
-
// Compute shader support
|
|
18
28
|
@property (nonatomic, strong) id<MTLComputePipelineState> computePipeline;
|
|
19
29
|
@property (nonatomic, strong) id<MTLBuffer> uniformBuffer;
|
|
20
30
|
@property (nonatomic, strong) id<MTLTexture> outputTexture;
|
|
21
|
-
@property (nonatomic,
|
|
31
|
+
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, id<MTLTexture>> *computeTextures;
|
|
32
|
+
@property (nonatomic, strong) id<MTLCommandBuffer> lastCommandBuffer;
|
|
33
|
+
@property (nonatomic, readonly) BOOL hasComputeShader;
|
|
34
|
+
- (BOOL)resizeToWidth:(int)width height:(int)height error:(NSError **)error;
|
|
35
|
+
- (void)waitForLastCommand;
|
|
36
|
+
- (void)releaseResources;
|
|
22
37
|
@end
|
|
23
38
|
|
|
24
39
|
@implementation MetacoMetalView
|
|
25
40
|
|
|
26
|
-
- (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} @catch (NSException *e) {
|
|
39
|
-
// Ignore exceptions during cleanup
|
|
41
|
+
- (BOOL)hasComputeShader { return self.computePipeline != nil; }
|
|
42
|
+
|
|
43
|
+
- (id<MTLTexture>)newTextureWithWidth:(int)width height:(int)height
|
|
44
|
+
usage:(MTLTextureUsage)usage storageMode:(MTLStorageMode)storageMode {
|
|
45
|
+
MTLTextureDescriptor *desc = [MTLTextureDescriptor
|
|
46
|
+
texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
|
|
47
|
+
width:width height:height mipmapped:NO];
|
|
48
|
+
desc.usage = usage;
|
|
49
|
+
desc.storageMode = storageMode;
|
|
50
|
+
if (@available(macOS 10.15, *)) {
|
|
51
|
+
if (storageMode == MTLStorageModeManaged && self.device.hasUnifiedMemory) {
|
|
52
|
+
desc.storageMode = MTLStorageModeShared;
|
|
40
53
|
}
|
|
41
54
|
}
|
|
42
|
-
|
|
43
|
-
// Clear all Metal resources
|
|
44
|
-
self.computePipeline = nil;
|
|
45
|
-
self.uniformBuffer = nil;
|
|
46
|
-
self.outputTexture = nil;
|
|
47
|
-
self.texture = nil;
|
|
48
|
-
self.commandQueue = nil;
|
|
49
|
-
self.metalLayer = nil;
|
|
50
|
-
self.device = nil;
|
|
51
|
-
self.hasComputeShader = NO;
|
|
55
|
+
return [self.device newTextureWithDescriptor:desc];
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
// dealloc is handled automatically by ARC - no manual cleanup needed
|
|
55
|
-
// cocoa_window_destroy handles synchronization with GPU before release
|
|
56
|
-
|
|
57
58
|
- (instancetype)initWithFrame:(NSRect)frame device:(id<MTLDevice>)device width:(int)w height:(int)h {
|
|
58
59
|
self = [super initWithFrame:frame];
|
|
59
60
|
if (self) {
|
|
60
61
|
self.device = device;
|
|
61
62
|
self.texWidth = w;
|
|
62
63
|
self.texHeight = h;
|
|
64
|
+
self.commandQueue = [device newCommandQueue];
|
|
65
|
+
if (!self.commandQueue) return nil;
|
|
66
|
+
self.computeTextures = [NSMutableDictionary dictionary];
|
|
67
|
+
|
|
68
|
+
// A render pass preserves RGBA channel meaning when writing a BGRA drawable.
|
|
69
|
+
NSString *source = @"#include <metal_stdlib>\n"
|
|
70
|
+
"using namespace metal;\n"
|
|
71
|
+
"vertex float4 metaco_vertex(uint i [[vertex_id]]) {\n"
|
|
72
|
+
" const float2 p[] = {float2(-1,-1), float2(3,-1), float2(-1,3)};\n"
|
|
73
|
+
" return float4(p[i], 0, 1);\n"
|
|
74
|
+
"}\n"
|
|
75
|
+
"fragment float4 metaco_fragment(float4 p [[position]],\n"
|
|
76
|
+
" texture2d<float> input [[texture(0)]]) { return input.read(uint2(p.xy)); }\n";
|
|
77
|
+
id<MTLLibrary> library = [device newLibraryWithSource:source options:nil error:nil];
|
|
78
|
+
if (!library) return nil;
|
|
79
|
+
MTLRenderPipelineDescriptor *pipeline = [MTLRenderPipelineDescriptor new];
|
|
80
|
+
pipeline.vertexFunction = [library newFunctionWithName:@"metaco_vertex"];
|
|
81
|
+
pipeline.fragmentFunction = [library newFunctionWithName:@"metaco_fragment"];
|
|
82
|
+
pipeline.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
|
|
83
|
+
if (!pipeline.vertexFunction || !pipeline.fragmentFunction) return nil;
|
|
84
|
+
self.renderPipeline = [device newRenderPipelineStateWithDescriptor:pipeline error:nil];
|
|
85
|
+
if (!self.renderPipeline) return nil;
|
|
86
|
+
|
|
87
|
+
MTLTextureDescriptor *desc = [MTLTextureDescriptor
|
|
88
|
+
texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:w height:h mipmapped:NO];
|
|
89
|
+
desc.usage = MTLTextureUsageShaderRead;
|
|
90
|
+
desc.storageMode = MTLStorageModeManaged;
|
|
91
|
+
if (@available(macOS 10.15, *)) {
|
|
92
|
+
if (device.hasUnifiedMemory) desc.storageMode = MTLStorageModeShared;
|
|
93
|
+
}
|
|
94
|
+
self.texture = [device newTextureWithDescriptor:desc];
|
|
95
|
+
if (!self.texture) return nil;
|
|
63
96
|
|
|
64
97
|
self.wantsLayer = YES;
|
|
65
98
|
self.metalLayer = [CAMetalLayer layer];
|
|
99
|
+
if (!self.metalLayer) return nil;
|
|
66
100
|
self.metalLayer.device = device;
|
|
67
101
|
self.metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
|
|
68
|
-
self.metalLayer.framebufferOnly =
|
|
69
|
-
self.metalLayer.displaySyncEnabled = NO;
|
|
102
|
+
self.metalLayer.framebufferOnly = YES;
|
|
103
|
+
self.metalLayer.displaySyncEnabled = NO;
|
|
70
104
|
self.metalLayer.frame = frame;
|
|
71
105
|
self.metalLayer.drawableSize = CGSizeMake(w, h);
|
|
72
106
|
self.layer = self.metalLayer;
|
|
73
|
-
|
|
74
|
-
self.commandQueue = [device newCommandQueue];
|
|
75
|
-
|
|
76
|
-
// Create texture for pixel data
|
|
77
|
-
MTLTextureDescriptor *texDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
|
|
78
|
-
width:w
|
|
79
|
-
height:h
|
|
80
|
-
mipmapped:NO];
|
|
81
|
-
texDesc.usage = MTLTextureUsageShaderRead;
|
|
82
|
-
self.texture = [device newTextureWithDescriptor:texDesc];
|
|
83
107
|
}
|
|
84
108
|
return self;
|
|
85
109
|
}
|
|
86
110
|
|
|
87
111
|
- (void)updatePixels:(const uint8_t *)data {
|
|
88
|
-
|
|
89
|
-
|
|
112
|
+
[self.texture replaceRegion:MTLRegionMake2D(0, 0, self.texWidth, self.texHeight)
|
|
113
|
+
mipmapLevel:0 withBytes:data bytesPerRow:(size_t)self.texWidth * 4];
|
|
90
114
|
}
|
|
91
115
|
|
|
92
|
-
- (
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
- (BOOL)renderTexture:(id<MTLTexture>)source toTexture:(id<MTLTexture>)target
|
|
117
|
+
commandBuffer:(id<MTLCommandBuffer>)commandBuffer error:(NSError **)error {
|
|
118
|
+
MTLRenderPassDescriptor *pass = [MTLRenderPassDescriptor renderPassDescriptor];
|
|
119
|
+
pass.colorAttachments[0].texture = target;
|
|
120
|
+
pass.colorAttachments[0].loadAction = MTLLoadActionDontCare;
|
|
121
|
+
pass.colorAttachments[0].storeAction = MTLStoreActionStore;
|
|
122
|
+
id<MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor:pass];
|
|
123
|
+
if (!encoder) {
|
|
124
|
+
native_error(error, @"Failed to create render encoder");
|
|
125
|
+
return NO;
|
|
126
|
+
}
|
|
127
|
+
[encoder setRenderPipelineState:self.renderPipeline];
|
|
128
|
+
[encoder setFragmentTexture:source atIndex:0];
|
|
129
|
+
[encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
|
|
130
|
+
[encoder endEncoding];
|
|
131
|
+
return YES;
|
|
132
|
+
}
|
|
95
133
|
|
|
134
|
+
- (id<MTLCommandBuffer>)presentTexture:(id<MTLTexture>)texture error:(NSError **)error {
|
|
135
|
+
id<CAMetalDrawable> drawable = [self.metalLayer nextDrawable];
|
|
136
|
+
if (!drawable) return nil; // An occluded window can temporarily have no drawable.
|
|
96
137
|
id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
[
|
|
102
|
-
sourceSlice:0
|
|
103
|
-
sourceLevel:0
|
|
104
|
-
sourceOrigin:MTLOriginMake(0, 0, 0)
|
|
105
|
-
sourceSize:MTLSizeMake(self.texWidth, self.texHeight, 1)
|
|
106
|
-
toTexture:drawable.texture
|
|
107
|
-
destinationSlice:0
|
|
108
|
-
destinationLevel:0
|
|
109
|
-
destinationOrigin:MTLOriginMake(0, 0, 0)];
|
|
110
|
-
|
|
111
|
-
[blitEncoder endEncoding];
|
|
112
|
-
|
|
138
|
+
if (!commandBuffer) {
|
|
139
|
+
native_error(error, @"Failed to create render command buffer");
|
|
140
|
+
return nil;
|
|
141
|
+
}
|
|
142
|
+
if (![self renderTexture:texture toTexture:drawable.texture commandBuffer:commandBuffer error:error]) return nil;
|
|
113
143
|
[commandBuffer presentDrawable:drawable];
|
|
114
|
-
|
|
144
|
+
return commandBuffer;
|
|
115
145
|
}
|
|
116
146
|
|
|
117
|
-
#pragma mark - Compute Shader Support
|
|
118
|
-
|
|
119
147
|
- (BOOL)compileComputeShader:(NSString *)mslSource error:(NSError **)error {
|
|
120
|
-
|
|
121
|
-
id<MTLLibrary> library = [self.device newLibraryWithSource:mslSource
|
|
122
|
-
options:nil
|
|
123
|
-
error:error];
|
|
148
|
+
id<MTLLibrary> library = [self.device newLibraryWithSource:mslSource options:nil error:error];
|
|
124
149
|
if (!library) return NO;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
150
|
+
id<MTLFunction> function = [library newFunctionWithName:@"compute_shader"];
|
|
151
|
+
if (!function) {
|
|
152
|
+
native_error(error, @"compute_shader function not found");
|
|
153
|
+
return NO;
|
|
154
|
+
}
|
|
155
|
+
id<MTLComputePipelineState> pipeline = [self.device newComputePipelineStateWithFunction:function error:error];
|
|
156
|
+
if (!pipeline) return NO;
|
|
157
|
+
|
|
158
|
+
id<MTLTexture> output = [self newTextureWithWidth:self.texWidth height:self.texHeight
|
|
159
|
+
usage:MTLTextureUsageShaderWrite | MTLTextureUsageShaderRead
|
|
160
|
+
storageMode:MTLStorageModePrivate];
|
|
161
|
+
id<MTLBuffer> uniforms = [self.device newBufferWithLength:256 options:MTLResourceStorageModeShared];
|
|
162
|
+
if (!output || !uniforms || !uniforms.contents) {
|
|
163
|
+
native_error(error, @"Failed to allocate compute resources");
|
|
131
164
|
return NO;
|
|
132
165
|
}
|
|
133
166
|
|
|
134
|
-
//
|
|
135
|
-
self.computePipeline =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// Create output texture (writable)
|
|
139
|
-
MTLTextureDescriptor *texDesc = [MTLTextureDescriptor
|
|
140
|
-
texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
|
|
141
|
-
width:self.texWidth
|
|
142
|
-
height:self.texHeight
|
|
143
|
-
mipmapped:NO];
|
|
144
|
-
texDesc.usage = MTLTextureUsageShaderWrite | MTLTextureUsageShaderRead;
|
|
145
|
-
self.outputTexture = [self.device newTextureWithDescriptor:texDesc];
|
|
146
|
-
|
|
147
|
-
// Create uniform buffer (256 bytes should be enough for most shaders)
|
|
148
|
-
self.uniformBuffer = [self.device newBufferWithLength:256
|
|
149
|
-
options:MTLResourceStorageModeShared];
|
|
150
|
-
|
|
151
|
-
self.hasComputeShader = YES;
|
|
167
|
+
// Publish the new state only after every resource has been created.
|
|
168
|
+
self.computePipeline = pipeline;
|
|
169
|
+
self.outputTexture = output;
|
|
170
|
+
self.uniformBuffer = uniforms;
|
|
152
171
|
return YES;
|
|
153
172
|
}
|
|
154
173
|
|
|
155
|
-
- (
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
id<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
NSUInteger threadGroupWidth = 16;
|
|
171
|
-
NSUInteger threadGroupHeight = 16;
|
|
172
|
-
|
|
173
|
-
if (threadGroupWidth * threadGroupHeight > threadGroupSize) {
|
|
174
|
-
threadGroupWidth = 8;
|
|
175
|
-
threadGroupHeight = 8;
|
|
174
|
+
- (BOOL)resizeToWidth:(int)width height:(int)height error:(NSError **)error {
|
|
175
|
+
if (width == self.texWidth && height == self.texHeight) return YES;
|
|
176
|
+
|
|
177
|
+
id<MTLTexture> input = [self newTextureWithWidth:width height:height
|
|
178
|
+
usage:MTLTextureUsageShaderRead
|
|
179
|
+
storageMode:MTLStorageModeManaged];
|
|
180
|
+
id<MTLTexture> output = nil;
|
|
181
|
+
if (self.hasComputeShader) {
|
|
182
|
+
output = [self newTextureWithWidth:width height:height
|
|
183
|
+
usage:MTLTextureUsageShaderWrite | MTLTextureUsageShaderRead
|
|
184
|
+
storageMode:MTLStorageModePrivate];
|
|
185
|
+
}
|
|
186
|
+
if (!input || (self.hasComputeShader && !output)) {
|
|
187
|
+
native_error(error, @"Failed to resize Metal textures");
|
|
188
|
+
return NO;
|
|
176
189
|
}
|
|
177
190
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
[computeEncoder dispatchThreadgroups:threadGroups threadsPerThreadgroup:threadsPerGroup];
|
|
186
|
-
[computeEncoder endEncoding];
|
|
187
|
-
|
|
188
|
-
[commandBuffer commit];
|
|
189
|
-
[commandBuffer waitUntilCompleted];
|
|
191
|
+
self.texture = input;
|
|
192
|
+
self.outputTexture = output;
|
|
193
|
+
self.texWidth = width;
|
|
194
|
+
self.texHeight = height;
|
|
195
|
+
self.metalLayer.drawableSize = CGSizeMake(width, height);
|
|
196
|
+
return YES;
|
|
190
197
|
}
|
|
191
198
|
|
|
192
|
-
- (void)
|
|
193
|
-
|
|
199
|
+
- (void)releaseResources {
|
|
200
|
+
[self waitForLastCommand];
|
|
201
|
+
self.metalLayer.delegate = nil;
|
|
202
|
+
[self.metalLayer removeFromSuperlayer];
|
|
203
|
+
self.layer = nil;
|
|
204
|
+
self.metalLayer = nil;
|
|
205
|
+
self.computeTextures = nil;
|
|
206
|
+
self.outputTexture = nil;
|
|
207
|
+
self.uniformBuffer = nil;
|
|
208
|
+
self.computePipeline = nil;
|
|
209
|
+
self.renderPipeline = nil;
|
|
210
|
+
self.texture = nil;
|
|
211
|
+
self.commandQueue = nil;
|
|
212
|
+
self.device = nil;
|
|
213
|
+
}
|
|
194
214
|
|
|
195
|
-
|
|
196
|
-
|
|
215
|
+
- (void)waitForLastCommand {
|
|
216
|
+
[self.lastCommandBuffer waitUntilCompleted];
|
|
217
|
+
self.lastCommandBuffer = nil;
|
|
218
|
+
}
|
|
197
219
|
|
|
220
|
+
- (id<MTLCommandBuffer>)dispatchComputeWithUniforms:(const void *)data length:(NSUInteger)length
|
|
221
|
+
error:(NSError **)error {
|
|
222
|
+
if (!self.hasComputeShader || length > 256) {
|
|
223
|
+
native_error(error, @"Invalid compute state or uniform size");
|
|
224
|
+
return nil;
|
|
225
|
+
}
|
|
226
|
+
memset(self.uniformBuffer.contents, 0, 256);
|
|
227
|
+
memcpy(self.uniformBuffer.contents, data, length);
|
|
198
228
|
id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
|
|
199
|
-
id<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
229
|
+
id<MTLComputeCommandEncoder> encoder = [commandBuffer computeCommandEncoder];
|
|
230
|
+
if (!commandBuffer || !encoder) {
|
|
231
|
+
native_error(error, @"Failed to create compute command buffer or encoder");
|
|
232
|
+
return nil;
|
|
233
|
+
}
|
|
234
|
+
[encoder setComputePipelineState:self.computePipeline];
|
|
235
|
+
[encoder setTexture:self.outputTexture atIndex:0];
|
|
236
|
+
for (NSNumber *index in self.computeTextures) {
|
|
237
|
+
[encoder setTexture:self.computeTextures[index] atIndex:1 + index.unsignedIntegerValue];
|
|
238
|
+
}
|
|
239
|
+
[encoder setBuffer:self.uniformBuffer offset:0 atIndex:0];
|
|
240
|
+
|
|
241
|
+
NSUInteger limit = MIN(self.computePipeline.maxTotalThreadsPerThreadgroup,
|
|
242
|
+
self.device.maxThreadsPerThreadgroup.width);
|
|
243
|
+
if (!limit) {
|
|
244
|
+
[encoder endEncoding];
|
|
245
|
+
native_error(error, @"Compute pipeline has no supported threadgroup size");
|
|
246
|
+
return nil;
|
|
247
|
+
}
|
|
248
|
+
NSUInteger width = MAX((NSUInteger)1, MIN(self.computePipeline.threadExecutionWidth, limit));
|
|
249
|
+
// Use complete rows that divide the image, so even older GPUs dispatch no extra pixels.
|
|
250
|
+
while ((NSUInteger)self.texWidth % width != 0) width--;
|
|
251
|
+
[encoder dispatchThreadgroups:MTLSizeMake(self.texWidth / width, self.texHeight, 1)
|
|
252
|
+
threadsPerThreadgroup:MTLSizeMake(width, 1, 1)];
|
|
253
|
+
[encoder endEncoding];
|
|
254
|
+
return commandBuffer;
|
|
215
255
|
}
|
|
216
256
|
|
|
217
257
|
@end
|
|
218
258
|
|
|
219
|
-
|
|
259
|
+
static NSArray<NSString *> *event_modifiers(NSEvent *event) {
|
|
260
|
+
NSEventModifierFlags flags = event.modifierFlags;
|
|
261
|
+
NSMutableArray<NSString *> *result = [NSMutableArray array];
|
|
262
|
+
if (flags & NSEventModifierFlagShift) [result addObject:@"shift"];
|
|
263
|
+
if (flags & NSEventModifierFlagControl) [result addObject:@"control"];
|
|
264
|
+
if (flags & NSEventModifierFlagOption) [result addObject:@"option"];
|
|
265
|
+
if (flags & NSEventModifierFlagCommand) [result addObject:@"command"];
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@interface MetacoWindow : NSWindow <NSWindowDelegate>
|
|
220
270
|
@property (nonatomic, assign) BOOL shouldClose;
|
|
221
271
|
@property (nonatomic, strong) NSMutableArray *pendingEvents;
|
|
222
272
|
@property (nonatomic, strong) MetacoMetalView *metalView;
|
|
223
273
|
@property (nonatomic, assign) BOOL useMetal;
|
|
274
|
+
@property (nonatomic, assign) BOOL highDPI;
|
|
275
|
+
@property (nonatomic, assign) int initialWidth;
|
|
276
|
+
@property (nonatomic, assign) int initialHeight;
|
|
277
|
+
@property (nonatomic, assign) BOOL initializing;
|
|
278
|
+
@property (nonatomic, assign) BOOL hasResized;
|
|
224
279
|
// Fallback for non-Metal
|
|
225
280
|
@property (nonatomic, strong) NSBitmapImageRep *bitmapRep;
|
|
226
281
|
@property (nonatomic, strong) NSImageView *imageView;
|
|
@@ -228,32 +283,41 @@
|
|
|
228
283
|
|
|
229
284
|
@implementation MetacoWindow
|
|
230
285
|
|
|
231
|
-
- (instancetype)initWithWidth:(int)width height:(int)height title:(NSString *)title
|
|
286
|
+
- (instancetype)initWithWidth:(int)width height:(int)height title:(NSString *)title
|
|
287
|
+
resizable:(BOOL)resizable highDPI:(BOOL)highDPI {
|
|
232
288
|
NSRect frame = NSMakeRect(100, 100, width, height);
|
|
289
|
+
NSUInteger style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable;
|
|
290
|
+
if (resizable) style |= NSWindowStyleMaskResizable;
|
|
233
291
|
self = [super initWithContentRect:frame
|
|
234
|
-
styleMask:
|
|
235
|
-
NSWindowStyleMaskClosable |
|
|
236
|
-
NSWindowStyleMaskMiniaturizable)
|
|
292
|
+
styleMask:style
|
|
237
293
|
backing:NSBackingStoreBuffered
|
|
238
294
|
defer:NO];
|
|
239
295
|
if (self) {
|
|
296
|
+
self.releasedWhenClosed = YES;
|
|
240
297
|
self.shouldClose = NO;
|
|
241
298
|
self.pendingEvents = [NSMutableArray array];
|
|
299
|
+
self.highDPI = highDPI;
|
|
300
|
+
self.initialWidth = width;
|
|
301
|
+
self.initialHeight = height;
|
|
302
|
+
self.initializing = YES;
|
|
303
|
+
self.hasResized = NO;
|
|
242
304
|
[self setTitle:title];
|
|
243
|
-
[self setDelegate:
|
|
305
|
+
[self setDelegate:self];
|
|
244
306
|
|
|
245
307
|
// Try to use Metal
|
|
246
308
|
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
|
|
247
309
|
if (device) {
|
|
248
|
-
self.useMetal = YES;
|
|
249
310
|
self.metalView = [[MetacoMetalView alloc] initWithFrame:NSMakeRect(0, 0, width, height)
|
|
250
311
|
device:device
|
|
251
312
|
width:width
|
|
252
313
|
height:height];
|
|
314
|
+
}
|
|
315
|
+
self.useMetal = self.metalView != nil;
|
|
316
|
+
if (self.useMetal) {
|
|
253
317
|
[self setContentView:self.metalView];
|
|
318
|
+
self.metalView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
254
319
|
} else {
|
|
255
|
-
//
|
|
256
|
-
self.useMetal = NO;
|
|
320
|
+
// Device or rendering resource creation failed: use the bitmap path.
|
|
257
321
|
self.bitmapRep = [[NSBitmapImageRep alloc]
|
|
258
322
|
initWithBitmapDataPlanes:NULL
|
|
259
323
|
pixelsWide:width
|
|
@@ -263,19 +327,31 @@
|
|
|
263
327
|
hasAlpha:YES
|
|
264
328
|
isPlanar:NO
|
|
265
329
|
colorSpaceName:NSDeviceRGBColorSpace
|
|
266
|
-
|
|
330
|
+
bitmapFormat:NSBitmapFormatAlphaNonpremultiplied
|
|
331
|
+
bytesPerRow:(size_t)width * 4
|
|
267
332
|
bitsPerPixel:32];
|
|
268
333
|
|
|
334
|
+
if (!self.bitmapRep || !self.bitmapRep.bitmapData) {
|
|
335
|
+
[self close];
|
|
336
|
+
return nil;
|
|
337
|
+
}
|
|
269
338
|
NSImage *image = [[NSImage alloc] initWithSize:NSMakeSize(width, height)];
|
|
270
339
|
[image addRepresentation:self.bitmapRep];
|
|
271
340
|
|
|
272
341
|
self.imageView = [[NSImageView alloc] initWithFrame:NSMakeRect(0, 0, width, height)];
|
|
342
|
+
if (!image || !self.imageView) {
|
|
343
|
+
[self close];
|
|
344
|
+
return nil;
|
|
345
|
+
}
|
|
273
346
|
[self.imageView setImage:image];
|
|
274
347
|
[self.imageView setImageScaling:NSImageScaleAxesIndependently];
|
|
348
|
+
self.imageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
275
349
|
[self setContentView:self.imageView];
|
|
276
350
|
}
|
|
277
351
|
|
|
352
|
+
[self setContentSize:NSMakeSize(width, height)];
|
|
278
353
|
[self setAcceptsMouseMovedEvents:YES];
|
|
354
|
+
self.initializing = NO;
|
|
279
355
|
[self makeKeyAndOrderFront:nil];
|
|
280
356
|
}
|
|
281
357
|
return self;
|
|
@@ -286,11 +362,44 @@
|
|
|
286
362
|
return NO;
|
|
287
363
|
}
|
|
288
364
|
|
|
365
|
+
- (void)windowDidBecomeKey:(NSNotification *)notification {
|
|
366
|
+
[self.pendingEvents addObject:@{@"type": @"focus"}];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
- (void)windowDidResignKey:(NSNotification *)notification {
|
|
370
|
+
[self.pendingEvents addObject:@{@"type": @"blur"}];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
- (void)windowDidResize:(NSNotification *)notification {
|
|
374
|
+
if (self.initializing) return;
|
|
375
|
+
NSRect content = [self contentRectForFrameRect:self.frame];
|
|
376
|
+
if (!self.hasResized && content.size.width <= 1.0 && content.size.height <= 1.0 &&
|
|
377
|
+
(self.initialWidth > 1 || self.initialHeight > 1)) return;
|
|
378
|
+
self.hasResized = YES;
|
|
379
|
+
NSRect backing = [self convertRectToBacking:content];
|
|
380
|
+
int width = MAX(1, (int)llround(content.size.width));
|
|
381
|
+
int height = MAX(1, (int)llround(content.size.height));
|
|
382
|
+
int framebufferWidth = self.highDPI ? MAX(1, (int)llround(backing.size.width)) : width;
|
|
383
|
+
int framebufferHeight = self.highDPI ? MAX(1, (int)llround(backing.size.height)) : height;
|
|
384
|
+
[self.pendingEvents addObject:@{
|
|
385
|
+
@"type": @"resize",
|
|
386
|
+
@"width": @(width),
|
|
387
|
+
@"height": @(height),
|
|
388
|
+
@"framebuffer_width": @(framebufferWidth),
|
|
389
|
+
@"framebuffer_height": @(framebufferHeight)
|
|
390
|
+
}];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
- (void)windowDidChangeBackingProperties:(NSNotification *)notification {
|
|
394
|
+
[self windowDidResize:notification];
|
|
395
|
+
}
|
|
396
|
+
|
|
289
397
|
- (void)keyDown:(NSEvent *)event {
|
|
290
398
|
NSDictionary *eventDict = @{
|
|
291
399
|
@"type": @"key_press",
|
|
292
400
|
@"key": @([event keyCode]),
|
|
293
|
-
@"char": [event characters] ?: @""
|
|
401
|
+
@"char": [event characters] ?: @"",
|
|
402
|
+
@"modifiers": event_modifiers(event)
|
|
294
403
|
};
|
|
295
404
|
[self.pendingEvents addObject:eventDict];
|
|
296
405
|
}
|
|
@@ -298,7 +407,8 @@
|
|
|
298
407
|
- (void)keyUp:(NSEvent *)event {
|
|
299
408
|
NSDictionary *eventDict = @{
|
|
300
409
|
@"type": @"key_release",
|
|
301
|
-
@"key": @([event keyCode])
|
|
410
|
+
@"key": @([event keyCode]),
|
|
411
|
+
@"modifiers": event_modifiers(event)
|
|
302
412
|
};
|
|
303
413
|
[self.pendingEvents addObject:eventDict];
|
|
304
414
|
}
|
|
@@ -309,7 +419,8 @@
|
|
|
309
419
|
@"type": @"mouse_press",
|
|
310
420
|
@"x": @(loc.x),
|
|
311
421
|
@"y": @(loc.y),
|
|
312
|
-
@"button": @([event buttonNumber])
|
|
422
|
+
@"button": @([event buttonNumber]),
|
|
423
|
+
@"modifiers": event_modifiers(event)
|
|
313
424
|
};
|
|
314
425
|
[self.pendingEvents addObject:eventDict];
|
|
315
426
|
}
|
|
@@ -320,7 +431,8 @@
|
|
|
320
431
|
@"type": @"mouse_release",
|
|
321
432
|
@"x": @(loc.x),
|
|
322
433
|
@"y": @(loc.y),
|
|
323
|
-
@"button": @([event buttonNumber])
|
|
434
|
+
@"button": @([event buttonNumber]),
|
|
435
|
+
@"modifiers": event_modifiers(event)
|
|
324
436
|
};
|
|
325
437
|
[self.pendingEvents addObject:eventDict];
|
|
326
438
|
}
|
|
@@ -330,114 +442,435 @@
|
|
|
330
442
|
NSDictionary *eventDict = @{
|
|
331
443
|
@"type": @"mouse_move",
|
|
332
444
|
@"x": @(loc.x),
|
|
333
|
-
@"y": @(loc.y)
|
|
445
|
+
@"y": @(loc.y),
|
|
446
|
+
@"modifiers": event_modifiers(event)
|
|
334
447
|
};
|
|
335
448
|
[self.pendingEvents addObject:eventDict];
|
|
336
449
|
}
|
|
337
450
|
|
|
451
|
+
- (void)scrollWheel:(NSEvent *)event {
|
|
452
|
+
// Cocoa reports wheel ticks for non-precise devices; expose both as pixels.
|
|
453
|
+
double scale = event.hasPreciseScrollingDeltas ? 1.0 : 10.0;
|
|
454
|
+
[self.pendingEvents addObject:@{
|
|
455
|
+
@"type": @"scroll",
|
|
456
|
+
@"dx": @(event.scrollingDeltaX * scale),
|
|
457
|
+
@"dy": @(event.scrollingDeltaY * scale),
|
|
458
|
+
@"modifiers": event_modifiers(event)
|
|
459
|
+
}];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
- (void)rightMouseDown:(NSEvent *)event { [self mouseDown:event]; }
|
|
463
|
+
- (void)rightMouseUp:(NSEvent *)event { [self mouseUp:event]; }
|
|
464
|
+
- (void)otherMouseDown:(NSEvent *)event { [self mouseDown:event]; }
|
|
465
|
+
- (void)otherMouseUp:(NSEvent *)event { [self mouseUp:event]; }
|
|
466
|
+
- (void)mouseDragged:(NSEvent *)event { [self mouseMoved:event]; }
|
|
467
|
+
- (void)rightMouseDragged:(NSEvent *)event { [self mouseMoved:event]; }
|
|
468
|
+
- (void)otherMouseDragged:(NSEvent *)event { [self mouseMoved:event]; }
|
|
469
|
+
|
|
338
470
|
- (BOOL)canBecomeKeyWindow { return YES; }
|
|
339
471
|
- (BOOL)acceptsFirstResponder { return YES; }
|
|
340
472
|
@end
|
|
341
473
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
474
|
+
typedef struct {
|
|
475
|
+
void *window;
|
|
476
|
+
int width;
|
|
477
|
+
int height;
|
|
478
|
+
int logical_width;
|
|
479
|
+
int logical_height;
|
|
480
|
+
size_t byte_length;
|
|
481
|
+
BOOL busy;
|
|
482
|
+
} MetacoHandle;
|
|
483
|
+
|
|
484
|
+
typedef struct {
|
|
485
|
+
void *texture;
|
|
486
|
+
VALUE owner;
|
|
487
|
+
int width;
|
|
488
|
+
int height;
|
|
489
|
+
size_t byte_length;
|
|
490
|
+
} MetacoTextureHandle;
|
|
491
|
+
|
|
492
|
+
static VALUE cMetacoWindow;
|
|
493
|
+
static VALUE cMetacoTexture;
|
|
494
|
+
|
|
495
|
+
static void require_main_thread(void) {
|
|
496
|
+
if (!pthread_main_np()) rb_raise(rb_eThreadError, "Metaco must be called on the main thread");
|
|
349
497
|
}
|
|
350
498
|
|
|
351
|
-
static
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return ULONG2NUM((unsigned long)(__bridge_retained void *)window);
|
|
499
|
+
static size_t pixel_byte_length(int width, int height) {
|
|
500
|
+
if (width <= 0 || height <= 0 || width > 16384 || height > 16384) {
|
|
501
|
+
rb_raise(rb_eArgError, "Width and height must be between 1 and 16384");
|
|
502
|
+
}
|
|
503
|
+
if ((size_t)width > SIZE_MAX / 4 / (size_t)height ||
|
|
504
|
+
(size_t)width * 4 * (size_t)height > LONG_MAX) {
|
|
505
|
+
rb_raise(rb_eArgError, "Pixel buffer size is too large");
|
|
359
506
|
}
|
|
507
|
+
return (size_t)width * (size_t)height * 4;
|
|
360
508
|
}
|
|
361
509
|
|
|
362
|
-
static
|
|
363
|
-
|
|
364
|
-
|
|
510
|
+
static void window_dimensions(MetacoWindow *window, int *logical_width, int *logical_height,
|
|
511
|
+
int *framebuffer_width, int *framebuffer_height) {
|
|
512
|
+
NSRect content = [window contentRectForFrameRect:window.frame];
|
|
513
|
+
NSRect backing = [window convertRectToBacking:content];
|
|
514
|
+
*logical_width = MAX(1, (int)llround(content.size.width));
|
|
515
|
+
*logical_height = MAX(1, (int)llround(content.size.height));
|
|
516
|
+
if (!window.hasResized) {
|
|
517
|
+
*logical_width = window.initialWidth;
|
|
518
|
+
*logical_height = window.initialHeight;
|
|
519
|
+
}
|
|
520
|
+
if (window.highDPI) {
|
|
521
|
+
*framebuffer_width = MAX(*logical_width, (int)llround(backing.size.width));
|
|
522
|
+
*framebuffer_height = MAX(*logical_height, (int)llround(backing.size.height));
|
|
523
|
+
} else {
|
|
524
|
+
*framebuffer_width = *logical_width;
|
|
525
|
+
*framebuffer_height = *logical_height;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
365
528
|
|
|
366
|
-
|
|
367
|
-
|
|
529
|
+
static BOOL resize_bitmap(MetacoWindow *window, int width, int height, NSError **error) {
|
|
530
|
+
NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc]
|
|
531
|
+
initWithBitmapDataPlanes:NULL
|
|
532
|
+
pixelsWide:width
|
|
533
|
+
pixelsHigh:height
|
|
534
|
+
bitsPerSample:8
|
|
535
|
+
samplesPerPixel:4
|
|
536
|
+
hasAlpha:YES
|
|
537
|
+
isPlanar:NO
|
|
538
|
+
colorSpaceName:NSDeviceRGBColorSpace
|
|
539
|
+
bitmapFormat:NSBitmapFormatAlphaNonpremultiplied
|
|
540
|
+
bytesPerRow:(size_t)width * 4
|
|
541
|
+
bitsPerPixel:32];
|
|
542
|
+
NSImage *image = [[NSImage alloc] initWithSize:NSMakeSize(width, height)];
|
|
543
|
+
if (!bitmap || !bitmap.bitmapData || !image) {
|
|
544
|
+
native_error(error, @"Failed to resize bitmap resources");
|
|
545
|
+
return NO;
|
|
546
|
+
}
|
|
547
|
+
[image addRepresentation:bitmap];
|
|
548
|
+
window.bitmapRep = bitmap;
|
|
549
|
+
[window.imageView setFrame:window.contentView.bounds];
|
|
550
|
+
[window.imageView setImage:image];
|
|
551
|
+
return YES;
|
|
552
|
+
}
|
|
368
553
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
// Mark that we've cleaned up the compute shader
|
|
384
|
-
metalView.hasComputeShader = NO;
|
|
554
|
+
static BOOL sync_handle_dimensions(MetacoHandle *handle, NSError **error) {
|
|
555
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
556
|
+
int logical_width, logical_height, framebuffer_width, framebuffer_height;
|
|
557
|
+
window_dimensions(window, &logical_width, &logical_height, &framebuffer_width, &framebuffer_height);
|
|
558
|
+
if (framebuffer_width != handle->width || framebuffer_height != handle->height) {
|
|
559
|
+
BOOL resized = window.useMetal
|
|
560
|
+
? [window.metalView resizeToWidth:framebuffer_width height:framebuffer_height error:error]
|
|
561
|
+
: resize_bitmap(window, framebuffer_width, framebuffer_height, error);
|
|
562
|
+
if (!resized) return NO;
|
|
563
|
+
handle->width = framebuffer_width;
|
|
564
|
+
handle->height = framebuffer_height;
|
|
565
|
+
handle->byte_length = pixel_byte_length(framebuffer_width, framebuffer_height);
|
|
385
566
|
}
|
|
567
|
+
handle->logical_width = logical_width;
|
|
568
|
+
handle->logical_height = logical_height;
|
|
569
|
+
return YES;
|
|
570
|
+
}
|
|
386
571
|
|
|
387
|
-
|
|
388
|
-
|
|
572
|
+
static void release_window(void *ptr) {
|
|
573
|
+
@autoreleasepool {
|
|
574
|
+
MetacoWindow *window = (__bridge_transfer MetacoWindow *)ptr;
|
|
575
|
+
window.delegate = nil;
|
|
576
|
+
[window orderOut:nil];
|
|
577
|
+
[window setContentView:nil];
|
|
578
|
+
[window.metalView releaseResources];
|
|
579
|
+
window.metalView = nil;
|
|
580
|
+
window.imageView = nil;
|
|
581
|
+
window.bitmapRep = nil;
|
|
582
|
+
window.pendingEvents = nil;
|
|
389
583
|
[window close];
|
|
390
584
|
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
static void handle_free(void *ptr) {
|
|
588
|
+
MetacoHandle *handle = ptr;
|
|
589
|
+
if (handle->window) {
|
|
590
|
+
if (pthread_main_np()) release_window(handle->window);
|
|
591
|
+
else dispatch_async_f(dispatch_get_main_queue(), handle->window, release_window);
|
|
592
|
+
}
|
|
593
|
+
ruby_xfree(handle);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
static size_t handle_size(const void *ptr) { return sizeof(MetacoHandle); }
|
|
597
|
+
|
|
598
|
+
static const rb_data_type_t handle_type = {
|
|
599
|
+
.wrap_struct_name = "Metaco::Window",
|
|
600
|
+
.function = {.dfree = handle_free, .dsize = handle_size},
|
|
601
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
static MetacoHandle *get_handle(VALUE value, BOOL allow_closed) {
|
|
605
|
+
require_main_thread();
|
|
606
|
+
MetacoHandle *handle;
|
|
607
|
+
TypedData_Get_Struct(value, MetacoHandle, &handle_type, handle);
|
|
608
|
+
if (!allow_closed && !handle->window) rb_raise(rb_eArgError, "Window is closed");
|
|
609
|
+
if (handle->busy) rb_raise(rb_eRuntimeError, "Window is busy");
|
|
610
|
+
return handle;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
static void texture_mark(void *ptr) {
|
|
614
|
+
MetacoTextureHandle *texture = ptr;
|
|
615
|
+
if (texture) rb_gc_mark(texture->owner);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
static void texture_free(void *ptr) {
|
|
619
|
+
MetacoTextureHandle *texture = ptr;
|
|
620
|
+
if (!texture) return;
|
|
621
|
+
if (texture->texture) CFRelease((CFTypeRef)texture->texture);
|
|
622
|
+
ruby_xfree(texture);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
static size_t texture_size(const void *ptr) { return sizeof(MetacoTextureHandle); }
|
|
391
626
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
627
|
+
static const rb_data_type_t texture_type = {
|
|
628
|
+
.wrap_struct_name = "Metaco::Texture",
|
|
629
|
+
.function = {.dmark = texture_mark, .dfree = texture_free, .dsize = texture_size},
|
|
630
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
static MetacoTextureHandle *get_texture(VALUE value, BOOL allow_destroyed) {
|
|
634
|
+
require_main_thread();
|
|
635
|
+
MetacoTextureHandle *texture;
|
|
636
|
+
TypedData_Get_Struct(value, MetacoTextureHandle, &texture_type, texture);
|
|
637
|
+
if (!allow_destroyed && !texture->texture) rb_raise(rb_eArgError, "Texture is destroyed");
|
|
638
|
+
return texture;
|
|
639
|
+
}
|
|
395
640
|
|
|
641
|
+
static VALUE cocoa_init(VALUE self) {
|
|
642
|
+
require_main_thread();
|
|
643
|
+
@autoreleasepool {
|
|
644
|
+
[NSApplication sharedApplication];
|
|
645
|
+
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
|
646
|
+
[NSApp activateIgnoringOtherApps:YES];
|
|
647
|
+
}
|
|
396
648
|
return Qnil;
|
|
397
649
|
}
|
|
398
650
|
|
|
399
|
-
static VALUE
|
|
651
|
+
static VALUE cocoa_window_create_native(VALUE self, VALUE width, VALUE height, VALUE title,
|
|
652
|
+
VALUE resizable, VALUE high_dpi) {
|
|
653
|
+
require_main_thread();
|
|
654
|
+
int w = NUM2INT(width);
|
|
655
|
+
int h = NUM2INT(height);
|
|
656
|
+
size_t length = pixel_byte_length(w, h);
|
|
657
|
+
Check_Type(title, T_STRING);
|
|
658
|
+
if (!NSApp) rb_raise(rb_eRuntimeError, "Call Metaco.init before creating a window");
|
|
659
|
+
|
|
660
|
+
MetacoHandle *handle;
|
|
661
|
+
VALUE result = TypedData_Make_Struct(cMetacoWindow, MetacoHandle, &handle_type, handle);
|
|
662
|
+
handle->window = NULL;
|
|
663
|
+
handle->width = w;
|
|
664
|
+
handle->height = h;
|
|
665
|
+
handle->logical_width = w;
|
|
666
|
+
handle->logical_height = h;
|
|
667
|
+
handle->byte_length = length;
|
|
668
|
+
BOOL valid_title;
|
|
400
669
|
@autoreleasepool {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
670
|
+
NSString *text = [[NSString alloc] initWithBytes:RSTRING_PTR(title)
|
|
671
|
+
length:RSTRING_LEN(title)
|
|
672
|
+
encoding:NSUTF8StringEncoding];
|
|
673
|
+
valid_title = text != nil;
|
|
674
|
+
if (valid_title) {
|
|
675
|
+
MetacoWindow *window = [[MetacoWindow alloc] initWithWidth:w height:h title:text
|
|
676
|
+
resizable:RTEST(resizable)
|
|
677
|
+
highDPI:RTEST(high_dpi)];
|
|
678
|
+
handle->window = (__bridge_retained void *)window;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
RB_GC_GUARD(title);
|
|
682
|
+
if (!valid_title) rb_raise(rb_eArgError, "Title must contain valid UTF-8");
|
|
683
|
+
if (!handle->window) rb_raise(rb_eRuntimeError, "Failed to create window resources");
|
|
684
|
+
NSError *error = nil;
|
|
685
|
+
if (!sync_handle_dimensions(handle, &error)) {
|
|
686
|
+
void *ptr = handle->window;
|
|
687
|
+
handle->window = NULL;
|
|
688
|
+
if (ptr) release_window(ptr);
|
|
689
|
+
rb_raise(rb_eRuntimeError, "%s", error.localizedDescription.UTF8String);
|
|
690
|
+
}
|
|
691
|
+
return result;
|
|
692
|
+
}
|
|
404
693
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
694
|
+
static VALUE cocoa_window_create(int argc, VALUE *argv, VALUE self) {
|
|
695
|
+
VALUE width, height, title, keywords;
|
|
696
|
+
rb_scan_args(argc, argv, "3:", &width, &height, &title, &keywords);
|
|
697
|
+
VALUE resizable = Qfalse;
|
|
698
|
+
VALUE high_dpi = Qfalse;
|
|
699
|
+
if (!NIL_P(keywords)) {
|
|
700
|
+
ID names[] = {rb_intern("resizable"), rb_intern("high_dpi")};
|
|
701
|
+
VALUE values[] = {Qfalse, Qfalse};
|
|
702
|
+
rb_get_kwargs(keywords, names, 0, 2, values);
|
|
703
|
+
resizable = values[0] == Qundef ? Qfalse : values[0];
|
|
704
|
+
high_dpi = values[1] == Qundef ? Qfalse : values[1];
|
|
705
|
+
}
|
|
706
|
+
return cocoa_window_create_native(self, width, height, title, resizable, high_dpi);
|
|
707
|
+
}
|
|
408
708
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
709
|
+
static VALUE cocoa_window_destroy(VALUE self, VALUE value) {
|
|
710
|
+
MetacoHandle *handle = get_handle(value, YES);
|
|
711
|
+
void *ptr = handle->window;
|
|
712
|
+
handle->window = NULL;
|
|
713
|
+
if (ptr) release_window(ptr);
|
|
714
|
+
RB_GC_GUARD(value);
|
|
715
|
+
return Qnil;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
static VALUE cocoa_set_pixels(VALUE self, VALUE value, VALUE buffer, VALUE width, VALUE height) {
|
|
719
|
+
require_main_thread();
|
|
720
|
+
int w = NUM2INT(width);
|
|
721
|
+
int h = NUM2INT(height);
|
|
722
|
+
size_t length = pixel_byte_length(w, h);
|
|
723
|
+
Check_Type(buffer, T_STRING);
|
|
724
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
725
|
+
NSError *resize_error = nil;
|
|
726
|
+
if (!sync_handle_dimensions(handle, &resize_error)) {
|
|
727
|
+
rb_raise(rb_eRuntimeError, "%s", resize_error.localizedDescription.UTF8String);
|
|
728
|
+
}
|
|
729
|
+
if (w != handle->width || h != handle->height) {
|
|
730
|
+
rb_raise(rb_eArgError, "Pixel dimensions must match the window");
|
|
731
|
+
}
|
|
732
|
+
if ((size_t)RSTRING_LEN(buffer) < length) rb_raise(rb_eArgError, "Buffer too small");
|
|
412
733
|
|
|
734
|
+
@autoreleasepool {
|
|
735
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
413
736
|
if (window.useMetal) {
|
|
414
|
-
[window.metalView updatePixels:
|
|
737
|
+
[window.metalView updatePixels:(const uint8_t *)RSTRING_PTR(buffer)];
|
|
415
738
|
} else {
|
|
416
|
-
|
|
417
|
-
memcpy(bitmapData, data, w * h * 4);
|
|
739
|
+
memcpy(window.bitmapRep.bitmapData, RSTRING_PTR(buffer), handle->byte_length);
|
|
418
740
|
}
|
|
419
741
|
}
|
|
742
|
+
RB_GC_GUARD(buffer);
|
|
743
|
+
RB_GC_GUARD(value);
|
|
420
744
|
return Qnil;
|
|
421
745
|
}
|
|
422
746
|
|
|
423
|
-
static
|
|
747
|
+
static void *submit_command(void *ptr) {
|
|
424
748
|
@autoreleasepool {
|
|
425
|
-
|
|
749
|
+
id<MTLCommandBuffer> command = (__bridge id<MTLCommandBuffer>)ptr;
|
|
750
|
+
[command commit];
|
|
751
|
+
[command waitUntilCompleted];
|
|
752
|
+
}
|
|
753
|
+
return NULL;
|
|
754
|
+
}
|
|
426
755
|
|
|
427
|
-
|
|
428
|
-
|
|
756
|
+
static VALUE submit_without_gvl(VALUE ptr) {
|
|
757
|
+
rb_thread_call_without_gvl(submit_command, (void *)ptr, NULL, NULL);
|
|
758
|
+
return Qnil;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
static void finish_command(id<MTLCommandBuffer> command, int *state, NSError **error) {
|
|
762
|
+
if (!command) return;
|
|
763
|
+
// ponytail: one frame in flight; add buffering only if synchronous presentation limits throughput.
|
|
764
|
+
// rb_protect also catches interrupts while the GVL is released/reacquired.
|
|
765
|
+
rb_protect(submit_without_gvl, (VALUE)(__bridge void *)command, state);
|
|
766
|
+
if (!*state && command.status != MTLCommandBufferStatusCompleted) {
|
|
767
|
+
if (command.error) *error = command.error;
|
|
768
|
+
else native_error(error, @"GPU command failed");
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Only call inside rb_protect (directly or through events_to_ruby).
|
|
773
|
+
static VALUE string_to_ruby(VALUE ptr) {
|
|
774
|
+
__unsafe_unretained NSString *text = (__bridge NSString *)(void *)ptr;
|
|
775
|
+
return rb_utf8_str_new(text.UTF8String, [text lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
static VALUE cocoa_present_frame(VALUE value, BOOL compute) {
|
|
779
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
780
|
+
NSError *resize_error = nil;
|
|
781
|
+
if (!sync_handle_dimensions(handle, &resize_error)) {
|
|
782
|
+
rb_raise(rb_eRuntimeError, "%s", resize_error.localizedDescription.UTF8String);
|
|
783
|
+
}
|
|
784
|
+
VALUE message = Qnil;
|
|
785
|
+
int state = 0;
|
|
786
|
+
handle->busy = YES;
|
|
787
|
+
@autoreleasepool {
|
|
788
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
789
|
+
NSError *error = nil;
|
|
790
|
+
if (compute && (!window.useMetal || !window.metalView.hasComputeShader)) {
|
|
791
|
+
native_error(&error, @"Compute shader not compiled");
|
|
792
|
+
} else if (window.useMetal) {
|
|
793
|
+
id<MTLTexture> texture = compute ? window.metalView.outputTexture : window.metalView.texture;
|
|
794
|
+
id<MTLCommandBuffer> command = [window.metalView presentTexture:texture error:&error];
|
|
795
|
+
window.metalView.lastCommandBuffer = command;
|
|
796
|
+
finish_command(command, &state, &error);
|
|
797
|
+
if (!state) window.metalView.lastCommandBuffer = nil;
|
|
429
798
|
} else {
|
|
430
799
|
[window.imageView setNeedsDisplay:YES];
|
|
431
800
|
[window displayIfNeeded];
|
|
432
801
|
}
|
|
802
|
+
if (error && !state) {
|
|
803
|
+
message = rb_protect(string_to_ruby, (VALUE)(__bridge void *)error.localizedDescription, &state);
|
|
804
|
+
}
|
|
433
805
|
}
|
|
806
|
+
handle->busy = NO;
|
|
807
|
+
RB_GC_GUARD(value);
|
|
808
|
+
if (state) rb_jump_tag(state);
|
|
809
|
+
if (!NIL_P(message)) rb_exc_raise(rb_exc_new_str(rb_eRuntimeError, message));
|
|
434
810
|
return Qnil;
|
|
435
811
|
}
|
|
436
812
|
|
|
437
|
-
static VALUE
|
|
438
|
-
|
|
439
|
-
|
|
813
|
+
static VALUE cocoa_present(VALUE self, VALUE value) { return cocoa_present_frame(value, NO); }
|
|
814
|
+
static VALUE cocoa_present_compute(VALUE self, VALUE value) { return cocoa_present_frame(value, YES); }
|
|
815
|
+
|
|
816
|
+
// Called only via rb_protect: no owning Objective-C locals or autorelease pools
|
|
817
|
+
// may live in a frame that Ruby's longjmp can bypass.
|
|
818
|
+
static VALUE events_to_ruby(VALUE ptr) {
|
|
819
|
+
__unsafe_unretained NSArray *pending = (__bridge NSArray *)(void *)ptr;
|
|
820
|
+
VALUE events = rb_ary_new();
|
|
821
|
+
for (NSUInteger i = 0; i < pending.count; i++) {
|
|
822
|
+
__unsafe_unretained NSDictionary *dict = pending[i];
|
|
823
|
+
VALUE hash = rb_hash_new();
|
|
824
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("type")),
|
|
825
|
+
ID2SYM(rb_intern([dict[@"type"] UTF8String])));
|
|
826
|
+
if (dict[@"key"]) {
|
|
827
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("key")), INT2NUM([dict[@"key"] intValue]));
|
|
828
|
+
}
|
|
829
|
+
if (dict[@"char"]) {
|
|
830
|
+
__unsafe_unretained NSString *characters = dict[@"char"];
|
|
831
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("char")),
|
|
832
|
+
string_to_ruby((VALUE)(__bridge void *)characters));
|
|
833
|
+
}
|
|
834
|
+
if (dict[@"x"]) {
|
|
835
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("x")), DBL2NUM([dict[@"x"] doubleValue]));
|
|
836
|
+
}
|
|
837
|
+
if (dict[@"y"]) {
|
|
838
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("y")), DBL2NUM([dict[@"y"] doubleValue]));
|
|
839
|
+
}
|
|
840
|
+
if (dict[@"button"]) {
|
|
841
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("button")), INT2NUM([dict[@"button"] intValue]));
|
|
842
|
+
}
|
|
843
|
+
if (dict[@"dx"]) rb_hash_aset(hash, ID2SYM(rb_intern("dx")), DBL2NUM([dict[@"dx"] doubleValue]));
|
|
844
|
+
if (dict[@"dy"]) rb_hash_aset(hash, ID2SYM(rb_intern("dy")), DBL2NUM([dict[@"dy"] doubleValue]));
|
|
845
|
+
if (dict[@"width"]) rb_hash_aset(hash, ID2SYM(rb_intern("width")), INT2NUM([dict[@"width"] intValue]));
|
|
846
|
+
if (dict[@"height"]) rb_hash_aset(hash, ID2SYM(rb_intern("height")), INT2NUM([dict[@"height"] intValue]));
|
|
847
|
+
if (dict[@"framebuffer_width"]) {
|
|
848
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("framebuffer_width")),
|
|
849
|
+
INT2NUM([dict[@"framebuffer_width"] intValue]));
|
|
850
|
+
}
|
|
851
|
+
if (dict[@"framebuffer_height"]) {
|
|
852
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("framebuffer_height")),
|
|
853
|
+
INT2NUM([dict[@"framebuffer_height"] intValue]));
|
|
854
|
+
}
|
|
855
|
+
if (dict[@"modifiers"]) {
|
|
856
|
+
VALUE modifiers = rb_ary_new();
|
|
857
|
+
for (__unsafe_unretained NSString *modifier in dict[@"modifiers"]) {
|
|
858
|
+
rb_ary_push(modifiers, ID2SYM(rb_intern(modifier.UTF8String)));
|
|
859
|
+
}
|
|
860
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("modifiers")), modifiers);
|
|
861
|
+
}
|
|
862
|
+
rb_ary_push(events, hash);
|
|
863
|
+
}
|
|
864
|
+
return events;
|
|
865
|
+
}
|
|
440
866
|
|
|
867
|
+
static VALUE cocoa_poll_events(VALUE self, VALUE value) {
|
|
868
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
869
|
+
int state = 0;
|
|
870
|
+
VALUE events;
|
|
871
|
+
handle->busy = YES;
|
|
872
|
+
@autoreleasepool {
|
|
873
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
441
874
|
NSEvent *event;
|
|
442
875
|
while ((event = [NSApp nextEventMatchingMask:NSEventMaskAny
|
|
443
876
|
untilDate:nil
|
|
@@ -445,111 +878,310 @@ static VALUE cocoa_poll_events(VALUE self, VALUE window_ptr) {
|
|
|
445
878
|
dequeue:YES])) {
|
|
446
879
|
[NSApp sendEvent:event];
|
|
447
880
|
}
|
|
448
|
-
|
|
449
881
|
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
|
|
450
882
|
beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.001]];
|
|
451
883
|
[NSApp updateWindows];
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
for (NSDictionary *dict in window.pendingEvents) {
|
|
455
|
-
VALUE hash = rb_hash_new();
|
|
456
|
-
|
|
457
|
-
NSString *type = dict[@"type"];
|
|
458
|
-
rb_hash_aset(hash, ID2SYM(rb_intern("type")),
|
|
459
|
-
ID2SYM(rb_intern([type UTF8String])));
|
|
460
|
-
|
|
461
|
-
if (dict[@"key"]) {
|
|
462
|
-
rb_hash_aset(hash, ID2SYM(rb_intern("key")), INT2NUM([dict[@"key"] intValue]));
|
|
463
|
-
}
|
|
464
|
-
if (dict[@"char"]) {
|
|
465
|
-
rb_hash_aset(hash, ID2SYM(rb_intern("char")),
|
|
466
|
-
rb_str_new_cstr([dict[@"char"] UTF8String]));
|
|
467
|
-
}
|
|
468
|
-
if (dict[@"x"]) {
|
|
469
|
-
rb_hash_aset(hash, ID2SYM(rb_intern("x")), DBL2NUM([dict[@"x"] doubleValue]));
|
|
470
|
-
}
|
|
471
|
-
if (dict[@"y"]) {
|
|
472
|
-
rb_hash_aset(hash, ID2SYM(rb_intern("y")), DBL2NUM([dict[@"y"] doubleValue]));
|
|
473
|
-
}
|
|
474
|
-
if (dict[@"button"]) {
|
|
475
|
-
rb_hash_aset(hash, ID2SYM(rb_intern("button")), INT2NUM([dict[@"button"] intValue]));
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
rb_ary_push(events, hash);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
[window.pendingEvents removeAllObjects];
|
|
482
|
-
return events;
|
|
884
|
+
events = rb_protect(events_to_ruby, (VALUE)(__bridge void *)window.pendingEvents, &state);
|
|
885
|
+
if (!state) [window.pendingEvents removeAllObjects];
|
|
483
886
|
}
|
|
887
|
+
handle->busy = NO;
|
|
888
|
+
RB_GC_GUARD(value);
|
|
889
|
+
if (state) rb_jump_tag(state);
|
|
890
|
+
return events;
|
|
484
891
|
}
|
|
485
892
|
|
|
486
|
-
static VALUE cocoa_should_close(VALUE self, VALUE
|
|
487
|
-
|
|
488
|
-
|
|
893
|
+
static VALUE cocoa_should_close(VALUE self, VALUE value) {
|
|
894
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
895
|
+
BOOL result;
|
|
896
|
+
@autoreleasepool {
|
|
897
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
898
|
+
result = window.shouldClose;
|
|
899
|
+
}
|
|
900
|
+
RB_GC_GUARD(value);
|
|
901
|
+
return result ? Qtrue : Qfalse;
|
|
489
902
|
}
|
|
490
903
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
904
|
+
static VALUE cocoa_metal_compute_available(VALUE self, VALUE value) {
|
|
905
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
906
|
+
BOOL result;
|
|
907
|
+
@autoreleasepool {
|
|
908
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
909
|
+
result = window.useMetal;
|
|
910
|
+
}
|
|
911
|
+
RB_GC_GUARD(value);
|
|
912
|
+
return result ? Qtrue : Qfalse;
|
|
496
913
|
}
|
|
497
914
|
|
|
498
|
-
static VALUE cocoa_compile_compute_shader(VALUE self, VALUE
|
|
915
|
+
static VALUE cocoa_compile_compute_shader(VALUE self, VALUE value, VALUE msl_source) {
|
|
916
|
+
Check_Type(msl_source, T_STRING);
|
|
917
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
918
|
+
NSError *resize_error = nil;
|
|
919
|
+
if (!sync_handle_dimensions(handle, &resize_error)) {
|
|
920
|
+
rb_raise(rb_eRuntimeError, "%s", resize_error.localizedDescription.UTF8String);
|
|
921
|
+
}
|
|
922
|
+
VALUE message = Qnil;
|
|
923
|
+
int state = 0;
|
|
924
|
+
handle->busy = YES;
|
|
499
925
|
@autoreleasepool {
|
|
500
|
-
MetacoWindow *window = (__bridge MetacoWindow *)
|
|
926
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
927
|
+
NSString *source = [[NSString alloc] initWithBytes:RSTRING_PTR(msl_source)
|
|
928
|
+
length:RSTRING_LEN(msl_source)
|
|
929
|
+
encoding:NSUTF8StringEncoding];
|
|
930
|
+
NSError *error = nil;
|
|
501
931
|
if (!window.useMetal) {
|
|
502
|
-
|
|
932
|
+
native_error(&error, @"Metal not available");
|
|
933
|
+
} else if (!source) {
|
|
934
|
+
native_error(&error, @"Shader source must contain valid UTF-8");
|
|
935
|
+
} else if (![window.metalView compileComputeShader:source error:&error] && !error) {
|
|
936
|
+
native_error(&error, @"Failed to compile shader");
|
|
503
937
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
NSString *source = [NSString stringWithUTF8String:StringValueCStr(msl_source)];
|
|
507
|
-
NSError *error = nil;
|
|
508
|
-
|
|
509
|
-
if (![window.metalView compileComputeShader:source error:&error]) {
|
|
510
|
-
rb_raise(rb_eRuntimeError, "Failed to compile shader: %s",
|
|
511
|
-
[[error localizedDescription] UTF8String]);
|
|
938
|
+
if (error) {
|
|
939
|
+
message = rb_protect(string_to_ruby, (VALUE)(__bridge void *)error.localizedDescription, &state);
|
|
512
940
|
}
|
|
513
941
|
}
|
|
942
|
+
handle->busy = NO;
|
|
943
|
+
RB_GC_GUARD(msl_source);
|
|
944
|
+
RB_GC_GUARD(value);
|
|
945
|
+
if (state) rb_jump_tag(state);
|
|
946
|
+
if (!NIL_P(message)) rb_exc_raise(rb_exc_new_str(rb_eRuntimeError, message));
|
|
514
947
|
return Qtrue;
|
|
515
948
|
}
|
|
516
949
|
|
|
517
|
-
static VALUE cocoa_dispatch_compute(VALUE self, VALUE
|
|
950
|
+
static VALUE cocoa_dispatch_compute(VALUE self, VALUE value, VALUE uniform_data) {
|
|
951
|
+
Check_Type(uniform_data, T_STRING);
|
|
952
|
+
if (RSTRING_LEN(uniform_data) > 256) rb_raise(rb_eArgError, "Uniform data must be at most 256 bytes");
|
|
953
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
954
|
+
NSError *resize_error = nil;
|
|
955
|
+
if (!sync_handle_dimensions(handle, &resize_error)) {
|
|
956
|
+
rb_raise(rb_eRuntimeError, "%s", resize_error.localizedDescription.UTF8String);
|
|
957
|
+
}
|
|
958
|
+
VALUE message = Qnil;
|
|
959
|
+
int state = 0;
|
|
960
|
+
handle->busy = YES;
|
|
518
961
|
@autoreleasepool {
|
|
519
|
-
MetacoWindow *window = (__bridge MetacoWindow *)
|
|
962
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
963
|
+
NSError *error = nil;
|
|
520
964
|
if (!window.useMetal || !window.metalView.hasComputeShader) {
|
|
521
|
-
|
|
965
|
+
native_error(&error, @"Compute shader not compiled");
|
|
966
|
+
} else {
|
|
967
|
+
id<MTLCommandBuffer> command = [window.metalView dispatchComputeWithUniforms:RSTRING_PTR(uniform_data)
|
|
968
|
+
length:RSTRING_LEN(uniform_data)
|
|
969
|
+
error:&error];
|
|
970
|
+
window.metalView.lastCommandBuffer = command;
|
|
971
|
+
finish_command(command, &state, &error);
|
|
972
|
+
if (!state) window.metalView.lastCommandBuffer = nil;
|
|
973
|
+
}
|
|
974
|
+
if (error && !state) {
|
|
975
|
+
message = rb_protect(string_to_ruby, (VALUE)(__bridge void *)error.localizedDescription, &state);
|
|
522
976
|
}
|
|
977
|
+
}
|
|
978
|
+
handle->busy = NO;
|
|
979
|
+
RB_GC_GUARD(uniform_data);
|
|
980
|
+
RB_GC_GUARD(value);
|
|
981
|
+
if (state) rb_jump_tag(state);
|
|
982
|
+
if (!NIL_P(message)) rb_exc_raise(rb_exc_new_str(rb_eRuntimeError, message));
|
|
983
|
+
return Qnil;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
static VALUE cocoa_has_compute_shader(VALUE self, VALUE value) {
|
|
987
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
988
|
+
BOOL result;
|
|
989
|
+
@autoreleasepool {
|
|
990
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
991
|
+
result = window.useMetal && window.metalView.hasComputeShader;
|
|
992
|
+
}
|
|
993
|
+
RB_GC_GUARD(value);
|
|
994
|
+
return result ? Qtrue : Qfalse;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
static VALUE cocoa_window_size(VALUE self, VALUE value) {
|
|
998
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
999
|
+
NSError *resize_error = nil;
|
|
1000
|
+
if (!sync_handle_dimensions(handle, &resize_error)) {
|
|
1001
|
+
rb_raise(rb_eRuntimeError, "%s", resize_error.localizedDescription.UTF8String);
|
|
1002
|
+
}
|
|
1003
|
+
VALUE dimensions = rb_ary_new_from_args(2, INT2NUM(handle->logical_width), INT2NUM(handle->logical_height));
|
|
1004
|
+
RB_GC_GUARD(value);
|
|
1005
|
+
return dimensions;
|
|
1006
|
+
}
|
|
523
1007
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1008
|
+
static VALUE cocoa_framebuffer_size(VALUE self, VALUE value) {
|
|
1009
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
1010
|
+
NSError *resize_error = nil;
|
|
1011
|
+
if (!sync_handle_dimensions(handle, &resize_error)) {
|
|
1012
|
+
rb_raise(rb_eRuntimeError, "%s", resize_error.localizedDescription.UTF8String);
|
|
1013
|
+
}
|
|
1014
|
+
VALUE dimensions = rb_ary_new_from_args(2, INT2NUM(handle->width), INT2NUM(handle->height));
|
|
1015
|
+
RB_GC_GUARD(value);
|
|
1016
|
+
return dimensions;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
static VALUE cocoa_texture_create_native(VALUE self, VALUE value, VALUE width, VALUE height, VALUE bytes) {
|
|
1020
|
+
int w = NUM2INT(width);
|
|
1021
|
+
int h = NUM2INT(height);
|
|
1022
|
+
size_t length = pixel_byte_length(w, h);
|
|
1023
|
+
Check_Type(bytes, T_STRING);
|
|
1024
|
+
if ((size_t)RSTRING_LEN(bytes) != length) rb_raise(rb_eArgError, "Texture buffer size mismatch");
|
|
1025
|
+
MetacoHandle *owner = get_handle(value, NO);
|
|
1026
|
+
MetacoWindow *window = (__bridge MetacoWindow *)owner->window;
|
|
1027
|
+
if (!window.useMetal) rb_raise(rb_eRuntimeError, "Metal is not available");
|
|
1028
|
+
MetacoTextureHandle *texture;
|
|
1029
|
+
VALUE result = TypedData_Make_Struct(cMetacoTexture, MetacoTextureHandle, &texture_type, texture);
|
|
1030
|
+
texture->owner = value;
|
|
1031
|
+
texture->width = w;
|
|
1032
|
+
texture->height = h;
|
|
1033
|
+
texture->byte_length = length;
|
|
1034
|
+
@autoreleasepool {
|
|
1035
|
+
MTLTextureDescriptor *desc = [MTLTextureDescriptor
|
|
1036
|
+
texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:w height:h mipmapped:NO];
|
|
1037
|
+
desc.usage = MTLTextureUsageShaderRead;
|
|
1038
|
+
desc.storageMode = MTLStorageModeManaged;
|
|
1039
|
+
if (@available(macOS 10.15, *)) {
|
|
1040
|
+
if (window.metalView.device.hasUnifiedMemory) desc.storageMode = MTLStorageModeShared;
|
|
1041
|
+
}
|
|
1042
|
+
id<MTLTexture> metalTexture = [window.metalView.device newTextureWithDescriptor:desc];
|
|
1043
|
+
if (metalTexture) {
|
|
1044
|
+
[metalTexture replaceRegion:MTLRegionMake2D(0, 0, w, h) mipmapLevel:0
|
|
1045
|
+
withBytes:RSTRING_PTR(bytes) bytesPerRow:(size_t)w * 4];
|
|
1046
|
+
texture->texture = (__bridge_retained void *)metalTexture;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
RB_GC_GUARD(bytes);
|
|
1050
|
+
RB_GC_GUARD(value);
|
|
1051
|
+
if (!texture->texture) rb_raise(rb_eRuntimeError, "Failed to allocate Metal texture");
|
|
1052
|
+
return result;
|
|
1053
|
+
}
|
|
527
1054
|
|
|
528
|
-
|
|
1055
|
+
static VALUE cocoa_texture_update(VALUE self, VALUE value, VALUE bytes) {
|
|
1056
|
+
MetacoTextureHandle *texture = get_texture(value, NO);
|
|
1057
|
+
get_handle(texture->owner, NO);
|
|
1058
|
+
Check_Type(bytes, T_STRING);
|
|
1059
|
+
if ((size_t)RSTRING_LEN(bytes) != texture->byte_length) rb_raise(rb_eArgError, "Texture buffer size mismatch");
|
|
1060
|
+
@autoreleasepool {
|
|
1061
|
+
id<MTLTexture> metalTexture = (__bridge id<MTLTexture>)texture->texture;
|
|
1062
|
+
[metalTexture replaceRegion:MTLRegionMake2D(0, 0, texture->width, texture->height)
|
|
1063
|
+
mipmapLevel:0 withBytes:RSTRING_PTR(bytes)
|
|
1064
|
+
bytesPerRow:(size_t)texture->width * 4];
|
|
529
1065
|
}
|
|
1066
|
+
RB_GC_GUARD(bytes);
|
|
1067
|
+
RB_GC_GUARD(value);
|
|
530
1068
|
return Qnil;
|
|
531
1069
|
}
|
|
532
1070
|
|
|
533
|
-
static VALUE
|
|
1071
|
+
static VALUE cocoa_texture_destroy(VALUE self, VALUE value) {
|
|
1072
|
+
MetacoTextureHandle *texture = get_texture(value, YES);
|
|
1073
|
+
if (!texture->texture) return Qnil;
|
|
1074
|
+
MetacoHandle *owner;
|
|
1075
|
+
TypedData_Get_Struct(texture->owner, MetacoHandle, &handle_type, owner);
|
|
534
1076
|
@autoreleasepool {
|
|
535
|
-
|
|
536
|
-
if (window
|
|
537
|
-
|
|
1077
|
+
id<MTLTexture> metalTexture = (__bridge id<MTLTexture>)texture->texture;
|
|
1078
|
+
if (owner->window) {
|
|
1079
|
+
MetacoWindow *window = (__bridge MetacoWindow *)owner->window;
|
|
1080
|
+
NSArray *keys = [window.metalView.computeTextures allKeysForObject:metalTexture];
|
|
1081
|
+
[window.metalView.computeTextures removeObjectsForKeys:keys];
|
|
538
1082
|
}
|
|
1083
|
+
CFRelease((CFTypeRef)texture->texture);
|
|
1084
|
+
texture->texture = NULL;
|
|
1085
|
+
}
|
|
1086
|
+
RB_GC_GUARD(value);
|
|
1087
|
+
return Qnil;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
static VALUE cocoa_bind_compute_texture(VALUE self, VALUE value, VALUE index, VALUE texture_value) {
|
|
1091
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
1092
|
+
int slot = NUM2INT(index);
|
|
1093
|
+
if (slot < 0 || slot > 15) rb_raise(rb_eArgError, "Texture index must be between 0 and 15");
|
|
1094
|
+
MetacoTextureHandle *texture = get_texture(texture_value, NO);
|
|
1095
|
+
if (texture->owner != value) rb_raise(rb_eArgError, "Texture belongs to another window");
|
|
1096
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
1097
|
+
if (!window.useMetal) rb_raise(rb_eRuntimeError, "Metal is not available");
|
|
1098
|
+
@autoreleasepool {
|
|
1099
|
+
window.metalView.computeTextures[@(slot)] = (__bridge id<MTLTexture>)texture->texture;
|
|
539
1100
|
}
|
|
1101
|
+
RB_GC_GUARD(value);
|
|
1102
|
+
RB_GC_GUARD(texture_value);
|
|
540
1103
|
return Qnil;
|
|
541
1104
|
}
|
|
542
1105
|
|
|
543
|
-
static VALUE
|
|
544
|
-
|
|
545
|
-
|
|
1106
|
+
static VALUE cocoa_read_pixels_native(VALUE self, VALUE value, VALUE source) {
|
|
1107
|
+
if (!SYMBOL_P(source)) rb_raise(rb_eTypeError, "source must be a Symbol");
|
|
1108
|
+
ID source_id = SYM2ID(source);
|
|
1109
|
+
BOOL compute = source_id == rb_intern("compute");
|
|
1110
|
+
if (!compute && source_id != rb_intern("set_pixels")) {
|
|
1111
|
+
rb_raise(rb_eArgError, "source must be :compute or :set_pixels");
|
|
1112
|
+
}
|
|
1113
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
1114
|
+
NSError *resize_error = nil;
|
|
1115
|
+
if (!sync_handle_dimensions(handle, &resize_error)) {
|
|
1116
|
+
rb_raise(rb_eRuntimeError, "%s", resize_error.localizedDescription.UTF8String);
|
|
1117
|
+
}
|
|
1118
|
+
VALUE output = rb_str_new(NULL, handle->byte_length);
|
|
1119
|
+
VALUE message = Qnil;
|
|
1120
|
+
int state = 0;
|
|
1121
|
+
handle->busy = YES;
|
|
1122
|
+
@autoreleasepool {
|
|
1123
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
1124
|
+
NSError *error = nil;
|
|
1125
|
+
if (!window.useMetal) {
|
|
1126
|
+
if (compute) native_error(&error, @"Metal compute is not available");
|
|
1127
|
+
else memcpy(RSTRING_PTR(output), window.bitmapRep.bitmapData, handle->byte_length);
|
|
1128
|
+
} else {
|
|
1129
|
+
id<MTLTexture> sourceTexture = compute ? window.metalView.outputTexture : window.metalView.texture;
|
|
1130
|
+
if (!sourceTexture) {
|
|
1131
|
+
native_error(&error, @"Pixel source is not available");
|
|
1132
|
+
} else {
|
|
1133
|
+
MTLTextureDescriptor *desc = [MTLTextureDescriptor
|
|
1134
|
+
texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
|
|
1135
|
+
width:handle->width height:handle->height mipmapped:NO];
|
|
1136
|
+
desc.storageMode = MTLStorageModeManaged;
|
|
1137
|
+
if (@available(macOS 10.15, *)) {
|
|
1138
|
+
if (window.metalView.device.hasUnifiedMemory) desc.storageMode = MTLStorageModeShared;
|
|
1139
|
+
}
|
|
1140
|
+
desc.usage = MTLTextureUsageShaderRead;
|
|
1141
|
+
id<MTLTexture> staging = [window.metalView.device newTextureWithDescriptor:desc];
|
|
1142
|
+
id<MTLCommandBuffer> command = [window.metalView.commandQueue commandBuffer];
|
|
1143
|
+
id<MTLBlitCommandEncoder> blit = [command blitCommandEncoder];
|
|
1144
|
+
if (!staging || !command || !blit) {
|
|
1145
|
+
native_error(&error, @"Failed to allocate readback resources");
|
|
1146
|
+
} else {
|
|
1147
|
+
[blit copyFromTexture:sourceTexture sourceSlice:0 sourceLevel:0
|
|
1148
|
+
sourceOrigin:MTLOriginMake(0, 0, 0)
|
|
1149
|
+
sourceSize:MTLSizeMake(handle->width, handle->height, 1)
|
|
1150
|
+
toTexture:staging destinationSlice:0 destinationLevel:0
|
|
1151
|
+
destinationOrigin:MTLOriginMake(0, 0, 0)];
|
|
1152
|
+
if (desc.storageMode == MTLStorageModeManaged) [blit synchronizeResource:staging];
|
|
1153
|
+
[blit endEncoding];
|
|
1154
|
+
finish_command(command, &state, &error);
|
|
1155
|
+
if (!state && !error) {
|
|
1156
|
+
[staging getBytes:RSTRING_PTR(output) bytesPerRow:(size_t)handle->width * 4
|
|
1157
|
+
fromRegion:MTLRegionMake2D(0, 0, handle->width, handle->height)
|
|
1158
|
+
mipmapLevel:0];
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (error && !state) {
|
|
1164
|
+
message = rb_protect(string_to_ruby, (VALUE)(__bridge void *)error.localizedDescription, &state);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
handle->busy = NO;
|
|
1168
|
+
RB_GC_GUARD(value);
|
|
1169
|
+
if (state) rb_jump_tag(state);
|
|
1170
|
+
if (!NIL_P(message)) rb_exc_raise(rb_exc_new_str(rb_eRuntimeError, message));
|
|
1171
|
+
return output;
|
|
546
1172
|
}
|
|
547
1173
|
|
|
548
1174
|
void Init_metaco(void) {
|
|
549
1175
|
VALUE mMetaco = rb_define_module("Metaco");
|
|
1176
|
+
cMetacoWindow = rb_define_class_under(mMetaco, "Window", rb_cObject);
|
|
1177
|
+
rb_global_variable(&cMetacoWindow);
|
|
1178
|
+
rb_undef_alloc_func(cMetacoWindow);
|
|
1179
|
+
cMetacoTexture = rb_define_class_under(mMetaco, "Texture", rb_cObject);
|
|
1180
|
+
rb_global_variable(&cMetacoTexture);
|
|
1181
|
+
rb_undef_alloc_func(cMetacoTexture);
|
|
550
1182
|
|
|
551
1183
|
rb_define_module_function(mMetaco, "init", cocoa_init, 0);
|
|
552
|
-
rb_define_module_function(mMetaco, "window_create", cocoa_window_create,
|
|
1184
|
+
rb_define_module_function(mMetaco, "window_create", cocoa_window_create, -1);
|
|
553
1185
|
rb_define_module_function(mMetaco, "window_destroy", cocoa_window_destroy, 1);
|
|
554
1186
|
rb_define_module_function(mMetaco, "set_pixels", cocoa_set_pixels, 4);
|
|
555
1187
|
rb_define_module_function(mMetaco, "present", cocoa_present, 1);
|
|
@@ -562,4 +1194,11 @@ void Init_metaco(void) {
|
|
|
562
1194
|
rb_define_module_function(mMetaco, "dispatch_compute", cocoa_dispatch_compute, 2);
|
|
563
1195
|
rb_define_module_function(mMetaco, "present_compute", cocoa_present_compute, 1);
|
|
564
1196
|
rb_define_module_function(mMetaco, "has_compute_shader?", cocoa_has_compute_shader, 1);
|
|
1197
|
+
rb_define_module_function(mMetaco, "window_size", cocoa_window_size, 1);
|
|
1198
|
+
rb_define_module_function(mMetaco, "framebuffer_size", cocoa_framebuffer_size, 1);
|
|
1199
|
+
rb_define_module_function(mMetaco, "read_pixels_native", cocoa_read_pixels_native, 2);
|
|
1200
|
+
rb_define_module_function(mMetaco, "texture_create_native", cocoa_texture_create_native, 4);
|
|
1201
|
+
rb_define_module_function(mMetaco, "texture_update", cocoa_texture_update, 2);
|
|
1202
|
+
rb_define_module_function(mMetaco, "texture_destroy", cocoa_texture_destroy, 1);
|
|
1203
|
+
rb_define_module_function(mMetaco, "bind_compute_texture", cocoa_bind_compute_texture, 3);
|
|
565
1204
|
}
|