metaco 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -2
- data/README.md +26 -5
- data/Rakefile +15 -1
- data/ext/metaco/metaco.m +428 -292
- data/lib/metaco/version.rb +1 -1
- data/lib/metaco.rb +22 -3
- metadata +5 -6
- data/metaco.gemspec +0 -28
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56719f087b9757be4e9f9b47df20a2be5e48e30fffe781c130c982a4d5da0593
|
|
4
|
+
data.tar.gz: 9be3b5f19b8d255953964097538d705fbd00ca184b7301ecc51acbeeecad6f49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fb8667d5665d9392cd1d8efec9128211f83be708e82f2a8eebcfe3623fc2deb3dd4785daf8756e286d6edd378cfe316d2898fd7def72e94a777d846cee13a0d1
|
|
7
|
+
data.tar.gz: c83bd6e0bf4164c9af4028e7103219d4005a7bd6df06fab57dc8db365381692fd986ba5a9c8d2d5740fa82b835470df504c1eecd8fb054dc98e0c7dcf3ecb810
|
data/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.2.0 - 2026-09-09
|
|
4
|
+
|
|
5
|
+
- Replace integer window pointers with GC-managed `Metaco::Window` handles. Validate closed handles, main-thread access, dimensions, and pixel buffer capacity.
|
|
6
|
+
- Preserve Ruby exception cleanup, UTF-8/NUL key input, and right/middle mouse and drag events. Complete the unsupported-platform API without hiding macOS load errors.
|
|
7
|
+
- Preserve RGBA colors through a Metal render pass, synchronize GPU work without holding the GVL, propagate GPU errors, and validate all rendering/compute allocations.
|
|
8
|
+
- Keep the previous shader on recompilation failure; reject uniforms over 256 bytes and zero unused bytes. Dispatch exactly the image dimensions within pipeline limits.
|
|
9
|
+
- Add native pixel readback and fault-injection regressions; run GUI tests in macOS CI and support mandatory Metal validation on a GPU runner.
|
|
4
10
|
|
|
5
11
|
## 0.1.0 - 2026-01-02
|
|
6
12
|
|
|
7
|
-
- Initial release.
|
|
13
|
+
- Initial release.
|
data/README.md
CHANGED
|
@@ -87,6 +87,7 @@ if Metaco.metal_compute_available?(handle)
|
|
|
87
87
|
constant float4 &uniforms [[buffer(0)]],
|
|
88
88
|
uint2 gid [[thread_position_in_grid]])
|
|
89
89
|
{
|
|
90
|
+
if (gid.x >= output.get_width() || gid.y >= output.get_height()) return;
|
|
90
91
|
float2 uv = float2(gid) / float2(output.get_width(), output.get_height());
|
|
91
92
|
output.write(float4(uv.x, uv.y, uniforms.x, 1.0), gid);
|
|
92
93
|
}
|
|
@@ -114,8 +115,8 @@ Metaco.window_destroy(handle)
|
|
|
114
115
|
| Method | Description |
|
|
115
116
|
|--------|-------------|
|
|
116
117
|
| `init` | Initialize Cocoa application |
|
|
117
|
-
| `window_create(width, height, title)` | Create a new window, returns handle |
|
|
118
|
-
| `window_destroy(handle)` |
|
|
118
|
+
| `window_create(width, height, title)` | Create a new window, returns a `Metaco::Window` handle |
|
|
119
|
+
| `window_destroy(handle)` | Close and release the window; repeated calls are safe |
|
|
119
120
|
| `should_close?(handle)` | Check if window should close |
|
|
120
121
|
| `poll_events(handle)` | Poll and return pending events |
|
|
121
122
|
|
|
@@ -124,7 +125,7 @@ Metaco.window_destroy(handle)
|
|
|
124
125
|
| Method | Description |
|
|
125
126
|
|--------|-------------|
|
|
126
127
|
| `set_pixels(handle, buffer, width, height)` | Set pixel data (RGBA format) |
|
|
127
|
-
| `present(handle)` | Present the frame |
|
|
128
|
+
| `present(handle)` | Present the frame and wait for GPU completion |
|
|
128
129
|
|
|
129
130
|
### Compute Shaders
|
|
130
131
|
|
|
@@ -132,8 +133,8 @@ Metaco.window_destroy(handle)
|
|
|
132
133
|
|--------|-------------|
|
|
133
134
|
| `metal_compute_available?(handle)` | Check if Metal compute is available |
|
|
134
135
|
| `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 |
|
|
136
|
+
| `dispatch_compute(handle, uniforms)` | Execute compute shader and wait for GPU completion |
|
|
137
|
+
| `present_compute(handle)` | Present compute shader output and wait for GPU completion |
|
|
137
138
|
| `has_compute_shader?(handle)` | Check if shader is compiled |
|
|
138
139
|
|
|
139
140
|
### Event Types
|
|
@@ -144,6 +145,19 @@ Metaco.window_destroy(handle)
|
|
|
144
145
|
- `:mouse_release` - Mouse button released (`:x`, `:y`, `:button`)
|
|
145
146
|
- `:mouse_move` - Mouse moved (`:x`, `:y`)
|
|
146
147
|
|
|
148
|
+
### API contracts
|
|
149
|
+
|
|
150
|
+
- Call `init` before creating windows. All native APIs must run on the process's main thread; worker-thread calls raise `ThreadError`. GPU completion waits release Ruby's GVL.
|
|
151
|
+
- Handles are opaque `Metaco::Window` objects, replacing the integer pointers returned by 0.1.0. Pass them unchanged to Metaco methods. A closed handle raises `ArgumentError` on operations other than `window_destroy`; unrelated objects raise `TypeError`.
|
|
152
|
+
- Release windows in an `ensure` block. GC also releases abandoned windows, scheduling AppKit cleanup on the main thread when necessary; `poll_events` services this queue.
|
|
153
|
+
- Width and height must be between 1 and 16,384. `set_pixels` requires the original window dimensions and at least `width * height * 4` bytes of row-major RGBA data, with unpremultiplied alpha. Extra trailing bytes are ignored. Invalid dimensions or short buffers raise `ArgumentError`.
|
|
154
|
+
- Titles and shader sources must contain valid UTF-8. Key event `:char` strings use UTF-8 and preserve embedded NUL characters. Mouse buttons are numbered 0 (left), 1 (right), and 2 (middle); dragging produces `:mouse_move` events.
|
|
155
|
+
- Compute shaders use the entry point `compute_shader`, output texture 0, and a uniform buffer at index 0. Uniforms may contain 0–256 bytes; remaining bytes are zeroed on every dispatch. Larger inputs raise `ArgumentError`. Shader uniform structures must fit within 256 bytes.
|
|
156
|
+
- Failed compilation preserves the previous shader and its resources. Dispatch and compute presentation require a compiled shader. GPU command failures raise `RuntimeError` after native resources have been cleaned up.
|
|
157
|
+
- Dispatch covers exactly the window's pixels. Threadgroup dimensions may vary by pipeline and image width; shaders must not assume a fixed group size. The example's bounds check also makes it safe with other dispatch implementations.
|
|
158
|
+
- Presentation is synchronous so pixel uploads cannot overwrite an in-flight frame. An occluded window may have no drawable, in which case presentation is skipped. Rendering resource allocation failures select the bitmap fallback; compute availability then returns `false`.
|
|
159
|
+
- On unsupported platforms both compute availability queries return `false`, and other APIs raise `LoadError`. On macOS, native extension loading errors retain their original diagnostics.
|
|
160
|
+
|
|
147
161
|
## Development
|
|
148
162
|
|
|
149
163
|
```bash
|
|
@@ -155,8 +169,15 @@ bundle exec rake compile
|
|
|
155
169
|
|
|
156
170
|
# Run tests
|
|
157
171
|
bundle exec rake test
|
|
172
|
+
|
|
173
|
+
# Require actual GPU execution and validate Metal commands/shaders
|
|
174
|
+
METACO_REQUIRE_METAL=1 MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1 bundle exec rake test
|
|
158
175
|
```
|
|
159
176
|
|
|
177
|
+
The test task includes a separate native test extension for GPU pixel readback, allocation and command failure injection, Unicode input, and GC/exception cleanup. These helpers are excluded from the gem. CI runs window and bitmap tests on macOS even when Metal is unavailable; only GPU-specific tests are omitted in that case. Set `METACO_SKIP_GUI=1` explicitly for a session without WindowServer. Argument and fallback tests still run.
|
|
178
|
+
|
|
179
|
+
The optional `run_metal` workflow-dispatch input runs the same suite with mandatory GPU execution on a self-hosted runner labeled `macOS` and `metal`. It requires a logged-in GUI session and a Metal device.
|
|
180
|
+
|
|
160
181
|
## License
|
|
161
182
|
|
|
162
183
|
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
data/Rakefile
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
4
|
require "rake/extensiontask"
|
|
5
5
|
require "rake/testtask"
|
|
6
|
+
require "rbconfig"
|
|
6
7
|
|
|
7
8
|
Rake::ExtensionTask.new("metaco") do |ext|
|
|
8
9
|
ext.lib_dir = "lib/metaco"
|
|
@@ -14,5 +15,18 @@ Rake::TestTask.new(:test) do |t|
|
|
|
14
15
|
t.test_files = FileList["test/**/test_*.rb"]
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
desc "Run native regression tests with GPU readback and fault injection"
|
|
19
|
+
task :test_native do
|
|
20
|
+
next unless RUBY_PLATFORM.include?("darwin")
|
|
21
|
+
|
|
22
|
+
build_dir = File.expand_path("tmp/native/#{RUBY_PLATFORM}/#{RUBY_VERSION}", __dir__)
|
|
23
|
+
mkdir_p build_dir
|
|
24
|
+
Dir.chdir(build_dir) do
|
|
25
|
+
ruby File.expand_path("test/native/extconf.rb", __dir__)
|
|
26
|
+
sh RbConfig::CONFIG.fetch("MAKE", "make")
|
|
27
|
+
end
|
|
28
|
+
ruby "-I#{build_dir}", "test/native/regressions.rb"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
task test: [:compile, :test_native]
|
|
18
32
|
task default: :test
|
data/ext/metaco/metaco.m
CHANGED
|
@@ -6,53 +6,33 @@
|
|
|
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 <pthread.h>
|
|
12
|
+
#include <stdint.h>
|
|
13
|
+
|
|
14
|
+
static void native_error(NSError **error, NSString *message) {
|
|
15
|
+
if (error) *error = [NSError errorWithDomain:@"Metaco" code:1
|
|
16
|
+
userInfo:@{NSLocalizedDescriptionKey: message}];
|
|
17
|
+
}
|
|
9
18
|
|
|
10
19
|
@interface MetacoMetalView : NSView
|
|
11
20
|
@property (nonatomic, strong) CAMetalLayer *metalLayer;
|
|
12
21
|
@property (nonatomic, strong) id<MTLDevice> device;
|
|
13
22
|
@property (nonatomic, strong) id<MTLCommandQueue> commandQueue;
|
|
14
23
|
@property (nonatomic, strong) id<MTLTexture> texture;
|
|
24
|
+
@property (nonatomic, strong) id<MTLRenderPipelineState> renderPipeline;
|
|
15
25
|
@property (nonatomic, assign) int texWidth;
|
|
16
26
|
@property (nonatomic, assign) int texHeight;
|
|
17
|
-
// Compute shader support
|
|
18
27
|
@property (nonatomic, strong) id<MTLComputePipelineState> computePipeline;
|
|
19
28
|
@property (nonatomic, strong) id<MTLBuffer> uniformBuffer;
|
|
20
29
|
@property (nonatomic, strong) id<MTLTexture> outputTexture;
|
|
21
|
-
@property (nonatomic,
|
|
30
|
+
@property (nonatomic, readonly) BOOL hasComputeShader;
|
|
22
31
|
@end
|
|
23
32
|
|
|
24
33
|
@implementation MetacoMetalView
|
|
25
34
|
|
|
26
|
-
- (
|
|
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
|
|
35
|
+
- (BOOL)hasComputeShader { return self.computePipeline != nil; }
|
|
56
36
|
|
|
57
37
|
- (instancetype)initWithFrame:(NSRect)frame device:(id<MTLDevice>)device width:(int)w height:(int)h {
|
|
58
38
|
self = [super initWithFrame:frame];
|
|
@@ -60,163 +40,155 @@
|
|
|
60
40
|
self.device = device;
|
|
61
41
|
self.texWidth = w;
|
|
62
42
|
self.texHeight = h;
|
|
43
|
+
self.commandQueue = [device newCommandQueue];
|
|
44
|
+
if (!self.commandQueue) return nil;
|
|
45
|
+
|
|
46
|
+
// A render pass preserves RGBA channel meaning when writing a BGRA drawable.
|
|
47
|
+
NSString *source = @"#include <metal_stdlib>\n"
|
|
48
|
+
"using namespace metal;\n"
|
|
49
|
+
"vertex float4 metaco_vertex(uint i [[vertex_id]]) {\n"
|
|
50
|
+
" const float2 p[] = {float2(-1,-1), float2(3,-1), float2(-1,3)};\n"
|
|
51
|
+
" return float4(p[i], 0, 1);\n"
|
|
52
|
+
"}\n"
|
|
53
|
+
"fragment float4 metaco_fragment(float4 p [[position]],\n"
|
|
54
|
+
" texture2d<float> input [[texture(0)]]) { return input.read(uint2(p.xy)); }\n";
|
|
55
|
+
id<MTLLibrary> library = [device newLibraryWithSource:source options:nil error:nil];
|
|
56
|
+
if (!library) return nil;
|
|
57
|
+
MTLRenderPipelineDescriptor *pipeline = [MTLRenderPipelineDescriptor new];
|
|
58
|
+
pipeline.vertexFunction = [library newFunctionWithName:@"metaco_vertex"];
|
|
59
|
+
pipeline.fragmentFunction = [library newFunctionWithName:@"metaco_fragment"];
|
|
60
|
+
pipeline.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
|
|
61
|
+
if (!pipeline.vertexFunction || !pipeline.fragmentFunction) return nil;
|
|
62
|
+
self.renderPipeline = [device newRenderPipelineStateWithDescriptor:pipeline error:nil];
|
|
63
|
+
if (!self.renderPipeline) return nil;
|
|
64
|
+
|
|
65
|
+
MTLTextureDescriptor *desc = [MTLTextureDescriptor
|
|
66
|
+
texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:w height:h mipmapped:NO];
|
|
67
|
+
desc.usage = MTLTextureUsageShaderRead;
|
|
68
|
+
desc.storageMode = MTLStorageModeManaged;
|
|
69
|
+
if (@available(macOS 10.15, *)) {
|
|
70
|
+
if (device.hasUnifiedMemory) desc.storageMode = MTLStorageModeShared;
|
|
71
|
+
}
|
|
72
|
+
self.texture = [device newTextureWithDescriptor:desc];
|
|
73
|
+
if (!self.texture) return nil;
|
|
63
74
|
|
|
64
75
|
self.wantsLayer = YES;
|
|
65
76
|
self.metalLayer = [CAMetalLayer layer];
|
|
77
|
+
if (!self.metalLayer) return nil;
|
|
66
78
|
self.metalLayer.device = device;
|
|
67
79
|
self.metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
|
|
68
|
-
self.metalLayer.framebufferOnly =
|
|
69
|
-
self.metalLayer.displaySyncEnabled = NO;
|
|
80
|
+
self.metalLayer.framebufferOnly = YES;
|
|
81
|
+
self.metalLayer.displaySyncEnabled = NO;
|
|
70
82
|
self.metalLayer.frame = frame;
|
|
71
83
|
self.metalLayer.drawableSize = CGSizeMake(w, h);
|
|
72
84
|
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
85
|
}
|
|
84
86
|
return self;
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
- (void)updatePixels:(const uint8_t *)data {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
[self.texture replaceRegion:MTLRegionMake2D(0, 0, self.texWidth, self.texHeight)
|
|
91
|
+
mipmapLevel:0 withBytes:data bytesPerRow:(size_t)self.texWidth * 4];
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
- (
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
- (BOOL)renderTexture:(id<MTLTexture>)source toTexture:(id<MTLTexture>)target
|
|
95
|
+
commandBuffer:(id<MTLCommandBuffer>)commandBuffer error:(NSError **)error {
|
|
96
|
+
MTLRenderPassDescriptor *pass = [MTLRenderPassDescriptor renderPassDescriptor];
|
|
97
|
+
pass.colorAttachments[0].texture = target;
|
|
98
|
+
pass.colorAttachments[0].loadAction = MTLLoadActionDontCare;
|
|
99
|
+
pass.colorAttachments[0].storeAction = MTLStoreActionStore;
|
|
100
|
+
id<MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor:pass];
|
|
101
|
+
if (!encoder) {
|
|
102
|
+
native_error(error, @"Failed to create render encoder");
|
|
103
|
+
return NO;
|
|
104
|
+
}
|
|
105
|
+
[encoder setRenderPipelineState:self.renderPipeline];
|
|
106
|
+
[encoder setFragmentTexture:source atIndex:0];
|
|
107
|
+
[encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
|
|
108
|
+
[encoder endEncoding];
|
|
109
|
+
return YES;
|
|
110
|
+
}
|
|
95
111
|
|
|
112
|
+
- (id<MTLCommandBuffer>)presentTexture:(id<MTLTexture>)texture error:(NSError **)error {
|
|
113
|
+
id<CAMetalDrawable> drawable = [self.metalLayer nextDrawable];
|
|
114
|
+
if (!drawable) return nil; // An occluded window can temporarily have no drawable.
|
|
96
115
|
id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
[
|
|
102
|
-
sourceSlice:0
|
|
103
|
-
sourceLevel:0
|
|
104
|
-
sourceOrigin:MTLOriginMake(0, 0, 0)
|
|
105
|
-
sourceSize:MTLSizeMake(self.texWidth, self.texHeight, 1)
|
|
106
|
-
toTexture:drawable.texture
|
|
107
|
-
destinationSlice:0
|
|
108
|
-
destinationLevel:0
|
|
109
|
-
destinationOrigin:MTLOriginMake(0, 0, 0)];
|
|
110
|
-
|
|
111
|
-
[blitEncoder endEncoding];
|
|
112
|
-
|
|
116
|
+
if (!commandBuffer) {
|
|
117
|
+
native_error(error, @"Failed to create render command buffer");
|
|
118
|
+
return nil;
|
|
119
|
+
}
|
|
120
|
+
if (![self renderTexture:texture toTexture:drawable.texture commandBuffer:commandBuffer error:error]) return nil;
|
|
113
121
|
[commandBuffer presentDrawable:drawable];
|
|
114
|
-
|
|
122
|
+
return commandBuffer;
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
#pragma mark - Compute Shader Support
|
|
118
|
-
|
|
119
125
|
- (BOOL)compileComputeShader:(NSString *)mslSource error:(NSError **)error {
|
|
120
|
-
|
|
121
|
-
id<MTLLibrary> library = [self.device newLibraryWithSource:mslSource
|
|
122
|
-
options:nil
|
|
123
|
-
error:error];
|
|
126
|
+
id<MTLLibrary> library = [self.device newLibraryWithSource:mslSource options:nil error:error];
|
|
124
127
|
if (!library) return NO;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (!computeFunc) {
|
|
129
|
-
if (error) *error = [NSError errorWithDomain:@"Metaco" code:1
|
|
130
|
-
userInfo:@{NSLocalizedDescriptionKey: @"compute_shader function not found"}];
|
|
128
|
+
id<MTLFunction> function = [library newFunctionWithName:@"compute_shader"];
|
|
129
|
+
if (!function) {
|
|
130
|
+
native_error(error, @"compute_shader function not found");
|
|
131
131
|
return NO;
|
|
132
132
|
}
|
|
133
|
+
id<MTLComputePipelineState> pipeline = [self.device newComputePipelineStateWithFunction:function error:error];
|
|
134
|
+
if (!pipeline) return NO;
|
|
133
135
|
|
|
134
|
-
|
|
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
|
|
136
|
+
MTLTextureDescriptor *desc = [MTLTextureDescriptor
|
|
140
137
|
texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
|
|
141
|
-
width:self.texWidth
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
138
|
+
width:self.texWidth height:self.texHeight mipmapped:NO];
|
|
139
|
+
desc.usage = MTLTextureUsageShaderWrite | MTLTextureUsageShaderRead;
|
|
140
|
+
desc.storageMode = MTLStorageModePrivate;
|
|
141
|
+
id<MTLTexture> output = [self.device newTextureWithDescriptor:desc];
|
|
142
|
+
id<MTLBuffer> uniforms = [self.device newBufferWithLength:256 options:MTLResourceStorageModeShared];
|
|
143
|
+
if (!output || !uniforms || !uniforms.contents) {
|
|
144
|
+
native_error(error, @"Failed to allocate compute resources");
|
|
145
|
+
return NO;
|
|
146
|
+
}
|
|
150
147
|
|
|
151
|
-
|
|
148
|
+
// Publish the new state only after every resource has been created.
|
|
149
|
+
self.computePipeline = pipeline;
|
|
150
|
+
self.outputTexture = output;
|
|
151
|
+
self.uniformBuffer = uniforms;
|
|
152
152
|
return YES;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
- (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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;
|
|
155
|
+
- (id<MTLCommandBuffer>)dispatchComputeWithUniforms:(const void *)data length:(NSUInteger)length
|
|
156
|
+
error:(NSError **)error {
|
|
157
|
+
if (!self.hasComputeShader || length > 256) {
|
|
158
|
+
native_error(error, @"Invalid compute state or uniform size");
|
|
159
|
+
return nil;
|
|
176
160
|
}
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
161
|
+
memset(self.uniformBuffer.contents, 0, 256);
|
|
162
|
+
memcpy(self.uniformBuffer.contents, data, length);
|
|
198
163
|
id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
|
|
199
|
-
id<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
164
|
+
id<MTLComputeCommandEncoder> encoder = [commandBuffer computeCommandEncoder];
|
|
165
|
+
if (!commandBuffer || !encoder) {
|
|
166
|
+
native_error(error, @"Failed to create compute command buffer or encoder");
|
|
167
|
+
return nil;
|
|
168
|
+
}
|
|
169
|
+
[encoder setComputePipelineState:self.computePipeline];
|
|
170
|
+
[encoder setTexture:self.outputTexture atIndex:0];
|
|
171
|
+
[encoder setBuffer:self.uniformBuffer offset:0 atIndex:0];
|
|
172
|
+
|
|
173
|
+
NSUInteger limit = MIN(self.computePipeline.maxTotalThreadsPerThreadgroup,
|
|
174
|
+
self.device.maxThreadsPerThreadgroup.width);
|
|
175
|
+
if (!limit) {
|
|
176
|
+
[encoder endEncoding];
|
|
177
|
+
native_error(error, @"Compute pipeline has no supported threadgroup size");
|
|
178
|
+
return nil;
|
|
179
|
+
}
|
|
180
|
+
NSUInteger width = MAX((NSUInteger)1, MIN(self.computePipeline.threadExecutionWidth, limit));
|
|
181
|
+
// Use complete rows that divide the image, so even older GPUs dispatch no extra pixels.
|
|
182
|
+
while ((NSUInteger)self.texWidth % width != 0) width--;
|
|
183
|
+
[encoder dispatchThreadgroups:MTLSizeMake(self.texWidth / width, self.texHeight, 1)
|
|
184
|
+
threadsPerThreadgroup:MTLSizeMake(width, 1, 1)];
|
|
185
|
+
[encoder endEncoding];
|
|
186
|
+
return commandBuffer;
|
|
215
187
|
}
|
|
216
188
|
|
|
217
189
|
@end
|
|
218
190
|
|
|
219
|
-
@interface MetacoWindow : NSWindow
|
|
191
|
+
@interface MetacoWindow : NSWindow <NSWindowDelegate>
|
|
220
192
|
@property (nonatomic, assign) BOOL shouldClose;
|
|
221
193
|
@property (nonatomic, strong) NSMutableArray *pendingEvents;
|
|
222
194
|
@property (nonatomic, strong) MetacoMetalView *metalView;
|
|
@@ -237,23 +209,25 @@
|
|
|
237
209
|
backing:NSBackingStoreBuffered
|
|
238
210
|
defer:NO];
|
|
239
211
|
if (self) {
|
|
212
|
+
self.releasedWhenClosed = NO;
|
|
240
213
|
self.shouldClose = NO;
|
|
241
214
|
self.pendingEvents = [NSMutableArray array];
|
|
242
215
|
[self setTitle:title];
|
|
243
|
-
[self setDelegate:
|
|
216
|
+
[self setDelegate:self];
|
|
244
217
|
|
|
245
218
|
// Try to use Metal
|
|
246
219
|
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
|
|
247
220
|
if (device) {
|
|
248
|
-
self.useMetal = YES;
|
|
249
221
|
self.metalView = [[MetacoMetalView alloc] initWithFrame:NSMakeRect(0, 0, width, height)
|
|
250
222
|
device:device
|
|
251
223
|
width:width
|
|
252
224
|
height:height];
|
|
225
|
+
}
|
|
226
|
+
self.useMetal = self.metalView != nil;
|
|
227
|
+
if (self.useMetal) {
|
|
253
228
|
[self setContentView:self.metalView];
|
|
254
229
|
} else {
|
|
255
|
-
//
|
|
256
|
-
self.useMetal = NO;
|
|
230
|
+
// Device or rendering resource creation failed: use the bitmap path.
|
|
257
231
|
self.bitmapRep = [[NSBitmapImageRep alloc]
|
|
258
232
|
initWithBitmapDataPlanes:NULL
|
|
259
233
|
pixelsWide:width
|
|
@@ -263,13 +237,22 @@
|
|
|
263
237
|
hasAlpha:YES
|
|
264
238
|
isPlanar:NO
|
|
265
239
|
colorSpaceName:NSDeviceRGBColorSpace
|
|
266
|
-
|
|
240
|
+
bitmapFormat:NSBitmapFormatAlphaNonpremultiplied
|
|
241
|
+
bytesPerRow:(size_t)width * 4
|
|
267
242
|
bitsPerPixel:32];
|
|
268
243
|
|
|
244
|
+
if (!self.bitmapRep || !self.bitmapRep.bitmapData) {
|
|
245
|
+
[self close];
|
|
246
|
+
return nil;
|
|
247
|
+
}
|
|
269
248
|
NSImage *image = [[NSImage alloc] initWithSize:NSMakeSize(width, height)];
|
|
270
249
|
[image addRepresentation:self.bitmapRep];
|
|
271
250
|
|
|
272
251
|
self.imageView = [[NSImageView alloc] initWithFrame:NSMakeRect(0, 0, width, height)];
|
|
252
|
+
if (!image || !self.imageView) {
|
|
253
|
+
[self close];
|
|
254
|
+
return nil;
|
|
255
|
+
}
|
|
273
256
|
[self.imageView setImage:image];
|
|
274
257
|
[self.imageView setImageScaling:NSImageScaleAxesIndependently];
|
|
275
258
|
[self setContentView:self.imageView];
|
|
@@ -335,11 +318,79 @@
|
|
|
335
318
|
[self.pendingEvents addObject:eventDict];
|
|
336
319
|
}
|
|
337
320
|
|
|
321
|
+
- (void)rightMouseDown:(NSEvent *)event { [self mouseDown:event]; }
|
|
322
|
+
- (void)rightMouseUp:(NSEvent *)event { [self mouseUp:event]; }
|
|
323
|
+
- (void)otherMouseDown:(NSEvent *)event { [self mouseDown:event]; }
|
|
324
|
+
- (void)otherMouseUp:(NSEvent *)event { [self mouseUp:event]; }
|
|
325
|
+
- (void)mouseDragged:(NSEvent *)event { [self mouseMoved:event]; }
|
|
326
|
+
- (void)rightMouseDragged:(NSEvent *)event { [self mouseMoved:event]; }
|
|
327
|
+
- (void)otherMouseDragged:(NSEvent *)event { [self mouseMoved:event]; }
|
|
328
|
+
|
|
338
329
|
- (BOOL)canBecomeKeyWindow { return YES; }
|
|
339
330
|
- (BOOL)acceptsFirstResponder { return YES; }
|
|
340
331
|
@end
|
|
341
332
|
|
|
333
|
+
typedef struct {
|
|
334
|
+
void *window;
|
|
335
|
+
int width;
|
|
336
|
+
int height;
|
|
337
|
+
size_t byte_length;
|
|
338
|
+
BOOL busy;
|
|
339
|
+
} MetacoHandle;
|
|
340
|
+
|
|
341
|
+
static VALUE cMetacoWindow;
|
|
342
|
+
|
|
343
|
+
static void require_main_thread(void) {
|
|
344
|
+
if (!pthread_main_np()) rb_raise(rb_eThreadError, "Metaco must be called on the main thread");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
static size_t pixel_byte_length(int width, int height) {
|
|
348
|
+
if (width <= 0 || height <= 0 || width > 16384 || height > 16384) {
|
|
349
|
+
rb_raise(rb_eArgError, "Width and height must be between 1 and 16384");
|
|
350
|
+
}
|
|
351
|
+
if ((size_t)width > SIZE_MAX / 4 / (size_t)height ||
|
|
352
|
+
(size_t)width * 4 * (size_t)height > LONG_MAX) {
|
|
353
|
+
rb_raise(rb_eArgError, "Pixel buffer size is too large");
|
|
354
|
+
}
|
|
355
|
+
return (size_t)width * (size_t)height * 4;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
static void release_window(void *ptr) {
|
|
359
|
+
@autoreleasepool {
|
|
360
|
+
MetacoWindow *window = (__bridge_transfer MetacoWindow *)ptr;
|
|
361
|
+
window.delegate = nil;
|
|
362
|
+
[window close];
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
static void handle_free(void *ptr) {
|
|
367
|
+
MetacoHandle *handle = ptr;
|
|
368
|
+
if (handle->window) {
|
|
369
|
+
if (pthread_main_np()) release_window(handle->window);
|
|
370
|
+
else dispatch_async_f(dispatch_get_main_queue(), handle->window, release_window);
|
|
371
|
+
}
|
|
372
|
+
ruby_xfree(handle);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
static size_t handle_size(const void *ptr) { return sizeof(MetacoHandle); }
|
|
376
|
+
|
|
377
|
+
static const rb_data_type_t handle_type = {
|
|
378
|
+
.wrap_struct_name = "Metaco::Window",
|
|
379
|
+
.function = {.dfree = handle_free, .dsize = handle_size},
|
|
380
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
static MetacoHandle *get_handle(VALUE value, BOOL allow_closed) {
|
|
384
|
+
require_main_thread();
|
|
385
|
+
MetacoHandle *handle;
|
|
386
|
+
TypedData_Get_Struct(value, MetacoHandle, &handle_type, handle);
|
|
387
|
+
if (!allow_closed && !handle->window) rb_raise(rb_eArgError, "Window is closed");
|
|
388
|
+
if (handle->busy) rb_raise(rb_eRuntimeError, "Window is busy");
|
|
389
|
+
return handle;
|
|
390
|
+
}
|
|
391
|
+
|
|
342
392
|
static VALUE cocoa_init(VALUE self) {
|
|
393
|
+
require_main_thread();
|
|
343
394
|
@autoreleasepool {
|
|
344
395
|
[NSApplication sharedApplication];
|
|
345
396
|
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
|
@@ -349,95 +400,171 @@ static VALUE cocoa_init(VALUE self) {
|
|
|
349
400
|
}
|
|
350
401
|
|
|
351
402
|
static VALUE cocoa_window_create(VALUE self, VALUE width, VALUE height, VALUE title) {
|
|
403
|
+
require_main_thread();
|
|
404
|
+
int w = NUM2INT(width);
|
|
405
|
+
int h = NUM2INT(height);
|
|
406
|
+
size_t length = pixel_byte_length(w, h);
|
|
407
|
+
Check_Type(title, T_STRING);
|
|
408
|
+
if (!NSApp) rb_raise(rb_eRuntimeError, "Call Metaco.init before creating a window");
|
|
409
|
+
|
|
410
|
+
MetacoHandle *handle;
|
|
411
|
+
VALUE result = TypedData_Make_Struct(cMetacoWindow, MetacoHandle, &handle_type, handle);
|
|
412
|
+
handle->width = w;
|
|
413
|
+
handle->height = h;
|
|
414
|
+
handle->byte_length = length;
|
|
415
|
+
BOOL valid_title;
|
|
352
416
|
@autoreleasepool {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
MetacoWindow *window = (__bridge MetacoWindow *)ptr;
|
|
368
|
-
|
|
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
|
-
}
|
|
417
|
+
NSString *text = [[NSString alloc] initWithBytes:RSTRING_PTR(title)
|
|
418
|
+
length:RSTRING_LEN(title)
|
|
419
|
+
encoding:NSUTF8StringEncoding];
|
|
420
|
+
valid_title = text != nil;
|
|
421
|
+
if (valid_title) {
|
|
422
|
+
MetacoWindow *window = [[MetacoWindow alloc] initWithWidth:w height:h title:text];
|
|
423
|
+
handle->window = (__bridge_retained void *)window;
|
|
382
424
|
}
|
|
383
|
-
// Mark that we've cleaned up the compute shader
|
|
384
|
-
metalView.hasComputeShader = NO;
|
|
385
425
|
}
|
|
426
|
+
RB_GC_GUARD(title);
|
|
427
|
+
if (!valid_title) rb_raise(rb_eArgError, "Title must contain valid UTF-8");
|
|
428
|
+
if (!handle->window) rb_raise(rb_eRuntimeError, "Failed to create window resources");
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
386
431
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
// Do this outside autoreleasepool to avoid double-release issues
|
|
394
|
-
(void)(__bridge_transfer id)ptr;
|
|
395
|
-
|
|
432
|
+
static VALUE cocoa_window_destroy(VALUE self, VALUE value) {
|
|
433
|
+
MetacoHandle *handle = get_handle(value, YES);
|
|
434
|
+
void *ptr = handle->window;
|
|
435
|
+
handle->window = NULL;
|
|
436
|
+
if (ptr) release_window(ptr);
|
|
437
|
+
RB_GC_GUARD(value);
|
|
396
438
|
return Qnil;
|
|
397
439
|
}
|
|
398
440
|
|
|
399
|
-
static VALUE cocoa_set_pixels(VALUE self, VALUE
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
rb_raise(rb_eArgError, "Buffer too small");
|
|
411
|
-
}
|
|
441
|
+
static VALUE cocoa_set_pixels(VALUE self, VALUE value, VALUE buffer, VALUE width, VALUE height) {
|
|
442
|
+
require_main_thread();
|
|
443
|
+
int w = NUM2INT(width);
|
|
444
|
+
int h = NUM2INT(height);
|
|
445
|
+
size_t length = pixel_byte_length(w, h);
|
|
446
|
+
Check_Type(buffer, T_STRING);
|
|
447
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
448
|
+
if (w != handle->width || h != handle->height) {
|
|
449
|
+
rb_raise(rb_eArgError, "Pixel dimensions must match the window");
|
|
450
|
+
}
|
|
451
|
+
if ((size_t)RSTRING_LEN(buffer) < length) rb_raise(rb_eArgError, "Buffer too small");
|
|
412
452
|
|
|
453
|
+
@autoreleasepool {
|
|
454
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
413
455
|
if (window.useMetal) {
|
|
414
|
-
[window.metalView updatePixels:
|
|
456
|
+
[window.metalView updatePixels:(const uint8_t *)RSTRING_PTR(buffer)];
|
|
415
457
|
} else {
|
|
416
|
-
|
|
417
|
-
memcpy(bitmapData, data, w * h * 4);
|
|
458
|
+
memcpy(window.bitmapRep.bitmapData, RSTRING_PTR(buffer), handle->byte_length);
|
|
418
459
|
}
|
|
419
460
|
}
|
|
461
|
+
RB_GC_GUARD(buffer);
|
|
462
|
+
RB_GC_GUARD(value);
|
|
420
463
|
return Qnil;
|
|
421
464
|
}
|
|
422
465
|
|
|
423
|
-
static
|
|
466
|
+
static void *submit_command(void *ptr) {
|
|
424
467
|
@autoreleasepool {
|
|
425
|
-
|
|
468
|
+
id<MTLCommandBuffer> command = (__bridge id<MTLCommandBuffer>)ptr;
|
|
469
|
+
[command commit];
|
|
470
|
+
[command waitUntilCompleted];
|
|
471
|
+
}
|
|
472
|
+
return NULL;
|
|
473
|
+
}
|
|
426
474
|
|
|
427
|
-
|
|
428
|
-
|
|
475
|
+
static VALUE submit_without_gvl(VALUE ptr) {
|
|
476
|
+
rb_thread_call_without_gvl(submit_command, (void *)ptr, NULL, NULL);
|
|
477
|
+
return Qnil;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
static void finish_command(id<MTLCommandBuffer> command, int *state, NSError **error) {
|
|
481
|
+
if (!command) return;
|
|
482
|
+
// ponytail: one frame in flight; add buffering only if synchronous presentation limits throughput.
|
|
483
|
+
// rb_protect also catches interrupts while the GVL is released/reacquired.
|
|
484
|
+
rb_protect(submit_without_gvl, (VALUE)(__bridge void *)command, state);
|
|
485
|
+
if (!*state && command.status != MTLCommandBufferStatusCompleted) {
|
|
486
|
+
if (command.error) *error = command.error;
|
|
487
|
+
else native_error(error, @"GPU command failed");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Only call inside rb_protect (directly or through events_to_ruby).
|
|
492
|
+
static VALUE string_to_ruby(VALUE ptr) {
|
|
493
|
+
__unsafe_unretained NSString *text = (__bridge NSString *)(void *)ptr;
|
|
494
|
+
return rb_utf8_str_new(text.UTF8String, [text lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
static VALUE cocoa_present_frame(VALUE value, BOOL compute) {
|
|
498
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
499
|
+
VALUE message = Qnil;
|
|
500
|
+
int state = 0;
|
|
501
|
+
handle->busy = YES;
|
|
502
|
+
@autoreleasepool {
|
|
503
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
504
|
+
NSError *error = nil;
|
|
505
|
+
if (compute && (!window.useMetal || !window.metalView.hasComputeShader)) {
|
|
506
|
+
native_error(&error, @"Compute shader not compiled");
|
|
507
|
+
} else if (window.useMetal) {
|
|
508
|
+
id<MTLTexture> texture = compute ? window.metalView.outputTexture : window.metalView.texture;
|
|
509
|
+
id<MTLCommandBuffer> command = [window.metalView presentTexture:texture error:&error];
|
|
510
|
+
finish_command(command, &state, &error);
|
|
429
511
|
} else {
|
|
430
512
|
[window.imageView setNeedsDisplay:YES];
|
|
431
513
|
[window displayIfNeeded];
|
|
432
514
|
}
|
|
515
|
+
if (error && !state) {
|
|
516
|
+
message = rb_protect(string_to_ruby, (VALUE)(__bridge void *)error.localizedDescription, &state);
|
|
517
|
+
}
|
|
433
518
|
}
|
|
519
|
+
handle->busy = NO;
|
|
520
|
+
RB_GC_GUARD(value);
|
|
521
|
+
if (state) rb_jump_tag(state);
|
|
522
|
+
if (!NIL_P(message)) rb_exc_raise(rb_exc_new_str(rb_eRuntimeError, message));
|
|
434
523
|
return Qnil;
|
|
435
524
|
}
|
|
436
525
|
|
|
437
|
-
static VALUE
|
|
438
|
-
|
|
439
|
-
|
|
526
|
+
static VALUE cocoa_present(VALUE self, VALUE value) { return cocoa_present_frame(value, NO); }
|
|
527
|
+
static VALUE cocoa_present_compute(VALUE self, VALUE value) { return cocoa_present_frame(value, YES); }
|
|
528
|
+
|
|
529
|
+
// Called only via rb_protect: no owning Objective-C locals or autorelease pools
|
|
530
|
+
// may live in a frame that Ruby's longjmp can bypass.
|
|
531
|
+
static VALUE events_to_ruby(VALUE ptr) {
|
|
532
|
+
__unsafe_unretained NSArray *pending = (__bridge NSArray *)(void *)ptr;
|
|
533
|
+
VALUE events = rb_ary_new();
|
|
534
|
+
for (NSUInteger i = 0; i < pending.count; i++) {
|
|
535
|
+
__unsafe_unretained NSDictionary *dict = pending[i];
|
|
536
|
+
VALUE hash = rb_hash_new();
|
|
537
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("type")),
|
|
538
|
+
ID2SYM(rb_intern([dict[@"type"] UTF8String])));
|
|
539
|
+
if (dict[@"key"]) {
|
|
540
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("key")), INT2NUM([dict[@"key"] intValue]));
|
|
541
|
+
}
|
|
542
|
+
if (dict[@"char"]) {
|
|
543
|
+
__unsafe_unretained NSString *characters = dict[@"char"];
|
|
544
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("char")),
|
|
545
|
+
string_to_ruby((VALUE)(__bridge void *)characters));
|
|
546
|
+
}
|
|
547
|
+
if (dict[@"x"]) {
|
|
548
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("x")), DBL2NUM([dict[@"x"] doubleValue]));
|
|
549
|
+
}
|
|
550
|
+
if (dict[@"y"]) {
|
|
551
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("y")), DBL2NUM([dict[@"y"] doubleValue]));
|
|
552
|
+
}
|
|
553
|
+
if (dict[@"button"]) {
|
|
554
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("button")), INT2NUM([dict[@"button"] intValue]));
|
|
555
|
+
}
|
|
556
|
+
rb_ary_push(events, hash);
|
|
557
|
+
}
|
|
558
|
+
return events;
|
|
559
|
+
}
|
|
440
560
|
|
|
561
|
+
static VALUE cocoa_poll_events(VALUE self, VALUE value) {
|
|
562
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
563
|
+
int state = 0;
|
|
564
|
+
VALUE events;
|
|
565
|
+
handle->busy = YES;
|
|
566
|
+
@autoreleasepool {
|
|
567
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
441
568
|
NSEvent *event;
|
|
442
569
|
while ((event = [NSApp nextEventMatchingMask:NSEventMaskAny
|
|
443
570
|
untilDate:nil
|
|
@@ -445,108 +572,117 @@ static VALUE cocoa_poll_events(VALUE self, VALUE window_ptr) {
|
|
|
445
572
|
dequeue:YES])) {
|
|
446
573
|
[NSApp sendEvent:event];
|
|
447
574
|
}
|
|
448
|
-
|
|
449
575
|
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
|
|
450
576
|
beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.001]];
|
|
451
577
|
[NSApp updateWindows];
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
for (NSDictionary *dict in window.pendingEvents) {
|
|
455
|
-
VALUE hash = rb_hash_new();
|
|
456
|
-
|
|
457
|
-
NSString *type = dict[@"type"];
|
|
458
|
-
rb_hash_aset(hash, ID2SYM(rb_intern("type")),
|
|
459
|
-
ID2SYM(rb_intern([type UTF8String])));
|
|
460
|
-
|
|
461
|
-
if (dict[@"key"]) {
|
|
462
|
-
rb_hash_aset(hash, ID2SYM(rb_intern("key")), INT2NUM([dict[@"key"] intValue]));
|
|
463
|
-
}
|
|
464
|
-
if (dict[@"char"]) {
|
|
465
|
-
rb_hash_aset(hash, ID2SYM(rb_intern("char")),
|
|
466
|
-
rb_str_new_cstr([dict[@"char"] UTF8String]));
|
|
467
|
-
}
|
|
468
|
-
if (dict[@"x"]) {
|
|
469
|
-
rb_hash_aset(hash, ID2SYM(rb_intern("x")), DBL2NUM([dict[@"x"] doubleValue]));
|
|
470
|
-
}
|
|
471
|
-
if (dict[@"y"]) {
|
|
472
|
-
rb_hash_aset(hash, ID2SYM(rb_intern("y")), DBL2NUM([dict[@"y"] doubleValue]));
|
|
473
|
-
}
|
|
474
|
-
if (dict[@"button"]) {
|
|
475
|
-
rb_hash_aset(hash, ID2SYM(rb_intern("button")), INT2NUM([dict[@"button"] intValue]));
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
rb_ary_push(events, hash);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
[window.pendingEvents removeAllObjects];
|
|
482
|
-
return events;
|
|
578
|
+
events = rb_protect(events_to_ruby, (VALUE)(__bridge void *)window.pendingEvents, &state);
|
|
579
|
+
if (!state) [window.pendingEvents removeAllObjects];
|
|
483
580
|
}
|
|
581
|
+
handle->busy = NO;
|
|
582
|
+
RB_GC_GUARD(value);
|
|
583
|
+
if (state) rb_jump_tag(state);
|
|
584
|
+
return events;
|
|
484
585
|
}
|
|
485
586
|
|
|
486
|
-
static VALUE cocoa_should_close(VALUE self, VALUE
|
|
487
|
-
|
|
488
|
-
|
|
587
|
+
static VALUE cocoa_should_close(VALUE self, VALUE value) {
|
|
588
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
589
|
+
BOOL result;
|
|
590
|
+
@autoreleasepool {
|
|
591
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
592
|
+
result = window.shouldClose;
|
|
593
|
+
}
|
|
594
|
+
RB_GC_GUARD(value);
|
|
595
|
+
return result ? Qtrue : Qfalse;
|
|
489
596
|
}
|
|
490
597
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
598
|
+
static VALUE cocoa_metal_compute_available(VALUE self, VALUE value) {
|
|
599
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
600
|
+
BOOL result;
|
|
601
|
+
@autoreleasepool {
|
|
602
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
603
|
+
result = window.useMetal;
|
|
604
|
+
}
|
|
605
|
+
RB_GC_GUARD(value);
|
|
606
|
+
return result ? Qtrue : Qfalse;
|
|
496
607
|
}
|
|
497
608
|
|
|
498
|
-
static VALUE cocoa_compile_compute_shader(VALUE self, VALUE
|
|
609
|
+
static VALUE cocoa_compile_compute_shader(VALUE self, VALUE value, VALUE msl_source) {
|
|
610
|
+
Check_Type(msl_source, T_STRING);
|
|
611
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
612
|
+
VALUE message = Qnil;
|
|
613
|
+
int state = 0;
|
|
614
|
+
handle->busy = YES;
|
|
499
615
|
@autoreleasepool {
|
|
500
|
-
MetacoWindow *window = (__bridge MetacoWindow *)
|
|
616
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
617
|
+
NSString *source = [[NSString alloc] initWithBytes:RSTRING_PTR(msl_source)
|
|
618
|
+
length:RSTRING_LEN(msl_source)
|
|
619
|
+
encoding:NSUTF8StringEncoding];
|
|
620
|
+
NSError *error = nil;
|
|
501
621
|
if (!window.useMetal) {
|
|
502
|
-
|
|
622
|
+
native_error(&error, @"Metal not available");
|
|
623
|
+
} else if (!source) {
|
|
624
|
+
native_error(&error, @"Shader source must contain valid UTF-8");
|
|
625
|
+
} else if (![window.metalView compileComputeShader:source error:&error] && !error) {
|
|
626
|
+
native_error(&error, @"Failed to compile shader");
|
|
503
627
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
NSString *source = [NSString stringWithUTF8String:StringValueCStr(msl_source)];
|
|
507
|
-
NSError *error = nil;
|
|
508
|
-
|
|
509
|
-
if (![window.metalView compileComputeShader:source error:&error]) {
|
|
510
|
-
rb_raise(rb_eRuntimeError, "Failed to compile shader: %s",
|
|
511
|
-
[[error localizedDescription] UTF8String]);
|
|
628
|
+
if (error) {
|
|
629
|
+
message = rb_protect(string_to_ruby, (VALUE)(__bridge void *)error.localizedDescription, &state);
|
|
512
630
|
}
|
|
513
631
|
}
|
|
632
|
+
handle->busy = NO;
|
|
633
|
+
RB_GC_GUARD(msl_source);
|
|
634
|
+
RB_GC_GUARD(value);
|
|
635
|
+
if (state) rb_jump_tag(state);
|
|
636
|
+
if (!NIL_P(message)) rb_exc_raise(rb_exc_new_str(rb_eRuntimeError, message));
|
|
514
637
|
return Qtrue;
|
|
515
638
|
}
|
|
516
639
|
|
|
517
|
-
static VALUE cocoa_dispatch_compute(VALUE self, VALUE
|
|
640
|
+
static VALUE cocoa_dispatch_compute(VALUE self, VALUE value, VALUE uniform_data) {
|
|
641
|
+
Check_Type(uniform_data, T_STRING);
|
|
642
|
+
if (RSTRING_LEN(uniform_data) > 256) rb_raise(rb_eArgError, "Uniform data must be at most 256 bytes");
|
|
643
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
644
|
+
VALUE message = Qnil;
|
|
645
|
+
int state = 0;
|
|
646
|
+
handle->busy = YES;
|
|
518
647
|
@autoreleasepool {
|
|
519
|
-
MetacoWindow *window = (__bridge MetacoWindow *)
|
|
648
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
649
|
+
NSError *error = nil;
|
|
520
650
|
if (!window.useMetal || !window.metalView.hasComputeShader) {
|
|
521
|
-
|
|
651
|
+
native_error(&error, @"Compute shader not compiled");
|
|
652
|
+
} else {
|
|
653
|
+
id<MTLCommandBuffer> command = [window.metalView dispatchComputeWithUniforms:RSTRING_PTR(uniform_data)
|
|
654
|
+
length:RSTRING_LEN(uniform_data)
|
|
655
|
+
error:&error];
|
|
656
|
+
finish_command(command, &state, &error);
|
|
657
|
+
}
|
|
658
|
+
if (error && !state) {
|
|
659
|
+
message = rb_protect(string_to_ruby, (VALUE)(__bridge void *)error.localizedDescription, &state);
|
|
522
660
|
}
|
|
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
661
|
}
|
|
662
|
+
handle->busy = NO;
|
|
663
|
+
RB_GC_GUARD(uniform_data);
|
|
664
|
+
RB_GC_GUARD(value);
|
|
665
|
+
if (state) rb_jump_tag(state);
|
|
666
|
+
if (!NIL_P(message)) rb_exc_raise(rb_exc_new_str(rb_eRuntimeError, message));
|
|
530
667
|
return Qnil;
|
|
531
668
|
}
|
|
532
669
|
|
|
533
|
-
static VALUE
|
|
670
|
+
static VALUE cocoa_has_compute_shader(VALUE self, VALUE value) {
|
|
671
|
+
MetacoHandle *handle = get_handle(value, NO);
|
|
672
|
+
BOOL result;
|
|
534
673
|
@autoreleasepool {
|
|
535
|
-
MetacoWindow *window = (__bridge MetacoWindow *)
|
|
536
|
-
|
|
537
|
-
[window.metalView presentCompute];
|
|
538
|
-
}
|
|
674
|
+
MetacoWindow *window = (__bridge MetacoWindow *)handle->window;
|
|
675
|
+
result = window.useMetal && window.metalView.hasComputeShader;
|
|
539
676
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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;
|
|
677
|
+
RB_GC_GUARD(value);
|
|
678
|
+
return result ? Qtrue : Qfalse;
|
|
546
679
|
}
|
|
547
680
|
|
|
548
681
|
void Init_metaco(void) {
|
|
549
682
|
VALUE mMetaco = rb_define_module("Metaco");
|
|
683
|
+
cMetacoWindow = rb_define_class_under(mMetaco, "Window", rb_cObject);
|
|
684
|
+
rb_global_variable(&cMetacoWindow);
|
|
685
|
+
rb_undef_alloc_func(cMetacoWindow);
|
|
550
686
|
|
|
551
687
|
rb_define_module_function(mMetaco, "init", cocoa_init, 0);
|
|
552
688
|
rb_define_module_function(mMetaco, "window_create", cocoa_window_create, 3);
|
data/lib/metaco/version.rb
CHANGED
data/lib/metaco.rb
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "metaco/version"
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
if RUBY_PLATFORM.include?("darwin")
|
|
6
6
|
require "metaco/metaco"
|
|
7
|
-
|
|
8
|
-
# Extension not compiled - this is expected on non-macOS platforms
|
|
7
|
+
else
|
|
9
8
|
module Metaco
|
|
10
9
|
def self.init
|
|
11
10
|
raise LoadError, "Metaco is only available on macOS"
|
|
@@ -34,5 +33,25 @@ rescue LoadError
|
|
|
34
33
|
def self.should_close?(_handle)
|
|
35
34
|
raise LoadError, "Metaco is only available on macOS"
|
|
36
35
|
end
|
|
36
|
+
|
|
37
|
+
def self.metal_compute_available?(_handle)
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.has_compute_shader?(_handle)
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.compile_compute_shader(_handle, _source)
|
|
46
|
+
raise LoadError, "Metaco is only available on macOS"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.dispatch_compute(_handle, _uniforms)
|
|
50
|
+
raise LoadError, "Metaco is only available on macOS"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.present_compute(_handle)
|
|
54
|
+
raise LoadError, "Metaco is only available on macOS"
|
|
55
|
+
end
|
|
37
56
|
end
|
|
38
57
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: metaco
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
@@ -26,13 +26,12 @@ files:
|
|
|
26
26
|
- ext/metaco/metaco.m
|
|
27
27
|
- lib/metaco.rb
|
|
28
28
|
- lib/metaco/version.rb
|
|
29
|
-
|
|
30
|
-
homepage: https://github.com/ydah/metaco
|
|
29
|
+
homepage: https://github.com/rbgfx/metaco
|
|
31
30
|
licenses:
|
|
32
31
|
- MIT
|
|
33
32
|
metadata:
|
|
34
|
-
source_code_uri: https://github.com/
|
|
35
|
-
changelog_uri: https://github.com/
|
|
33
|
+
source_code_uri: https://github.com/rbgfx/metaco
|
|
34
|
+
changelog_uri: https://github.com/rbgfx/metaco/blob/main/CHANGELOG.md
|
|
36
35
|
rdoc_options: []
|
|
37
36
|
require_paths:
|
|
38
37
|
- lib
|
|
@@ -47,7 +46,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
47
46
|
- !ruby/object:Gem::Version
|
|
48
47
|
version: '0'
|
|
49
48
|
requirements: []
|
|
50
|
-
rubygems_version: 4.0.
|
|
49
|
+
rubygems_version: 4.0.19
|
|
51
50
|
specification_version: 4
|
|
52
51
|
summary: Native macOS Cocoa/Metal bridge for Ruby graphics applications
|
|
53
52
|
test_files: []
|
data/metaco.gemspec
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "lib/metaco/version"
|
|
4
|
-
|
|
5
|
-
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name = "metaco"
|
|
7
|
-
spec.version = Metaco::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/metaco"
|
|
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/metaco/extconf.rb"]
|
|
28
|
-
end
|