metaco 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +46 -10
- data/Rakefile +1 -0
- data/docs/index.html +38 -0
- data/ext/metaco/metaco.m +522 -19
- data/lib/metaco/version.rb +1 -1
- data/lib/metaco.rb +41 -1
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 338aed008f83c6aed4d0d9e14d2e39176337962507a6e76351f75bfd7c6bb6e2
|
|
4
|
+
data.tar.gz: 9c3a9abed42ab01969d8a57d627d4b6ef9789e7947f0721cb585748ff65cf99c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 30e20aa8707d371a3ff2836877bebef4afcb2343bd63cf5a915a0add4b339baf35bd200d5e5daf979b34acecf5549c016fa3823f49d0f38a8562233d36e7211a
|
|
7
|
+
data.tar.gz: 8be006a897ee591c15a7d8e3e3e96c5719e6357fdffe2d78000f3f1c2cfa1f39abac050d68d6ba7c25c1274cc2dc15cab40cd9986a8dbad29cfad21ea168e1fc
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.3.0 - 2026-09-25
|
|
6
|
+
|
|
7
|
+
- Add resizable windows, logical and framebuffer size reporting, Retina-aware dimensions, resize events, and resize-safe pixel/compute resources.
|
|
8
|
+
- Add native Metal input textures, pixel readback, and complete extension API stubs.
|
|
9
|
+
- Release native resources promptly when windows are explicitly destroyed or collected.
|
|
10
|
+
- Wait for outstanding Metal commands before teardown.
|
|
11
|
+
- Clarify that GC cleanup guarantees native resource release while AppKit may retain a closed window object temporarily.
|
|
12
|
+
|
|
3
13
|
## 0.2.0 - 2026-09-09
|
|
4
14
|
|
|
5
15
|
- Replace integer window pointers with GC-managed `Metaco::Window` handles. Validate closed handles, main-thread access, dimensions, and pixel buffer capacity.
|
data/README.md
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">metaco</h1>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">Native macOS window management and Metal GPU acceleration for Ruby.</p>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://rubygems.org/gems/metaco"><img src="https://badge.fury.io/rb/metaco.svg" alt="Gem Version"></a>
|
|
7
|
+
<a href="https://rubygems.org/gems/metaco"><img src="https://img.shields.io/gem/dt/metaco?label=downloads" alt="Downloads"></a>
|
|
8
|
+
<a href="https://www.ruby-lang.org/"><img src="https://img.shields.io/badge/ruby-%3E%3D3.0-CC342D?logo=ruby&logoColor=white" alt="Ruby Version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-750014.svg" alt="License"></a>
|
|
10
|
+
</p>
|
|
6
11
|
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
[Features](#features) · [Installation](#installation) · [Usage](#usage) · [API Reference](#api-reference)
|
|
13
|
+
|
|
14
|
+
***
|
|
15
|
+
|
|
16
|
+
Metaco is a Ruby C extension for Cocoa window management, pixel presentation, input events, and Metal GPU compute.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- Native Cocoa window lifecycle and RGBA pixel presentation.
|
|
21
|
+
- Keyboard, mouse, scroll, focus, and resize event polling.
|
|
22
|
+
- Metal compute shaders, texture upload, and pixel readback.
|
|
10
23
|
|
|
11
24
|
## Installation
|
|
12
25
|
|
|
@@ -28,6 +41,12 @@ Or install it yourself as:
|
|
|
28
41
|
gem install metaco
|
|
29
42
|
```
|
|
30
43
|
|
|
44
|
+
### Requirements
|
|
45
|
+
|
|
46
|
+
- macOS
|
|
47
|
+
- Ruby 3.0 or newer
|
|
48
|
+
- Xcode Command Line Tools
|
|
49
|
+
|
|
31
50
|
## Usage
|
|
32
51
|
|
|
33
52
|
### Basic Window
|
|
@@ -39,7 +58,7 @@ require "metaco"
|
|
|
39
58
|
Metaco.init
|
|
40
59
|
|
|
41
60
|
# Create a window
|
|
42
|
-
handle = Metaco.window_create(800, 600, "My Window")
|
|
61
|
+
handle = Metaco.window_create(800, 600, "My Window", resizable: true, high_dpi: true)
|
|
43
62
|
|
|
44
63
|
# Main loop
|
|
45
64
|
until Metaco.should_close?(handle)
|
|
@@ -115,16 +134,19 @@ Metaco.window_destroy(handle)
|
|
|
115
134
|
| Method | Description |
|
|
116
135
|
|--------|-------------|
|
|
117
136
|
| `init` | Initialize Cocoa application |
|
|
118
|
-
| `window_create(width, height, title)` | Create a new window, returns a `Metaco::Window` handle |
|
|
137
|
+
| `window_create(width, height, title, resizable: false, high_dpi: false)` | Create a new window, returns a `Metaco::Window` handle |
|
|
119
138
|
| `window_destroy(handle)` | Close and release the window; repeated calls are safe |
|
|
120
139
|
| `should_close?(handle)` | Check if window should close |
|
|
121
140
|
| `poll_events(handle)` | Poll and return pending events |
|
|
141
|
+
| `window_size(handle)` | Return the current logical `[width, height]` |
|
|
142
|
+
| `framebuffer_size(handle)` | Return the current drawable pixel `[width, height]` |
|
|
122
143
|
|
|
123
144
|
### Rendering
|
|
124
145
|
|
|
125
146
|
| Method | Description |
|
|
126
147
|
|--------|-------------|
|
|
127
148
|
| `set_pixels(handle, buffer, width, height)` | Set pixel data (RGBA format) |
|
|
149
|
+
| `read_pixels(handle, source: :compute)` | Read top-down RGBA8 pixels from `:compute` or `:set_pixels` |
|
|
128
150
|
| `present(handle)` | Present the frame and wait for GPU completion |
|
|
129
151
|
|
|
130
152
|
### Compute Shaders
|
|
@@ -136,6 +158,9 @@ Metaco.window_destroy(handle)
|
|
|
136
158
|
| `dispatch_compute(handle, uniforms)` | Execute compute shader and wait for GPU completion |
|
|
137
159
|
| `present_compute(handle)` | Present compute shader output and wait for GPU completion |
|
|
138
160
|
| `has_compute_shader?(handle)` | Check if shader is compiled |
|
|
161
|
+
| `texture_create(handle, width, height, bytes, filter: :linear, wrap: :clamp)` | Upload RGBA8 pixels as a Metal texture |
|
|
162
|
+
| `texture_update(texture, bytes)` / `texture_destroy(texture)` | Replace pixels or release a texture |
|
|
163
|
+
| `bind_compute_texture(handle, index, texture)` | Bind input texture at `texture(index + 1)` |
|
|
139
164
|
|
|
140
165
|
### Event Types
|
|
141
166
|
|
|
@@ -144,17 +169,24 @@ Metaco.window_destroy(handle)
|
|
|
144
169
|
- `:mouse_press` - Mouse button pressed (`:x`, `:y`, `:button`)
|
|
145
170
|
- `:mouse_release` - Mouse button released (`:x`, `:y`, `:button`)
|
|
146
171
|
- `:mouse_move` - Mouse moved (`:x`, `:y`)
|
|
172
|
+
- `:scroll` - Scroll distance in pixels (`:dx`, `:dy`, `:modifiers`). Wheel ticks are scaled by 10; positive `:dy` follows Cocoa's upward convention.
|
|
173
|
+
- `:focus` / `:blur` - Window key focus changed
|
|
174
|
+
- `:resize` - Window size changed (`:width`, `:height`, `:framebuffer_width`, `:framebuffer_height`)
|
|
175
|
+
|
|
176
|
+
Key and mouse events include `:modifiers`, an array of `:shift`, `:control`, `:option`, and `:command`.
|
|
147
177
|
|
|
148
178
|
### API contracts
|
|
149
179
|
|
|
150
180
|
- 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
181
|
- 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
|
|
153
|
-
- Width and height must be between 1 and 16,384. `set_pixels` requires the
|
|
182
|
+
- Release windows in an `ensure` block. GC also schedules AppKit cleanup on the main thread when necessary and releases abandoned window resources; `poll_events` services this queue.
|
|
183
|
+
- Width and height must be between 1 and 16,384. `resizable:` is false by default. `window_size` reports logical points; `framebuffer_size` reports drawable pixels, including the backing scale when `high_dpi: true`. `set_pixels` requires the current framebuffer 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
184
|
- 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
185
|
- 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
186
|
- 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
187
|
- 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.
|
|
188
|
+
- `read_pixels` copies the selected texture into CPU-visible memory before returning. It requires an open window and a completed render. GPU readback has not been verified on a Metal device in headless CI.
|
|
189
|
+
- Textures belong to the window that created them. Uploads require exactly `width * height * 4` RGBA8 bytes. Input textures use linear sampling and clamp addressing; other options raise `ArgumentError`. Destroyed textures cannot be updated or bound.
|
|
158
190
|
- 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
191
|
- 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
192
|
|
|
@@ -178,6 +210,10 @@ The test task includes a separate native test extension for GPU pixel readback,
|
|
|
178
210
|
|
|
179
211
|
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
212
|
|
|
213
|
+
## Contributing
|
|
214
|
+
|
|
215
|
+
Bug reports and pull requests are welcome at [rbgfx/metaco](https://github.com/rbgfx/metaco).
|
|
216
|
+
|
|
181
217
|
## License
|
|
182
218
|
|
|
183
219
|
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
data/Rakefile
CHANGED
data/docs/index.html
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en"><head>
|
|
3
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4
|
+
<meta name="theme-color" content="#020617"><meta name="description" content="A Ruby C extension for macOS windows, pixel presentation, event polling, and Metal compute.">
|
|
5
|
+
<meta property="og:title" content="Metaco — A native window, from Ruby."><meta property="og:description" content="A Ruby C extension for macOS windows, pixel presentation, event polling, and Metal compute."><meta property="og:type" content="website"><meta property="og:url" content="https://rbgfx.github.io/metaco/">
|
|
6
|
+
<meta name="twitter:card" content="summary"><link rel="canonical" href="https://rbgfx.github.io/metaco/"><title>Metaco — A native window, from Ruby.</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root{color-scheme:dark;--bg:#020617;--panel:#0f172a;--line:#334155;--ink:#e2e8f0;--muted:#94a3b8;--a:#38bdf8;--mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--sans:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;--w:min(100% - 2.5rem,76rem)}
|
|
9
|
+
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.65 var(--sans)}a{color:inherit;text-underline-offset:.22em}a:hover{color:var(--a)}a:focus-visible{outline:3px solid var(--a);outline-offset:4px}h1,h2,h3,p,figure,pre{margin:0}h1,h2,h3{line-height:1.08;text-wrap:balance}p{text-wrap:pretty}code,pre,.mono{font-family:var(--mono);font-variant-numeric:tabular-nums}.wrap{width:var(--w);margin:auto}.skip{position:fixed;left:1rem;top:1rem;z-index:1;transform:translateY(-160%);padding:.7rem 1rem;background:var(--ink);color:var(--bg)}.skip:focus{transform:none}
|
|
10
|
+
header{height:4.75rem;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between}.brand{display:flex;align-items:center;gap:.7rem;font-weight:750;text-decoration:none}.mark{width:2rem;height:2rem;display:grid;place-items:center;border:1px solid var(--a);color:var(--a);font:.85rem var(--mono)}nav{display:flex;gap:1.5rem;font-size:.9rem}nav a{text-decoration:none;color:var(--muted)}nav a:last-child{color:var(--ink)}
|
|
11
|
+
.hero{display:grid;grid-template-columns:.88fr 1.12fr;align-items:center;gap:clamp(2rem,6vw,6rem);padding-block:clamp(4rem,9vw,8rem)}.kicker,.visual figcaption,.num{font:.76rem/1.4 var(--mono);color:var(--a)}h1{max-width:11ch;margin-top:1rem;font-size:clamp(3rem,7.5vw,6.5rem);letter-spacing:-.065em}.lede{max-width:40rem;margin-top:1.5rem;color:var(--muted);font-size:1.15rem}.actions{display:flex;flex-wrap:wrap;gap:.8rem;margin-top:2rem}.button{min-height:2.8rem;padding:.65rem 1rem;border:1px solid var(--line);display:inline-flex;align-items:center;text-decoration:none;font-weight:650}.button.primary{background:var(--a);border-color:var(--a);color:var(--bg)}.install{margin-top:1.4rem;color:var(--muted);font:.86rem var(--mono)}.install code{color:var(--ink)}
|
|
12
|
+
.visual{border:1px solid var(--line);background:var(--panel)}.art{min-height:18rem;padding:1.25rem;display:grid;place-items:center;overflow:hidden}.art svg{width:100%;max-height:23rem}.visual figcaption{display:flex;justify-content:space-between;gap:1rem;padding:.75rem 1rem;border-top:1px solid var(--line);color:var(--muted)}.visual figcaption span:last-child{color:var(--a)}
|
|
13
|
+
section.info{padding-block:4.5rem;border-top:1px solid var(--line)}.section-head{display:grid;grid-template-columns:.72fr 1.28fr;gap:2rem;align-items:end;margin-bottom:2rem}h2{font-size:clamp(2rem,4vw,3.25rem);letter-spacing:-.045em}.section-head p,.feature p,.try-copy p{color:var(--muted)}.features{display:grid;grid-template-columns:repeat(3,1fr);border-top:1px solid var(--line)}.feature{padding:1.25rem 1.2rem 1.4rem 0;border-bottom:1px solid var(--line)}.feature+.feature{padding-left:1.2rem;border-left:1px solid var(--line)}.feature h3{margin-top:.8rem;font-size:1.1rem}.feature p{margin-top:.5rem;font-size:.94rem}.try{display:grid;grid-template-columns:.75fr 1.25fr;align-items:center;gap:clamp(2rem,7vw,7rem)}.try-copy p{margin-top:1rem}.try-copy a{color:var(--ink)}pre{overflow-x:auto;padding:1.25rem;border:1px solid var(--line);background:var(--panel);font-size:.82rem;line-height:1.8;tab-size:2}
|
|
14
|
+
footer{display:flex;justify-content:space-between;gap:1rem;padding-block:1.6rem;border-top:1px solid var(--line);color:var(--muted);font-size:.85rem}footer div{display:flex;gap:1.25rem}
|
|
15
|
+
@media(max-width:800px){.hero{grid-template-columns:1fr;gap:2.5rem}.hero-copy{max-width:42rem}.section-head,.try{grid-template-columns:1fr;gap:1rem}}@media(max-width:560px){:root{--w:min(100% - 1.5rem,76rem)}header{height:4.2rem}nav{gap:.8rem;font-size:.8rem}nav a:first-child{display:none}h1{font-size:clamp(3rem,15vw,5rem)}.features{grid-template-columns:1fr}.feature,.feature+.feature{padding:1.1rem 0;border-left:0}.num{display:inline-block}footer{flex-direction:column}.art{min-height:14rem}section.info{padding-block:3.5rem}pre{font-size:.75rem;padding:1rem}}
|
|
16
|
+
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
|
|
17
|
+
.window{width:100%;max-width:32rem;border:1px solid #475569;background:#020617}.window header{height:auto;padding:.7rem;border-bottom:1px solid #334155;font:.7rem var(--mono);color:#94a3b8}.window svg{display:block;padding:1rem}.window footer{justify-content:center;padding:.7rem;border-top:1px solid #334155;font:.7rem var(--mono);color:#fb923c}
|
|
18
|
+
</style>
|
|
19
|
+
</head><body style="--a:#fb923c"><a class="skip" href="#main">Skip to content</a><div class="wrap">
|
|
20
|
+
<header><a class="brand" href="#top" aria-label="Metaco home"><span class="mark" aria-hidden="true">M</span><span>Metaco</span></a><nav aria-label="Primary navigation"><a href="#features">Overview</a><a href="#quick-start">Quick start</a><a href="https://github.com/rbgfx/metaco">Source ↗</a></nav></header>
|
|
21
|
+
<main id="main"><section class="hero" id="top" aria-labelledby="title"><div class="hero-copy"><p class="kicker">Native macOS · Cocoa · Metal</p><h1 id="title">A native window, from Ruby.</h1><p class="lede">A Ruby C extension for macOS windows, pixel presentation, event polling, and Metal compute.</p><div class="actions"><a class="button primary" href="#quick-start">Get started</a><a class="button" href="https://rubygems.org/gems/metaco">RubyGems ↗</a></div><p class="install"><span aria-hidden="true">$</span> <code>gem install metaco</code></p></div>
|
|
22
|
+
<figure class="visual" aria-labelledby="cap-metaco"><div class="art"><div class="window"><header><span>● ● ●</span><span>Ruby Graphics</span><span>macOS</span></header><svg viewBox="0 0 360 190" aria-hidden="true"><rect width="360" height="190" fill="#111b2d"/><path d="M180 28L314 162H46Z" fill="#431407" stroke="#fb923c" stroke-width="2"/><path d="M180 28L180 162M46 162L247 95M314 162L113 95" stroke="#7c2d12"/></svg><footer>Ruby → Cocoa → Metal</footer></div></div><figcaption id="cap-metaco"><span>Cocoa window</span><span>Ruby calls → native APIs</span></figcaption></figure></section>
|
|
23
|
+
<section class="info" id="features" aria-labelledby="feat"><div class="section-head"><h2 id="feat">A focused bridge to the host.</h2><p>Create a native window, push RGBA pixels, receive input, or run Metal compute.</p></div><div class="features"><article class="feature"><span class="num">01</span><h3>Open a Cocoa window</h3><p>Create, update, present, and destroy windows from Ruby.</p></article><article class="feature"><span class="num">02</span><h3>Handle events</h3><p>Poll keyboard and mouse data in the application loop.</p></article><article class="feature"><span class="num">03</span><h3>Use Metal compute</h3><p>Check support, compile MSL, and dispatch compute work.</p></article></div></section>
|
|
24
|
+
<section class="info try" id="quick-start" aria-labelledby="quick"><div class="try-copy"><p class="kicker">A small first step</p><h2 id="quick">Start with one example.</h2><p>Install the gem, then follow the project guide for platform requirements and the full API.</p><p><a href="https://github.com/rbgfx/metaco#installation">Read the installation guide ↗</a></p></div><pre><code>require "metaco"
|
|
25
|
+
|
|
26
|
+
Metaco.init
|
|
27
|
+
handle = Metaco.window_create(
|
|
28
|
+
800, 600, "My Window",
|
|
29
|
+
resizable: true, high_dpi: true
|
|
30
|
+
)
|
|
31
|
+
begin
|
|
32
|
+
# rgba_bytes contains width * height RGBA pixels.
|
|
33
|
+
Metaco.set_pixels(handle, rgba_bytes, 800, 600)
|
|
34
|
+
Metaco.present(handle)
|
|
35
|
+
ensure
|
|
36
|
+
Metaco.window_destroy(handle)
|
|
37
|
+
end</code></pre></section></main>
|
|
38
|
+
<footer><span>Metaco · Ruby 3.0+ · MIT</span><div><a href="https://github.com/rbgfx/metaco">GitHub</a><a href="https://github.com/rbgfx/metaco/blob/main/LICENSE">License</a></div></footer></div></body></html>
|
data/ext/metaco/metaco.m
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
#import <ruby.h>
|
|
9
9
|
#import <ruby/thread.h>
|
|
10
10
|
#include <limits.h>
|
|
11
|
+
#include <math.h>
|
|
11
12
|
#include <pthread.h>
|
|
12
13
|
#include <stdint.h>
|
|
13
14
|
|
|
@@ -27,13 +28,33 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
27
28
|
@property (nonatomic, strong) id<MTLComputePipelineState> computePipeline;
|
|
28
29
|
@property (nonatomic, strong) id<MTLBuffer> uniformBuffer;
|
|
29
30
|
@property (nonatomic, strong) id<MTLTexture> outputTexture;
|
|
31
|
+
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, id<MTLTexture>> *computeTextures;
|
|
32
|
+
@property (nonatomic, strong) id<MTLCommandBuffer> lastCommandBuffer;
|
|
30
33
|
@property (nonatomic, readonly) BOOL hasComputeShader;
|
|
34
|
+
- (BOOL)resizeToWidth:(int)width height:(int)height error:(NSError **)error;
|
|
35
|
+
- (void)waitForLastCommand;
|
|
36
|
+
- (void)releaseResources;
|
|
31
37
|
@end
|
|
32
38
|
|
|
33
39
|
@implementation MetacoMetalView
|
|
34
40
|
|
|
35
41
|
- (BOOL)hasComputeShader { return self.computePipeline != nil; }
|
|
36
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;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [self.device newTextureWithDescriptor:desc];
|
|
56
|
+
}
|
|
57
|
+
|
|
37
58
|
- (instancetype)initWithFrame:(NSRect)frame device:(id<MTLDevice>)device width:(int)w height:(int)h {
|
|
38
59
|
self = [super initWithFrame:frame];
|
|
39
60
|
if (self) {
|
|
@@ -42,6 +63,7 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
42
63
|
self.texHeight = h;
|
|
43
64
|
self.commandQueue = [device newCommandQueue];
|
|
44
65
|
if (!self.commandQueue) return nil;
|
|
66
|
+
self.computeTextures = [NSMutableDictionary dictionary];
|
|
45
67
|
|
|
46
68
|
// A render pass preserves RGBA channel meaning when writing a BGRA drawable.
|
|
47
69
|
NSString *source = @"#include <metal_stdlib>\n"
|
|
@@ -133,12 +155,9 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
133
155
|
id<MTLComputePipelineState> pipeline = [self.device newComputePipelineStateWithFunction:function error:error];
|
|
134
156
|
if (!pipeline) return NO;
|
|
135
157
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
desc.usage = MTLTextureUsageShaderWrite | MTLTextureUsageShaderRead;
|
|
140
|
-
desc.storageMode = MTLStorageModePrivate;
|
|
141
|
-
id<MTLTexture> output = [self.device newTextureWithDescriptor:desc];
|
|
158
|
+
id<MTLTexture> output = [self newTextureWithWidth:self.texWidth height:self.texHeight
|
|
159
|
+
usage:MTLTextureUsageShaderWrite | MTLTextureUsageShaderRead
|
|
160
|
+
storageMode:MTLStorageModePrivate];
|
|
142
161
|
id<MTLBuffer> uniforms = [self.device newBufferWithLength:256 options:MTLResourceStorageModeShared];
|
|
143
162
|
if (!output || !uniforms || !uniforms.contents) {
|
|
144
163
|
native_error(error, @"Failed to allocate compute resources");
|
|
@@ -152,6 +171,52 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
152
171
|
return YES;
|
|
153
172
|
}
|
|
154
173
|
|
|
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;
|
|
189
|
+
}
|
|
190
|
+
|
|
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;
|
|
197
|
+
}
|
|
198
|
+
|
|
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
|
+
}
|
|
214
|
+
|
|
215
|
+
- (void)waitForLastCommand {
|
|
216
|
+
[self.lastCommandBuffer waitUntilCompleted];
|
|
217
|
+
self.lastCommandBuffer = nil;
|
|
218
|
+
}
|
|
219
|
+
|
|
155
220
|
- (id<MTLCommandBuffer>)dispatchComputeWithUniforms:(const void *)data length:(NSUInteger)length
|
|
156
221
|
error:(NSError **)error {
|
|
157
222
|
if (!self.hasComputeShader || length > 256) {
|
|
@@ -168,6 +233,9 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
168
233
|
}
|
|
169
234
|
[encoder setComputePipelineState:self.computePipeline];
|
|
170
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
|
+
}
|
|
171
239
|
[encoder setBuffer:self.uniformBuffer offset:0 atIndex:0];
|
|
172
240
|
|
|
173
241
|
NSUInteger limit = MIN(self.computePipeline.maxTotalThreadsPerThreadgroup,
|
|
@@ -188,11 +256,26 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
188
256
|
|
|
189
257
|
@end
|
|
190
258
|
|
|
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
|
+
|
|
191
269
|
@interface MetacoWindow : NSWindow <NSWindowDelegate>
|
|
192
270
|
@property (nonatomic, assign) BOOL shouldClose;
|
|
193
271
|
@property (nonatomic, strong) NSMutableArray *pendingEvents;
|
|
194
272
|
@property (nonatomic, strong) MetacoMetalView *metalView;
|
|
195
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;
|
|
196
279
|
// Fallback for non-Metal
|
|
197
280
|
@property (nonatomic, strong) NSBitmapImageRep *bitmapRep;
|
|
198
281
|
@property (nonatomic, strong) NSImageView *imageView;
|
|
@@ -200,18 +283,24 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
200
283
|
|
|
201
284
|
@implementation MetacoWindow
|
|
202
285
|
|
|
203
|
-
- (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 {
|
|
204
288
|
NSRect frame = NSMakeRect(100, 100, width, height);
|
|
289
|
+
NSUInteger style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable;
|
|
290
|
+
if (resizable) style |= NSWindowStyleMaskResizable;
|
|
205
291
|
self = [super initWithContentRect:frame
|
|
206
|
-
styleMask:
|
|
207
|
-
NSWindowStyleMaskClosable |
|
|
208
|
-
NSWindowStyleMaskMiniaturizable)
|
|
292
|
+
styleMask:style
|
|
209
293
|
backing:NSBackingStoreBuffered
|
|
210
294
|
defer:NO];
|
|
211
295
|
if (self) {
|
|
212
|
-
self.releasedWhenClosed =
|
|
296
|
+
self.releasedWhenClosed = YES;
|
|
213
297
|
self.shouldClose = NO;
|
|
214
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;
|
|
215
304
|
[self setTitle:title];
|
|
216
305
|
[self setDelegate:self];
|
|
217
306
|
|
|
@@ -226,6 +315,7 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
226
315
|
self.useMetal = self.metalView != nil;
|
|
227
316
|
if (self.useMetal) {
|
|
228
317
|
[self setContentView:self.metalView];
|
|
318
|
+
self.metalView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
229
319
|
} else {
|
|
230
320
|
// Device or rendering resource creation failed: use the bitmap path.
|
|
231
321
|
self.bitmapRep = [[NSBitmapImageRep alloc]
|
|
@@ -255,10 +345,13 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
255
345
|
}
|
|
256
346
|
[self.imageView setImage:image];
|
|
257
347
|
[self.imageView setImageScaling:NSImageScaleAxesIndependently];
|
|
348
|
+
self.imageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
258
349
|
[self setContentView:self.imageView];
|
|
259
350
|
}
|
|
260
351
|
|
|
352
|
+
[self setContentSize:NSMakeSize(width, height)];
|
|
261
353
|
[self setAcceptsMouseMovedEvents:YES];
|
|
354
|
+
self.initializing = NO;
|
|
262
355
|
[self makeKeyAndOrderFront:nil];
|
|
263
356
|
}
|
|
264
357
|
return self;
|
|
@@ -269,11 +362,44 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
269
362
|
return NO;
|
|
270
363
|
}
|
|
271
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
|
+
|
|
272
397
|
- (void)keyDown:(NSEvent *)event {
|
|
273
398
|
NSDictionary *eventDict = @{
|
|
274
399
|
@"type": @"key_press",
|
|
275
400
|
@"key": @([event keyCode]),
|
|
276
|
-
@"char": [event characters] ?: @""
|
|
401
|
+
@"char": [event characters] ?: @"",
|
|
402
|
+
@"modifiers": event_modifiers(event)
|
|
277
403
|
};
|
|
278
404
|
[self.pendingEvents addObject:eventDict];
|
|
279
405
|
}
|
|
@@ -281,7 +407,8 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
281
407
|
- (void)keyUp:(NSEvent *)event {
|
|
282
408
|
NSDictionary *eventDict = @{
|
|
283
409
|
@"type": @"key_release",
|
|
284
|
-
@"key": @([event keyCode])
|
|
410
|
+
@"key": @([event keyCode]),
|
|
411
|
+
@"modifiers": event_modifiers(event)
|
|
285
412
|
};
|
|
286
413
|
[self.pendingEvents addObject:eventDict];
|
|
287
414
|
}
|
|
@@ -292,7 +419,8 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
292
419
|
@"type": @"mouse_press",
|
|
293
420
|
@"x": @(loc.x),
|
|
294
421
|
@"y": @(loc.y),
|
|
295
|
-
@"button": @([event buttonNumber])
|
|
422
|
+
@"button": @([event buttonNumber]),
|
|
423
|
+
@"modifiers": event_modifiers(event)
|
|
296
424
|
};
|
|
297
425
|
[self.pendingEvents addObject:eventDict];
|
|
298
426
|
}
|
|
@@ -303,7 +431,8 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
303
431
|
@"type": @"mouse_release",
|
|
304
432
|
@"x": @(loc.x),
|
|
305
433
|
@"y": @(loc.y),
|
|
306
|
-
@"button": @([event buttonNumber])
|
|
434
|
+
@"button": @([event buttonNumber]),
|
|
435
|
+
@"modifiers": event_modifiers(event)
|
|
307
436
|
};
|
|
308
437
|
[self.pendingEvents addObject:eventDict];
|
|
309
438
|
}
|
|
@@ -313,11 +442,23 @@ static void native_error(NSError **error, NSString *message) {
|
|
|
313
442
|
NSDictionary *eventDict = @{
|
|
314
443
|
@"type": @"mouse_move",
|
|
315
444
|
@"x": @(loc.x),
|
|
316
|
-
@"y": @(loc.y)
|
|
445
|
+
@"y": @(loc.y),
|
|
446
|
+
@"modifiers": event_modifiers(event)
|
|
317
447
|
};
|
|
318
448
|
[self.pendingEvents addObject:eventDict];
|
|
319
449
|
}
|
|
320
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
|
+
|
|
321
462
|
- (void)rightMouseDown:(NSEvent *)event { [self mouseDown:event]; }
|
|
322
463
|
- (void)rightMouseUp:(NSEvent *)event { [self mouseUp:event]; }
|
|
323
464
|
- (void)otherMouseDown:(NSEvent *)event { [self mouseDown:event]; }
|
|
@@ -334,11 +475,22 @@ typedef struct {
|
|
|
334
475
|
void *window;
|
|
335
476
|
int width;
|
|
336
477
|
int height;
|
|
478
|
+
int logical_width;
|
|
479
|
+
int logical_height;
|
|
337
480
|
size_t byte_length;
|
|
338
481
|
BOOL busy;
|
|
339
482
|
} MetacoHandle;
|
|
340
483
|
|
|
484
|
+
typedef struct {
|
|
485
|
+
void *texture;
|
|
486
|
+
VALUE owner;
|
|
487
|
+
int width;
|
|
488
|
+
int height;
|
|
489
|
+
size_t byte_length;
|
|
490
|
+
} MetacoTextureHandle;
|
|
491
|
+
|
|
341
492
|
static VALUE cMetacoWindow;
|
|
493
|
+
static VALUE cMetacoTexture;
|
|
342
494
|
|
|
343
495
|
static void require_main_thread(void) {
|
|
344
496
|
if (!pthread_main_np()) rb_raise(rb_eThreadError, "Metaco must be called on the main thread");
|
|
@@ -355,10 +507,79 @@ static size_t pixel_byte_length(int width, int height) {
|
|
|
355
507
|
return (size_t)width * (size_t)height * 4;
|
|
356
508
|
}
|
|
357
509
|
|
|
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
|
+
}
|
|
528
|
+
|
|
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
|
+
}
|
|
553
|
+
|
|
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);
|
|
566
|
+
}
|
|
567
|
+
handle->logical_width = logical_width;
|
|
568
|
+
handle->logical_height = logical_height;
|
|
569
|
+
return YES;
|
|
570
|
+
}
|
|
571
|
+
|
|
358
572
|
static void release_window(void *ptr) {
|
|
359
573
|
@autoreleasepool {
|
|
360
574
|
MetacoWindow *window = (__bridge_transfer MetacoWindow *)ptr;
|
|
361
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;
|
|
362
583
|
[window close];
|
|
363
584
|
}
|
|
364
585
|
}
|
|
@@ -389,6 +610,34 @@ static MetacoHandle *get_handle(VALUE value, BOOL allow_closed) {
|
|
|
389
610
|
return handle;
|
|
390
611
|
}
|
|
391
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); }
|
|
626
|
+
|
|
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
|
+
}
|
|
640
|
+
|
|
392
641
|
static VALUE cocoa_init(VALUE self) {
|
|
393
642
|
require_main_thread();
|
|
394
643
|
@autoreleasepool {
|
|
@@ -399,7 +648,8 @@ static VALUE cocoa_init(VALUE self) {
|
|
|
399
648
|
return Qnil;
|
|
400
649
|
}
|
|
401
650
|
|
|
402
|
-
static VALUE
|
|
651
|
+
static VALUE cocoa_window_create_native(VALUE self, VALUE width, VALUE height, VALUE title,
|
|
652
|
+
VALUE resizable, VALUE high_dpi) {
|
|
403
653
|
require_main_thread();
|
|
404
654
|
int w = NUM2INT(width);
|
|
405
655
|
int h = NUM2INT(height);
|
|
@@ -409,8 +659,11 @@ static VALUE cocoa_window_create(VALUE self, VALUE width, VALUE height, VALUE ti
|
|
|
409
659
|
|
|
410
660
|
MetacoHandle *handle;
|
|
411
661
|
VALUE result = TypedData_Make_Struct(cMetacoWindow, MetacoHandle, &handle_type, handle);
|
|
662
|
+
handle->window = NULL;
|
|
412
663
|
handle->width = w;
|
|
413
664
|
handle->height = h;
|
|
665
|
+
handle->logical_width = w;
|
|
666
|
+
handle->logical_height = h;
|
|
414
667
|
handle->byte_length = length;
|
|
415
668
|
BOOL valid_title;
|
|
416
669
|
@autoreleasepool {
|
|
@@ -419,16 +672,40 @@ static VALUE cocoa_window_create(VALUE self, VALUE width, VALUE height, VALUE ti
|
|
|
419
672
|
encoding:NSUTF8StringEncoding];
|
|
420
673
|
valid_title = text != nil;
|
|
421
674
|
if (valid_title) {
|
|
422
|
-
MetacoWindow *window = [[MetacoWindow alloc] initWithWidth:w height:h title:text
|
|
675
|
+
MetacoWindow *window = [[MetacoWindow alloc] initWithWidth:w height:h title:text
|
|
676
|
+
resizable:RTEST(resizable)
|
|
677
|
+
highDPI:RTEST(high_dpi)];
|
|
423
678
|
handle->window = (__bridge_retained void *)window;
|
|
424
679
|
}
|
|
425
680
|
}
|
|
426
681
|
RB_GC_GUARD(title);
|
|
427
682
|
if (!valid_title) rb_raise(rb_eArgError, "Title must contain valid UTF-8");
|
|
428
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
|
+
}
|
|
429
691
|
return result;
|
|
430
692
|
}
|
|
431
693
|
|
|
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
|
+
}
|
|
708
|
+
|
|
432
709
|
static VALUE cocoa_window_destroy(VALUE self, VALUE value) {
|
|
433
710
|
MetacoHandle *handle = get_handle(value, YES);
|
|
434
711
|
void *ptr = handle->window;
|
|
@@ -445,6 +722,10 @@ static VALUE cocoa_set_pixels(VALUE self, VALUE value, VALUE buffer, VALUE width
|
|
|
445
722
|
size_t length = pixel_byte_length(w, h);
|
|
446
723
|
Check_Type(buffer, T_STRING);
|
|
447
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
|
+
}
|
|
448
729
|
if (w != handle->width || h != handle->height) {
|
|
449
730
|
rb_raise(rb_eArgError, "Pixel dimensions must match the window");
|
|
450
731
|
}
|
|
@@ -496,6 +777,10 @@ static VALUE string_to_ruby(VALUE ptr) {
|
|
|
496
777
|
|
|
497
778
|
static VALUE cocoa_present_frame(VALUE value, BOOL compute) {
|
|
498
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
|
+
}
|
|
499
784
|
VALUE message = Qnil;
|
|
500
785
|
int state = 0;
|
|
501
786
|
handle->busy = YES;
|
|
@@ -507,7 +792,9 @@ static VALUE cocoa_present_frame(VALUE value, BOOL compute) {
|
|
|
507
792
|
} else if (window.useMetal) {
|
|
508
793
|
id<MTLTexture> texture = compute ? window.metalView.outputTexture : window.metalView.texture;
|
|
509
794
|
id<MTLCommandBuffer> command = [window.metalView presentTexture:texture error:&error];
|
|
795
|
+
window.metalView.lastCommandBuffer = command;
|
|
510
796
|
finish_command(command, &state, &error);
|
|
797
|
+
if (!state) window.metalView.lastCommandBuffer = nil;
|
|
511
798
|
} else {
|
|
512
799
|
[window.imageView setNeedsDisplay:YES];
|
|
513
800
|
[window displayIfNeeded];
|
|
@@ -553,6 +840,25 @@ static VALUE events_to_ruby(VALUE ptr) {
|
|
|
553
840
|
if (dict[@"button"]) {
|
|
554
841
|
rb_hash_aset(hash, ID2SYM(rb_intern("button")), INT2NUM([dict[@"button"] intValue]));
|
|
555
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
|
+
}
|
|
556
862
|
rb_ary_push(events, hash);
|
|
557
863
|
}
|
|
558
864
|
return events;
|
|
@@ -609,6 +915,10 @@ static VALUE cocoa_metal_compute_available(VALUE self, VALUE value) {
|
|
|
609
915
|
static VALUE cocoa_compile_compute_shader(VALUE self, VALUE value, VALUE msl_source) {
|
|
610
916
|
Check_Type(msl_source, T_STRING);
|
|
611
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
|
+
}
|
|
612
922
|
VALUE message = Qnil;
|
|
613
923
|
int state = 0;
|
|
614
924
|
handle->busy = YES;
|
|
@@ -641,6 +951,10 @@ static VALUE cocoa_dispatch_compute(VALUE self, VALUE value, VALUE uniform_data)
|
|
|
641
951
|
Check_Type(uniform_data, T_STRING);
|
|
642
952
|
if (RSTRING_LEN(uniform_data) > 256) rb_raise(rb_eArgError, "Uniform data must be at most 256 bytes");
|
|
643
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
|
+
}
|
|
644
958
|
VALUE message = Qnil;
|
|
645
959
|
int state = 0;
|
|
646
960
|
handle->busy = YES;
|
|
@@ -653,7 +967,9 @@ static VALUE cocoa_dispatch_compute(VALUE self, VALUE value, VALUE uniform_data)
|
|
|
653
967
|
id<MTLCommandBuffer> command = [window.metalView dispatchComputeWithUniforms:RSTRING_PTR(uniform_data)
|
|
654
968
|
length:RSTRING_LEN(uniform_data)
|
|
655
969
|
error:&error];
|
|
970
|
+
window.metalView.lastCommandBuffer = command;
|
|
656
971
|
finish_command(command, &state, &error);
|
|
972
|
+
if (!state) window.metalView.lastCommandBuffer = nil;
|
|
657
973
|
}
|
|
658
974
|
if (error && !state) {
|
|
659
975
|
message = rb_protect(string_to_ruby, (VALUE)(__bridge void *)error.localizedDescription, &state);
|
|
@@ -678,14 +994,194 @@ static VALUE cocoa_has_compute_shader(VALUE self, VALUE value) {
|
|
|
678
994
|
return result ? Qtrue : Qfalse;
|
|
679
995
|
}
|
|
680
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
|
+
}
|
|
1007
|
+
|
|
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
|
+
}
|
|
1054
|
+
|
|
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];
|
|
1065
|
+
}
|
|
1066
|
+
RB_GC_GUARD(bytes);
|
|
1067
|
+
RB_GC_GUARD(value);
|
|
1068
|
+
return Qnil;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
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);
|
|
1076
|
+
@autoreleasepool {
|
|
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];
|
|
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;
|
|
1100
|
+
}
|
|
1101
|
+
RB_GC_GUARD(value);
|
|
1102
|
+
RB_GC_GUARD(texture_value);
|
|
1103
|
+
return Qnil;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
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;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
681
1174
|
void Init_metaco(void) {
|
|
682
1175
|
VALUE mMetaco = rb_define_module("Metaco");
|
|
683
1176
|
cMetacoWindow = rb_define_class_under(mMetaco, "Window", rb_cObject);
|
|
684
1177
|
rb_global_variable(&cMetacoWindow);
|
|
685
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);
|
|
686
1182
|
|
|
687
1183
|
rb_define_module_function(mMetaco, "init", cocoa_init, 0);
|
|
688
|
-
rb_define_module_function(mMetaco, "window_create", cocoa_window_create,
|
|
1184
|
+
rb_define_module_function(mMetaco, "window_create", cocoa_window_create, -1);
|
|
689
1185
|
rb_define_module_function(mMetaco, "window_destroy", cocoa_window_destroy, 1);
|
|
690
1186
|
rb_define_module_function(mMetaco, "set_pixels", cocoa_set_pixels, 4);
|
|
691
1187
|
rb_define_module_function(mMetaco, "present", cocoa_present, 1);
|
|
@@ -698,4 +1194,11 @@ void Init_metaco(void) {
|
|
|
698
1194
|
rb_define_module_function(mMetaco, "dispatch_compute", cocoa_dispatch_compute, 2);
|
|
699
1195
|
rb_define_module_function(mMetaco, "present_compute", cocoa_present_compute, 1);
|
|
700
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);
|
|
701
1204
|
}
|
data/lib/metaco/version.rb
CHANGED
data/lib/metaco.rb
CHANGED
|
@@ -4,13 +4,25 @@ require_relative "metaco/version"
|
|
|
4
4
|
|
|
5
5
|
if RUBY_PLATFORM.include?("darwin")
|
|
6
6
|
require "metaco/metaco"
|
|
7
|
+
module Metaco
|
|
8
|
+
def self.texture_create(handle, width, height, bytes, filter: :linear, wrap: :clamp)
|
|
9
|
+
raise ArgumentError, "filter must be :linear" unless filter == :linear
|
|
10
|
+
raise ArgumentError, "wrap must be :clamp" unless wrap == :clamp
|
|
11
|
+
|
|
12
|
+
texture_create_native(handle, width, height, bytes)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.read_pixels(handle, source: :compute)
|
|
16
|
+
read_pixels_native(handle, source)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
7
19
|
else
|
|
8
20
|
module Metaco
|
|
9
21
|
def self.init
|
|
10
22
|
raise LoadError, "Metaco is only available on macOS"
|
|
11
23
|
end
|
|
12
24
|
|
|
13
|
-
def self.window_create(_w, _h, _t)
|
|
25
|
+
def self.window_create(_w, _h, _t, resizable: false, high_dpi: false)
|
|
14
26
|
raise LoadError, "Metaco is only available on macOS"
|
|
15
27
|
end
|
|
16
28
|
|
|
@@ -53,5 +65,33 @@ else
|
|
|
53
65
|
def self.present_compute(_handle)
|
|
54
66
|
raise LoadError, "Metaco is only available on macOS"
|
|
55
67
|
end
|
|
68
|
+
|
|
69
|
+
def self.texture_create(_handle, _width, _height, _bytes, filter: :linear, wrap: :clamp)
|
|
70
|
+
raise LoadError, "Metaco is only available on macOS"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.texture_update(_texture, _bytes)
|
|
74
|
+
raise LoadError, "Metaco is only available on macOS"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.texture_destroy(_texture)
|
|
78
|
+
raise LoadError, "Metaco is only available on macOS"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.bind_compute_texture(_handle, _index, _texture)
|
|
82
|
+
raise LoadError, "Metaco is only available on macOS"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.read_pixels(_handle, source: :compute)
|
|
86
|
+
raise LoadError, "Metaco is only available on macOS"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.window_size(_handle)
|
|
90
|
+
raise LoadError, "Metaco is only available on macOS"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.framebuffer_size(_handle)
|
|
94
|
+
raise LoadError, "Metaco is only available on macOS"
|
|
95
|
+
end
|
|
56
96
|
end
|
|
57
97
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: metaco
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-09-25 00:00:00.000000000 Z
|
|
11
12
|
dependencies: []
|
|
12
13
|
description: A Ruby C extension providing native macOS window management and Metal
|
|
13
14
|
GPU acceleration for graphics applications.
|
|
@@ -22,6 +23,7 @@ files:
|
|
|
22
23
|
- LICENSE
|
|
23
24
|
- README.md
|
|
24
25
|
- Rakefile
|
|
26
|
+
- docs/index.html
|
|
25
27
|
- ext/metaco/extconf.rb
|
|
26
28
|
- ext/metaco/metaco.m
|
|
27
29
|
- lib/metaco.rb
|
|
@@ -32,6 +34,7 @@ licenses:
|
|
|
32
34
|
metadata:
|
|
33
35
|
source_code_uri: https://github.com/rbgfx/metaco
|
|
34
36
|
changelog_uri: https://github.com/rbgfx/metaco/blob/main/CHANGELOG.md
|
|
37
|
+
post_install_message:
|
|
35
38
|
rdoc_options: []
|
|
36
39
|
require_paths:
|
|
37
40
|
- lib
|
|
@@ -46,7 +49,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
46
49
|
- !ruby/object:Gem::Version
|
|
47
50
|
version: '0'
|
|
48
51
|
requirements: []
|
|
49
|
-
rubygems_version: 4.
|
|
52
|
+
rubygems_version: 3.4.19
|
|
53
|
+
signing_key:
|
|
50
54
|
specification_version: 4
|
|
51
55
|
summary: Native macOS Cocoa/Metal bridge for Ruby graphics applications
|
|
52
56
|
test_files: []
|