rbgl_cocoa_bridge 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3ab4e46975fed72e539fffd29b860aeeba042a168256a0cb85abde601f79b7fc
4
+ data.tar.gz: ac539f4286672ed86f2d93fe62d6327cd271f4d320b2b0e54eccb7905ec5b06e
5
+ SHA512:
6
+ metadata.gz: aa4c3b42c4234476db6421e3fed2cdbd1c182b9d2257d4a8c321c7247af6c56a6e262beb2b921be7acb9f1aaa996e9bd6a0cdad66075e2c3de5ae93193501db4
7
+ data.tar.gz: 7b18283f5a33fc0ac58e2c247691500e8223a099f5721d3fdcaa3848e1e62dc5549eb611303f7c85b6fd4184a86694127f51aa8bea2643f40021e7a78755562d
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.1.0 - 2026-01-02
6
+
7
+ - Initial release.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # rbgl_cocoa_bridge
2
+
3
+ A Ruby C extension providing native macOS window management and Metal GPU acceleration for graphics applications.
4
+
5
+ ## Requirements
6
+
7
+ - macOS
8
+ - Ruby 3.1+
9
+ - Xcode Command Line Tools
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem "rbgl_cocoa_bridge"
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ ```bash
22
+ bundle install
23
+ ```
24
+
25
+ Or install it yourself as:
26
+
27
+ ```bash
28
+ gem install rbgl_cocoa_bridge
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ### Basic Window
34
+
35
+ ```ruby
36
+ require "rbgl_cocoa_bridge"
37
+
38
+ # Initialize Cocoa
39
+ RBGL::CocoaBridge.init
40
+
41
+ # Create a window
42
+ handle = RBGL::CocoaBridge.window_create(800, 600, "My Window")
43
+
44
+ # Main loop
45
+ until RBGL::CocoaBridge.should_close?(handle)
46
+ # Create pixel buffer (RGBA format)
47
+ width, height = 800, 600
48
+ buffer = "\xFF\x00\x00\xFF" * (width * height) # Red
49
+
50
+ # Update pixels and present
51
+ RBGL::CocoaBridge.set_pixels(handle, buffer, width, height)
52
+ RBGL::CocoaBridge.present(handle)
53
+
54
+ # Handle events
55
+ events = RBGL::CocoaBridge.poll_events(handle)
56
+ events.each do |event|
57
+ case event[:type]
58
+ when :key_press
59
+ puts "Key pressed: #{event[:key]}"
60
+ when :mouse_press
61
+ puts "Mouse clicked at: #{event[:x]}, #{event[:y]}"
62
+ end
63
+ end
64
+ end
65
+
66
+ # Cleanup
67
+ RBGL::CocoaBridge.window_destroy(handle)
68
+ ```
69
+
70
+ ### Metal Compute Shaders
71
+
72
+ ```ruby
73
+ require "rbgl_cocoa_bridge"
74
+
75
+ RBGL::CocoaBridge.init
76
+ handle = RBGL::CocoaBridge.window_create(800, 600, "Compute Shader")
77
+
78
+ # Check Metal availability
79
+ if RBGL::CocoaBridge.metal_compute_available?(handle)
80
+ # Compile shader (MSL)
81
+ shader = <<~MSL
82
+ #include <metal_stdlib>
83
+ using namespace metal;
84
+
85
+ kernel void compute_shader(
86
+ texture2d<float, access::write> output [[texture(0)]],
87
+ constant float4 &uniforms [[buffer(0)]],
88
+ uint2 gid [[thread_position_in_grid]])
89
+ {
90
+ float2 uv = float2(gid) / float2(output.get_width(), output.get_height());
91
+ output.write(float4(uv.x, uv.y, uniforms.x, 1.0), gid);
92
+ }
93
+ MSL
94
+
95
+ RBGL::CocoaBridge.compile_compute_shader(handle, shader)
96
+
97
+ until RBGL::CocoaBridge.should_close?(handle)
98
+ # Dispatch with uniforms
99
+ time = Time.now.to_f
100
+ uniforms = [Math.sin(time) * 0.5 + 0.5, 0.0, 0.0, 1.0].pack("f4")
101
+ RBGL::CocoaBridge.dispatch_compute(handle, uniforms)
102
+ RBGL::CocoaBridge.present_compute(handle)
103
+ RBGL::CocoaBridge.poll_events(handle)
104
+ end
105
+ end
106
+
107
+ RBGL::CocoaBridge.window_destroy(handle)
108
+ ```
109
+
110
+ ## API Reference
111
+
112
+ ### Window Management
113
+
114
+ | Method | Description |
115
+ |--------|-------------|
116
+ | `init` | Initialize Cocoa application |
117
+ | `window_create(width, height, title)` | Create a new window, returns handle |
118
+ | `window_destroy(handle)` | Destroy the window |
119
+ | `should_close?(handle)` | Check if window should close |
120
+ | `poll_events(handle)` | Poll and return pending events |
121
+
122
+ ### Rendering
123
+
124
+ | Method | Description |
125
+ |--------|-------------|
126
+ | `set_pixels(handle, buffer, width, height)` | Set pixel data (RGBA format) |
127
+ | `present(handle)` | Present the frame |
128
+
129
+ ### Compute Shaders
130
+
131
+ | Method | Description |
132
+ |--------|-------------|
133
+ | `metal_compute_available?(handle)` | Check if Metal compute is available |
134
+ | `compile_compute_shader(handle, msl_source)` | Compile MSL compute shader |
135
+ | `dispatch_compute(handle, uniforms)` | Execute compute shader |
136
+ | `present_compute(handle)` | Present compute shader output |
137
+ | `has_compute_shader?(handle)` | Check if shader is compiled |
138
+
139
+ ### Event Types
140
+
141
+ - `:key_press` - Key pressed (`:key`, `:char`)
142
+ - `:key_release` - Key released (`:key`)
143
+ - `:mouse_press` - Mouse button pressed (`:x`, `:y`, `:button`)
144
+ - `:mouse_release` - Mouse button released (`:x`, `:y`, `:button`)
145
+ - `:mouse_move` - Mouse moved (`:x`, `:y`)
146
+
147
+ ## Development
148
+
149
+ ```bash
150
+ # Install dependencies
151
+ bundle install
152
+
153
+ # Compile native extension
154
+ bundle exec rake compile
155
+
156
+ # Run tests
157
+ bundle exec rake test
158
+ ```
159
+
160
+ ## License
161
+
162
+ The gem is available as open source under the terms of the [MIT License](LICENSE).
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/extensiontask"
5
+ require "rake/testtask"
6
+
7
+ Rake::ExtensionTask.new("rbgl_cocoa_bridge") do |ext|
8
+ ext.lib_dir = "lib/rbgl_cocoa_bridge"
9
+ end
10
+
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << "test"
13
+ t.libs << "lib"
14
+ t.test_files = FileList["test/**/test_*.rb"]
15
+ end
16
+
17
+ task test: :compile
18
+ task default: :test
@@ -0,0 +1,6 @@
1
+ require "mkmf"
2
+
3
+ $CFLAGS << " -fobjc-arc"
4
+ $LDFLAGS << " -framework Cocoa -framework Metal -framework QuartzCore"
5
+
6
+ create_makefile("rbgl_cocoa_bridge/rbgl_cocoa_bridge")
@@ -0,0 +1,566 @@
1
+ /*
2
+ * RBGL Cocoa Bridge - Native macOS window support with Metal acceleration
3
+ */
4
+
5
+ #import <Cocoa/Cocoa.h>
6
+ #import <Metal/Metal.h>
7
+ #import <QuartzCore/CAMetalLayer.h>
8
+ #import <ruby.h>
9
+
10
+ @interface RBGLMetalView : NSView
11
+ @property (nonatomic, strong) CAMetalLayer *metalLayer;
12
+ @property (nonatomic, strong) id<MTLDevice> device;
13
+ @property (nonatomic, strong) id<MTLCommandQueue> commandQueue;
14
+ @property (nonatomic, strong) id<MTLTexture> texture;
15
+ @property (nonatomic, assign) int texWidth;
16
+ @property (nonatomic, assign) int texHeight;
17
+ // Compute shader support
18
+ @property (nonatomic, strong) id<MTLComputePipelineState> computePipeline;
19
+ @property (nonatomic, strong) id<MTLBuffer> uniformBuffer;
20
+ @property (nonatomic, strong) id<MTLTexture> outputTexture;
21
+ @property (nonatomic, assign) BOOL hasComputeShader;
22
+ @end
23
+
24
+ @implementation RBGLMetalView
25
+
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
40
+ }
41
+ }
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;
52
+ }
53
+
54
+ // dealloc is handled automatically by ARC - no manual cleanup needed
55
+ // cocoa_window_destroy handles synchronization with GPU before release
56
+
57
+ - (instancetype)initWithFrame:(NSRect)frame device:(id<MTLDevice>)device width:(int)w height:(int)h {
58
+ self = [super initWithFrame:frame];
59
+ if (self) {
60
+ self.device = device;
61
+ self.texWidth = w;
62
+ self.texHeight = h;
63
+
64
+ self.wantsLayer = YES;
65
+ self.metalLayer = [CAMetalLayer layer];
66
+ self.metalLayer.device = device;
67
+ self.metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
68
+ self.metalLayer.framebufferOnly = NO;
69
+ self.metalLayer.displaySyncEnabled = NO; // Disable VSync for max FPS
70
+ self.metalLayer.frame = frame;
71
+ self.metalLayer.drawableSize = CGSizeMake(w, h);
72
+ 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
+ }
84
+ return self;
85
+ }
86
+
87
+ - (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];
90
+ }
91
+
92
+ - (void)present {
93
+ id<CAMetalDrawable> drawable = [self.metalLayer nextDrawable];
94
+ if (!drawable) return;
95
+
96
+ 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
+
113
+ [commandBuffer presentDrawable:drawable];
114
+ [commandBuffer commit];
115
+ }
116
+
117
+ #pragma mark - Compute Shader Support
118
+
119
+ - (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];
124
+ 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:@"RBGL" code:1
130
+ userInfo:@{NSLocalizedDescriptionKey: @"compute_shader function not found"}];
131
+ return NO;
132
+ }
133
+
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;
152
+ return YES;
153
+ }
154
+
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;
176
+ }
177
+
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];
190
+ }
191
+
192
+ - (void)presentCompute {
193
+ if (!self.hasComputeShader) return;
194
+
195
+ id<CAMetalDrawable> drawable = [self.metalLayer nextDrawable];
196
+ if (!drawable) return;
197
+
198
+ 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];
215
+ }
216
+
217
+ @end
218
+
219
+ @interface RBGLWindow : NSWindow
220
+ @property (nonatomic, assign) BOOL shouldClose;
221
+ @property (nonatomic, strong) NSMutableArray *pendingEvents;
222
+ @property (nonatomic, strong) RBGLMetalView *metalView;
223
+ @property (nonatomic, assign) BOOL useMetal;
224
+ // Fallback for non-Metal
225
+ @property (nonatomic, strong) NSBitmapImageRep *bitmapRep;
226
+ @property (nonatomic, strong) NSImageView *imageView;
227
+ @end
228
+
229
+ @implementation RBGLWindow
230
+
231
+ - (instancetype)initWithWidth:(int)width height:(int)height title:(NSString *)title {
232
+ NSRect frame = NSMakeRect(100, 100, width, height);
233
+ self = [super initWithContentRect:frame
234
+ styleMask:(NSWindowStyleMaskTitled |
235
+ NSWindowStyleMaskClosable |
236
+ NSWindowStyleMaskMiniaturizable)
237
+ backing:NSBackingStoreBuffered
238
+ defer:NO];
239
+ if (self) {
240
+ self.shouldClose = NO;
241
+ self.pendingEvents = [NSMutableArray array];
242
+ [self setTitle:title];
243
+ [self setDelegate:(id<NSWindowDelegate>)self];
244
+
245
+ // Try to use Metal
246
+ id<MTLDevice> device = MTLCreateSystemDefaultDevice();
247
+ if (device) {
248
+ self.useMetal = YES;
249
+ self.metalView = [[RBGLMetalView alloc] initWithFrame:NSMakeRect(0, 0, width, height)
250
+ device:device
251
+ width:width
252
+ height:height];
253
+ [self setContentView:self.metalView];
254
+ } else {
255
+ // Fallback to bitmap
256
+ self.useMetal = NO;
257
+ self.bitmapRep = [[NSBitmapImageRep alloc]
258
+ initWithBitmapDataPlanes:NULL
259
+ pixelsWide:width
260
+ pixelsHigh:height
261
+ bitsPerSample:8
262
+ samplesPerPixel:4
263
+ hasAlpha:YES
264
+ isPlanar:NO
265
+ colorSpaceName:NSDeviceRGBColorSpace
266
+ bytesPerRow:width * 4
267
+ bitsPerPixel:32];
268
+
269
+ NSImage *image = [[NSImage alloc] initWithSize:NSMakeSize(width, height)];
270
+ [image addRepresentation:self.bitmapRep];
271
+
272
+ self.imageView = [[NSImageView alloc] initWithFrame:NSMakeRect(0, 0, width, height)];
273
+ [self.imageView setImage:image];
274
+ [self.imageView setImageScaling:NSImageScaleAxesIndependently];
275
+ [self setContentView:self.imageView];
276
+ }
277
+
278
+ [self setAcceptsMouseMovedEvents:YES];
279
+ [self makeKeyAndOrderFront:nil];
280
+ }
281
+ return self;
282
+ }
283
+
284
+ - (BOOL)windowShouldClose:(id)sender {
285
+ self.shouldClose = YES;
286
+ return NO;
287
+ }
288
+
289
+ - (void)keyDown:(NSEvent *)event {
290
+ NSDictionary *eventDict = @{
291
+ @"type": @"key_press",
292
+ @"key": @([event keyCode]),
293
+ @"char": [event characters] ?: @""
294
+ };
295
+ [self.pendingEvents addObject:eventDict];
296
+ }
297
+
298
+ - (void)keyUp:(NSEvent *)event {
299
+ NSDictionary *eventDict = @{
300
+ @"type": @"key_release",
301
+ @"key": @([event keyCode])
302
+ };
303
+ [self.pendingEvents addObject:eventDict];
304
+ }
305
+
306
+ - (void)mouseDown:(NSEvent *)event {
307
+ NSPoint loc = [event locationInWindow];
308
+ NSDictionary *eventDict = @{
309
+ @"type": @"mouse_press",
310
+ @"x": @(loc.x),
311
+ @"y": @(loc.y),
312
+ @"button": @([event buttonNumber])
313
+ };
314
+ [self.pendingEvents addObject:eventDict];
315
+ }
316
+
317
+ - (void)mouseUp:(NSEvent *)event {
318
+ NSPoint loc = [event locationInWindow];
319
+ NSDictionary *eventDict = @{
320
+ @"type": @"mouse_release",
321
+ @"x": @(loc.x),
322
+ @"y": @(loc.y),
323
+ @"button": @([event buttonNumber])
324
+ };
325
+ [self.pendingEvents addObject:eventDict];
326
+ }
327
+
328
+ - (void)mouseMoved:(NSEvent *)event {
329
+ NSPoint loc = [event locationInWindow];
330
+ NSDictionary *eventDict = @{
331
+ @"type": @"mouse_move",
332
+ @"x": @(loc.x),
333
+ @"y": @(loc.y)
334
+ };
335
+ [self.pendingEvents addObject:eventDict];
336
+ }
337
+
338
+ - (BOOL)canBecomeKeyWindow { return YES; }
339
+ - (BOOL)acceptsFirstResponder { return YES; }
340
+ @end
341
+
342
+ static VALUE cocoa_init(VALUE self) {
343
+ @autoreleasepool {
344
+ [NSApplication sharedApplication];
345
+ [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
346
+ [NSApp activateIgnoringOtherApps:YES];
347
+ }
348
+ return Qnil;
349
+ }
350
+
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
+ RBGLWindow *window = [[RBGLWindow alloc] initWithWidth:w height:h title:t];
358
+ return ULONG2NUM((unsigned long)(__bridge_retained void *)window);
359
+ }
360
+ }
361
+
362
+ static VALUE cocoa_window_destroy(VALUE self, VALUE window_ptr) {
363
+ void *ptr = (void *)NUM2ULONG(window_ptr);
364
+ if (!ptr) return Qnil;
365
+
366
+ // Get window reference without transferring ownership yet
367
+ RBGLWindow *window = (__bridge RBGLWindow *)ptr;
368
+
369
+ // Synchronize GPU before cleanup (outside autoreleasepool)
370
+ if (window && window.useMetal && window.metalView) {
371
+ RBGLMetalView *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;
385
+ }
386
+
387
+ // Close the window
388
+ if (window) {
389
+ [window close];
390
+ }
391
+
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;
395
+
396
+ return Qnil;
397
+ }
398
+
399
+ static VALUE cocoa_set_pixels(VALUE self, VALUE window_ptr, VALUE buffer, VALUE width, VALUE height) {
400
+ @autoreleasepool {
401
+ RBGLWindow *window = (__bridge RBGLWindow *)(void *)NUM2ULONG(window_ptr);
402
+ int w = NUM2INT(width);
403
+ int h = NUM2INT(height);
404
+
405
+ Check_Type(buffer, T_STRING);
406
+ const unsigned char *data = (const unsigned char *)RSTRING_PTR(buffer);
407
+ long len = RSTRING_LEN(buffer);
408
+
409
+ if (len < w * h * 4) {
410
+ rb_raise(rb_eArgError, "Buffer too small");
411
+ }
412
+
413
+ if (window.useMetal) {
414
+ [window.metalView updatePixels:data];
415
+ } else {
416
+ unsigned char *bitmapData = [window.bitmapRep bitmapData];
417
+ memcpy(bitmapData, data, w * h * 4);
418
+ }
419
+ }
420
+ return Qnil;
421
+ }
422
+
423
+ static VALUE cocoa_present(VALUE self, VALUE window_ptr) {
424
+ @autoreleasepool {
425
+ RBGLWindow *window = (__bridge RBGLWindow *)(void *)NUM2ULONG(window_ptr);
426
+
427
+ if (window.useMetal) {
428
+ [window.metalView present];
429
+ } else {
430
+ [window.imageView setNeedsDisplay:YES];
431
+ [window displayIfNeeded];
432
+ }
433
+ }
434
+ return Qnil;
435
+ }
436
+
437
+ static VALUE cocoa_poll_events(VALUE self, VALUE window_ptr) {
438
+ @autoreleasepool {
439
+ RBGLWindow *window = (__bridge RBGLWindow *)(void *)NUM2ULONG(window_ptr);
440
+
441
+ NSEvent *event;
442
+ while ((event = [NSApp nextEventMatchingMask:NSEventMaskAny
443
+ untilDate:nil
444
+ inMode:NSDefaultRunLoopMode
445
+ dequeue:YES])) {
446
+ [NSApp sendEvent:event];
447
+ }
448
+
449
+ [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
450
+ beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.001]];
451
+ [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;
483
+ }
484
+ }
485
+
486
+ static VALUE cocoa_should_close(VALUE self, VALUE window_ptr) {
487
+ RBGLWindow *window = (__bridge RBGLWindow *)(void *)NUM2ULONG(window_ptr);
488
+ return window.shouldClose ? Qtrue : Qfalse;
489
+ }
490
+
491
+ // ========== Compute Shader Bridge Functions ==========
492
+
493
+ static VALUE cocoa_metal_compute_available(VALUE self, VALUE window_ptr) {
494
+ RBGLWindow *window = (__bridge RBGLWindow *)(void *)NUM2ULONG(window_ptr);
495
+ return (window.useMetal && window.metalView.device) ? Qtrue : Qfalse;
496
+ }
497
+
498
+ static VALUE cocoa_compile_compute_shader(VALUE self, VALUE window_ptr, VALUE msl_source) {
499
+ @autoreleasepool {
500
+ RBGLWindow *window = (__bridge RBGLWindow *)(void *)NUM2ULONG(window_ptr);
501
+ if (!window.useMetal) {
502
+ rb_raise(rb_eRuntimeError, "Metal not available");
503
+ }
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]);
512
+ }
513
+ }
514
+ return Qtrue;
515
+ }
516
+
517
+ static VALUE cocoa_dispatch_compute(VALUE self, VALUE window_ptr, VALUE uniform_data) {
518
+ @autoreleasepool {
519
+ RBGLWindow *window = (__bridge RBGLWindow *)(void *)NUM2ULONG(window_ptr);
520
+ if (!window.useMetal || !window.metalView.hasComputeShader) {
521
+ rb_raise(rb_eRuntimeError, "Compute shader not compiled");
522
+ }
523
+
524
+ Check_Type(uniform_data, T_STRING);
525
+ const void *data = RSTRING_PTR(uniform_data);
526
+ long length = RSTRING_LEN(uniform_data);
527
+
528
+ [window.metalView dispatchComputeWithUniforms:data length:length];
529
+ }
530
+ return Qnil;
531
+ }
532
+
533
+ static VALUE cocoa_present_compute(VALUE self, VALUE window_ptr) {
534
+ @autoreleasepool {
535
+ RBGLWindow *window = (__bridge RBGLWindow *)(void *)NUM2ULONG(window_ptr);
536
+ if (window.useMetal && window.metalView.hasComputeShader) {
537
+ [window.metalView presentCompute];
538
+ }
539
+ }
540
+ return Qnil;
541
+ }
542
+
543
+ static VALUE cocoa_has_compute_shader(VALUE self, VALUE window_ptr) {
544
+ RBGLWindow *window = (__bridge RBGLWindow *)(void *)NUM2ULONG(window_ptr);
545
+ return (window.useMetal && window.metalView.hasComputeShader) ? Qtrue : Qfalse;
546
+ }
547
+
548
+ void Init_rbgl_cocoa_bridge(void) {
549
+ VALUE mRBGL = rb_define_module("RBGL");
550
+ VALUE mBridge = rb_define_module_under(mRBGL, "CocoaBridge");
551
+
552
+ rb_define_module_function(mBridge, "init", cocoa_init, 0);
553
+ rb_define_module_function(mBridge, "window_create", cocoa_window_create, 3);
554
+ rb_define_module_function(mBridge, "window_destroy", cocoa_window_destroy, 1);
555
+ rb_define_module_function(mBridge, "set_pixels", cocoa_set_pixels, 4);
556
+ rb_define_module_function(mBridge, "present", cocoa_present, 1);
557
+ rb_define_module_function(mBridge, "poll_events", cocoa_poll_events, 1);
558
+ rb_define_module_function(mBridge, "should_close?", cocoa_should_close, 1);
559
+
560
+ // Compute shader functions
561
+ rb_define_module_function(mBridge, "metal_compute_available?", cocoa_metal_compute_available, 1);
562
+ rb_define_module_function(mBridge, "compile_compute_shader", cocoa_compile_compute_shader, 2);
563
+ rb_define_module_function(mBridge, "dispatch_compute", cocoa_dispatch_compute, 2);
564
+ rb_define_module_function(mBridge, "present_compute", cocoa_present_compute, 1);
565
+ rb_define_module_function(mBridge, "has_compute_shader?", cocoa_has_compute_shader, 1);
566
+ }
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module CocoaBridge
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rbgl_cocoa_bridge/version"
4
+
5
+ begin
6
+ require "rbgl_cocoa_bridge/rbgl_cocoa_bridge"
7
+ rescue LoadError
8
+ # Extension not compiled - this is expected on non-macOS platforms
9
+ module RBGL
10
+ module CocoaBridge
11
+ def self.init
12
+ raise LoadError, "Cocoa bridge is only available on macOS"
13
+ end
14
+
15
+ def self.window_create(_w, _h, _t)
16
+ raise LoadError, "Cocoa bridge is only available on macOS"
17
+ end
18
+
19
+ def self.window_destroy(_handle)
20
+ raise LoadError, "Cocoa bridge is only available on macOS"
21
+ end
22
+
23
+ def self.set_pixels(_handle, _data, _w, _h)
24
+ raise LoadError, "Cocoa bridge is only available on macOS"
25
+ end
26
+
27
+ def self.present(_handle)
28
+ raise LoadError, "Cocoa bridge is only available on macOS"
29
+ end
30
+
31
+ def self.poll_events(_handle)
32
+ raise LoadError, "Cocoa bridge is only available on macOS"
33
+ end
34
+
35
+ def self.should_close?(_handle)
36
+ raise LoadError, "Cocoa bridge is only available on macOS"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rbgl_cocoa_bridge/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rbgl_cocoa_bridge"
7
+ spec.version = RBGL::CocoaBridge::VERSION
8
+ spec.authors = ["Yudai Takada"]
9
+ spec.email = ["t.yudai92@gmail.com"]
10
+
11
+ spec.summary = "Native macOS Cocoa/Metal bridge for Ruby graphics applications"
12
+ spec.description = "A Ruby C extension providing native macOS window management and Metal GPU acceleration for graphics applications."
13
+ spec.homepage = "https://github.com/ydah/rbgl_cocoa_bridge"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0.0"
16
+
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
19
+
20
+ spec.files = Dir.chdir(__dir__) do
21
+ `git ls-files -z`.split("\x0").reject do |f|
22
+ (File.expand_path(f) == __FILE__) ||
23
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
24
+ end
25
+ end
26
+ spec.require_paths = ["lib"]
27
+ spec.extensions = ["ext/rbgl_cocoa_bridge/extconf.rb"]
28
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rbgl_cocoa_bridge
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: A Ruby C extension providing native macOS window management and Metal
13
+ GPU acceleration for graphics applications.
14
+ email:
15
+ - t.yudai92@gmail.com
16
+ executables: []
17
+ extensions:
18
+ - ext/rbgl_cocoa_bridge/extconf.rb
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE
23
+ - README.md
24
+ - Rakefile
25
+ - ext/rbgl_cocoa_bridge/extconf.rb
26
+ - ext/rbgl_cocoa_bridge/rbgl_cocoa_bridge.m
27
+ - lib/rbgl_cocoa_bridge.rb
28
+ - lib/rbgl_cocoa_bridge/version.rb
29
+ - rbgl_cocoa_bridge.gemspec
30
+ homepage: https://github.com/ydah/rbgl_cocoa_bridge
31
+ licenses:
32
+ - MIT
33
+ metadata:
34
+ source_code_uri: https://github.com/ydah/rbgl_cocoa_bridge
35
+ changelog_uri: https://github.com/ydah/rbgl_cocoa_bridge/blob/main/CHANGELOG.md
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 3.0.0
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 4.0.3
51
+ specification_version: 4
52
+ summary: Native macOS Cocoa/Metal bridge for Ruby graphics applications
53
+ test_files: []