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.
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, assign) BOOL hasComputeShader;
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
- - (void)cleanup {
27
- // Only cleanup if we have resources
28
- if (!self.device) return;
29
-
30
- // Wait for GPU to finish before releasing resources
31
- if (self.commandQueue) {
32
- @try {
33
- id<MTLCommandBuffer> syncBuffer = [self.commandQueue commandBuffer];
34
- if (syncBuffer) {
35
- [syncBuffer commit];
36
- [syncBuffer waitUntilCompleted];
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 = NO;
69
- self.metalLayer.displaySyncEnabled = NO; // Disable VSync for max FPS
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
- MTLRegion region = MTLRegionMake2D(0, 0, self.texWidth, self.texHeight);
89
- [self.texture replaceRegion:region mipmapLevel:0 withBytes:data bytesPerRow:self.texWidth * 4];
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
- - (void)present {
93
- id<CAMetalDrawable> drawable = [self.metalLayer nextDrawable];
94
- if (!drawable) return;
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
- // Blit texture to drawable
99
- id<MTLBlitCommandEncoder> blitEncoder = [commandBuffer blitCommandEncoder];
100
-
101
- [blitEncoder copyFromTexture:self.texture
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
- [commandBuffer commit];
144
+ return commandBuffer;
115
145
  }
116
146
 
117
- #pragma mark - Compute Shader Support
118
-
119
147
  - (BOOL)compileComputeShader:(NSString *)mslSource error:(NSError **)error {
120
- // Compile MSL source to library
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
- // Get compute function
127
- id<MTLFunction> computeFunc = [library newFunctionWithName:@"compute_shader"];
128
- if (!computeFunc) {
129
- if (error) *error = [NSError errorWithDomain:@"Metaco" code:1
130
- userInfo:@{NSLocalizedDescriptionKey: @"compute_shader function not found"}];
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
- // Create compute pipeline
135
- self.computePipeline = [self.device newComputePipelineStateWithFunction:computeFunc error:error];
136
- if (!self.computePipeline) return NO;
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
- - (void)dispatchComputeWithUniforms:(const void *)uniformData length:(NSUInteger)length {
156
- if (!self.hasComputeShader) return;
157
-
158
- // Update uniform buffer
159
- memcpy(self.uniformBuffer.contents, uniformData, MIN(length, 256));
160
-
161
- id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
162
- id<MTLComputeCommandEncoder> computeEncoder = [commandBuffer computeCommandEncoder];
163
-
164
- [computeEncoder setComputePipelineState:self.computePipeline];
165
- [computeEncoder setTexture:self.outputTexture atIndex:0];
166
- [computeEncoder setBuffer:self.uniformBuffer offset:0 atIndex:0];
167
-
168
- // Calculate optimal thread group size
169
- NSUInteger threadGroupSize = self.computePipeline.maxTotalThreadsPerThreadgroup;
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
- MTLSize threadsPerGroup = MTLSizeMake(threadGroupWidth, threadGroupHeight, 1);
179
- MTLSize threadGroups = MTLSizeMake(
180
- (self.texWidth + threadGroupWidth - 1) / threadGroupWidth,
181
- (self.texHeight + threadGroupHeight - 1) / threadGroupHeight,
182
- 1
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)presentCompute {
193
- if (!self.hasComputeShader) return;
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
- id<CAMetalDrawable> drawable = [self.metalLayer nextDrawable];
196
- if (!drawable) return;
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<MTLBlitCommandEncoder> blitEncoder = [commandBuffer blitCommandEncoder];
200
-
201
- // Blit from compute output to drawable
202
- [blitEncoder copyFromTexture:self.outputTexture
203
- sourceSlice:0
204
- sourceLevel:0
205
- sourceOrigin:MTLOriginMake(0, 0, 0)
206
- sourceSize:MTLSizeMake(self.texWidth, self.texHeight, 1)
207
- toTexture:drawable.texture
208
- destinationSlice:0
209
- destinationLevel:0
210
- destinationOrigin:MTLOriginMake(0, 0, 0)];
211
-
212
- [blitEncoder endEncoding];
213
- [commandBuffer presentDrawable:drawable];
214
- [commandBuffer commit];
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
- @interface MetacoWindow : NSWindow
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:(NSWindowStyleMaskTitled |
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:(id<NSWindowDelegate>)self];
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
- // Fallback to bitmap
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
- bytesPerRow:width * 4
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
- static VALUE cocoa_init(VALUE self) {
343
- @autoreleasepool {
344
- [NSApplication sharedApplication];
345
- [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
346
- [NSApp activateIgnoringOtherApps:YES];
347
- }
348
- return Qnil;
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 VALUE cocoa_window_create(VALUE self, VALUE width, VALUE height, VALUE title) {
352
- @autoreleasepool {
353
- int w = NUM2INT(width);
354
- int h = NUM2INT(height);
355
- NSString *t = [NSString stringWithUTF8String:StringValueCStr(title)];
356
-
357
- MetacoWindow *window = [[MetacoWindow alloc] initWithWidth:w height:h title:t];
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 VALUE cocoa_window_destroy(VALUE self, VALUE window_ptr) {
363
- void *ptr = (void *)NUM2ULONG(window_ptr);
364
- if (!ptr) return Qnil;
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
- // Get window reference without transferring ownership yet
367
- MetacoWindow *window = (__bridge MetacoWindow *)ptr;
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
- // Synchronize GPU before cleanup (outside autoreleasepool)
370
- if (window && window.useMetal && window.metalView) {
371
- MetacoMetalView *metalView = window.metalView;
372
- if (metalView.commandQueue) {
373
- @try {
374
- id<MTLCommandBuffer> syncBuffer = [metalView.commandQueue commandBuffer];
375
- if (syncBuffer) {
376
- [syncBuffer commit];
377
- [syncBuffer waitUntilCompleted];
378
- }
379
- } @catch (NSException *e) {
380
- // Ignore - GPU may already be done
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
- // Close the window
388
- if (window) {
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
- // Now transfer ownership to ARC - this will release the object
393
- // Do this outside autoreleasepool to avoid double-release issues
394
- (void)(__bridge_transfer id)ptr;
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 cocoa_set_pixels(VALUE self, VALUE window_ptr, VALUE buffer, VALUE width, VALUE height) {
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
- MetacoWindow *window = (__bridge MetacoWindow *)(void *)NUM2ULONG(window_ptr);
402
- int w = NUM2INT(width);
403
- int h = NUM2INT(height);
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
- Check_Type(buffer, T_STRING);
406
- const unsigned char *data = (const unsigned char *)RSTRING_PTR(buffer);
407
- long len = RSTRING_LEN(buffer);
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
- if (len < w * h * 4) {
410
- rb_raise(rb_eArgError, "Buffer too small");
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:data];
737
+ [window.metalView updatePixels:(const uint8_t *)RSTRING_PTR(buffer)];
415
738
  } else {
416
- unsigned char *bitmapData = [window.bitmapRep bitmapData];
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 VALUE cocoa_present(VALUE self, VALUE window_ptr) {
747
+ static void *submit_command(void *ptr) {
424
748
  @autoreleasepool {
425
- MetacoWindow *window = (__bridge MetacoWindow *)(void *)NUM2ULONG(window_ptr);
749
+ id<MTLCommandBuffer> command = (__bridge id<MTLCommandBuffer>)ptr;
750
+ [command commit];
751
+ [command waitUntilCompleted];
752
+ }
753
+ return NULL;
754
+ }
426
755
 
427
- if (window.useMetal) {
428
- [window.metalView present];
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 cocoa_poll_events(VALUE self, VALUE window_ptr) {
438
- @autoreleasepool {
439
- MetacoWindow *window = (__bridge MetacoWindow *)(void *)NUM2ULONG(window_ptr);
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
- VALUE events = rb_ary_new();
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 window_ptr) {
487
- MetacoWindow *window = (__bridge MetacoWindow *)(void *)NUM2ULONG(window_ptr);
488
- return window.shouldClose ? Qtrue : Qfalse;
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
- // ========== Compute Shader Bridge Functions ==========
492
-
493
- static VALUE cocoa_metal_compute_available(VALUE self, VALUE window_ptr) {
494
- MetacoWindow *window = (__bridge MetacoWindow *)(void *)NUM2ULONG(window_ptr);
495
- return (window.useMetal && window.metalView.device) ? Qtrue : Qfalse;
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 window_ptr, VALUE msl_source) {
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 *)(void *)NUM2ULONG(window_ptr);
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
- rb_raise(rb_eRuntimeError, "Metal not available");
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
- Check_Type(msl_source, T_STRING);
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 window_ptr, VALUE uniform_data) {
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 *)(void *)NUM2ULONG(window_ptr);
962
+ MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
963
+ NSError *error = nil;
520
964
  if (!window.useMetal || !window.metalView.hasComputeShader) {
521
- rb_raise(rb_eRuntimeError, "Compute shader not compiled");
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
- Check_Type(uniform_data, T_STRING);
525
- const void *data = RSTRING_PTR(uniform_data);
526
- long length = RSTRING_LEN(uniform_data);
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
- [window.metalView dispatchComputeWithUniforms:data length:length];
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 cocoa_present_compute(VALUE self, VALUE window_ptr) {
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
- MetacoWindow *window = (__bridge MetacoWindow *)(void *)NUM2ULONG(window_ptr);
536
- if (window.useMetal && window.metalView.hasComputeShader) {
537
- [window.metalView presentCompute];
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 cocoa_has_compute_shader(VALUE self, VALUE window_ptr) {
544
- MetacoWindow *window = (__bridge MetacoWindow *)(void *)NUM2ULONG(window_ptr);
545
- return (window.useMetal && window.metalView.hasComputeShader) ? Qtrue : Qfalse;
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, 3);
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
  }