Skip to content

Repository files navigation

metaco

Native macOS window management and Metal GPU acceleration for Ruby.

Gem Version Downloads Ruby Version License

Features · Installation · Usage · API Reference


Metaco is a Ruby C extension for Cocoa window management, pixel presentation, input events, and Metal GPU compute.

Features

  • Native Cocoa window lifecycle and RGBA pixel presentation.
  • Keyboard, mouse, scroll, focus, and resize event polling.
  • Metal compute shaders, texture upload, and pixel readback.

Installation

Add this line to your application's Gemfile:

gem "metaco"

And then execute:

bundle install

Or install it yourself as:

gem install metaco

Requirements

  • macOS
  • Ruby 3.0 or newer
  • Xcode Command Line Tools

Usage

Basic Window

require "metaco"

# Initialize Cocoa
Metaco.init

# Create a window
handle = Metaco.window_create(800, 600, "My Window", resizable: true, high_dpi: true)

# Main loop
until Metaco.should_close?(handle)
  # Create pixel buffer (RGBA format)
  width, height = 800, 600
  buffer = "\xFF\x00\x00\xFF" * (width * height)  # Red

  # Update pixels and present
  Metaco.set_pixels(handle, buffer, width, height)
  Metaco.present(handle)

  # Handle events
  events = Metaco.poll_events(handle)
  events.each do |event|
    case event[:type]
    when :key_press
      puts "Key pressed: #{event[:key]}"
    when :mouse_press
      puts "Mouse clicked at: #{event[:x]}, #{event[:y]}"
    end
  end
end

# Cleanup
Metaco.window_destroy(handle)

Metal Compute Shaders

require "metaco"

Metaco.init
handle = Metaco.window_create(800, 600, "Compute Shader")

# Check Metal availability
if Metaco.metal_compute_available?(handle)
  # Compile shader (MSL)
  shader = <<~MSL
    #include <metal_stdlib>
    using namespace metal;

    kernel void compute_shader(
        texture2d<float, access::write> output [[texture(0)]],
        constant float4 &uniforms [[buffer(0)]],
        uint2 gid [[thread_position_in_grid]])
    {
        if (gid.x >= output.get_width() || gid.y >= output.get_height()) return;
        float2 uv = float2(gid) / float2(output.get_width(), output.get_height());
        output.write(float4(uv.x, uv.y, uniforms.x, 1.0), gid);
    }
  MSL

  Metaco.compile_compute_shader(handle, shader)

  until Metaco.should_close?(handle)
    # Dispatch with uniforms
    time = Time.now.to_f
    uniforms = [Math.sin(time) * 0.5 + 0.5, 0.0, 0.0, 1.0].pack("f4")
    Metaco.dispatch_compute(handle, uniforms)
    Metaco.present_compute(handle)
    Metaco.poll_events(handle)
  end
end

Metaco.window_destroy(handle)

API Reference

Window Management

Method Description
init Initialize Cocoa application
window_create(width, height, title, resizable: false, high_dpi: false) Create a new window, returns a Metaco::Window handle
window_destroy(handle) Close and release the window; repeated calls are safe
should_close?(handle) Check if window should close
poll_events(handle) Poll and return pending events
window_size(handle) Return the current logical [width, height]
framebuffer_size(handle) Return the current drawable pixel [width, height]

Rendering

Method Description
set_pixels(handle, buffer, width, height) Set pixel data (RGBA format)
read_pixels(handle, source: :compute) Read top-down RGBA8 pixels from :compute or :set_pixels
present(handle) Present the frame and wait for GPU completion

Compute Shaders

Method Description
metal_compute_available?(handle) Check if Metal compute is available
compile_compute_shader(handle, msl_source) Compile MSL compute shader
dispatch_compute(handle, uniforms) Execute compute shader and wait for GPU completion
present_compute(handle) Present compute shader output and wait for GPU completion
has_compute_shader?(handle) Check if shader is compiled
texture_create(handle, width, height, bytes, filter: :linear, wrap: :clamp) Upload RGBA8 pixels as a Metal texture
texture_update(texture, bytes) / texture_destroy(texture) Replace pixels or release a texture
bind_compute_texture(handle, index, texture) Bind input texture at texture(index + 1)

Event Types

  • :key_press - Key pressed (:key, :char)
  • :key_release - Key released (:key)
  • :mouse_press - Mouse button pressed (:x, :y, :button)
  • :mouse_release - Mouse button released (:x, :y, :button)
  • :mouse_move - Mouse moved (:x, :y)
  • :scroll - Scroll distance in pixels (:dx, :dy, :modifiers). Wheel ticks are scaled by 10; positive :dy follows Cocoa's upward convention.
  • :focus / :blur - Window key focus changed
  • :resize - Window size changed (:width, :height, :framebuffer_width, :framebuffer_height)

Key and mouse events include :modifiers, an array of :shift, :control, :option, and :command.

API contracts

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • On unsupported platforms both compute availability queries return false, and other APIs raise LoadError. On macOS, native extension loading errors retain their original diagnostics.

Development

# Install dependencies
bundle install

# Compile native extension
bundle exec rake compile

# Run tests
bundle exec rake test

# Require actual GPU execution and validate Metal commands/shaders
METACO_REQUIRE_METAL=1 MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1 bundle exec rake test

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.

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.

Contributing

Bug reports and pull requests are welcome at rbgfx/metaco.

License

The gem is available as open source under the terms of the MIT License.

About

Native macOS windows, pixel presentation, input events, and Metal compute for Ruby.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages