Search by

jovian / venusian-metal

projectsaturnstudios

The 'metal' rendering engine for Venusian Surface: a GpuDevice over ext-metal that draws Surface's draw lists on the Mac's GPU and presents into a CAMetalLayer.

0.10.x-dev 2026-10-15 14:13 UTC

This package is auto-updated.

Last update: 2026-10-15 14:13:39 UTC


README

The metal rendering engine for Venusian Surface. Metal through ext-metal.

Where it sits

DrawingManager → GpuRenderingEngine (Surface) → MetalDevice (this package) → ext-metal → Metal

The engine's framebuffer is a MetalFramebuffer, a Surface GLFramebuffer.

Requirements

  • macOS on Apple silicon. Engine blends by reading the target in its fragment functions; an Intel GPU refuses the pipelines with a DrawingException naming it.
  • PHP 8.4, ext-metal ^0.10, venusian-surface/drawing ^0.10.

Install

composer require jovian/venusian-metal

Provider discovered through extra.venusian.providers. It registers metal on app('drawing').

Usage

Offscreen. Run as a script; printed pixels are its output.

use Surface\Contracts\Drawing\RenderingEngine;
use Surface\Contracts\Framebuffers\FormatSpec;
use Surface\NutsAndBolts\Color;

$engine = app('drawing')->renderer('metal', ['width' => 320, 'height' => 240]);
$engine->frame(fn (RenderingEngine $g) => $g->clear(Color::rgb(16, 24, 32))->fillEllipse(160, 120, 60, 40, Color::rgb(255, 128, 0)));
$rgba = $engine->framebuffer()->flush(FormatSpec::rgba8());

echo bin2hex(substr($rgba, (120 * 320 + 160) * 4, 4)), "\n";   // ff8000ff  ellipse centre
echo bin2hex(substr($rgba, 0, 4)), "\n";                       // 101820ff  corner

On a display. The display sends what changed, in its own format.

$display = app('displays')->panel('st7796');
$engine = app('drawing')->renderer('metal', ['output' => $display]);

On a canvas.

$engine = app('drawing')->renderer('metal', ['output' => $canvas]);

Needs a toolkit that lends a Metal layer (venusian-appkit, slice 8b). Until then the refusal reads This window cannot host 'metal': it lends … It can host: ….

Presenting and HDR

Present = show pass: target any size → window's presentRect(), window's filter, black bars, encoded for layer. Follows window vsync (on syncs display; off/mailbox don't). frames_in_flight 1-3. presents() = when frames hit screen (AppKit event cycle delivers times; waitPresented() pumps it). Minimised/hidden window never stalls a present (measured: 20 vsynced presents in 209 ms).

HDR on EDR displays (needs ext-appkit):

$engine = $drawing->engine('metal', ['output' => $window, 'format' => TargetFormat::Rgba16Float]);
$engine->frame(fn ($g) => $g->image(HdrImage::fromFloats($rgba, $w, $h), 0, 0));

Target = linear half floats, SDR white 1.0. Pairs: rgba16float extended-linear sRGB / P3 (EDR, × window SDR white), rgb10a2 HDR10 (PQ, BT.2020, HDR10 metadata), rgba8 Display P3.

What it draws

Operation Draws
CLEAR the colour over the whole target, no blend
SCISSOR clip rectangle for what follows
SOLID flat-colour triangles, no blend
STENCIL_FILL + COVER a path: triangle lists into the stencil (winding or even-odd), then a cover quad blended where the stencil is non-zero and zeroed
ELLIPSE, RING per-sample inside tests of the outer (and inner) ellipse
UPLOAD + IMAGE a framebuffer as a texture; sampled at the inverse placement of the pixel centre, nearest or Velvet's 256ths bilinear
RECTS rectangles blended at the colour's alpha

Edges: four samples resolved (anti-aliased) or one (hard). Blending: Velvet's integer source-over per sample, so interior pixels match Velvet byte for byte (the parity suite's rule).

Reference

  • MetalDevice: __construct(?MTLDevice), metal(), target(), draw(), surfaces(), handles(), adopt(), present(), release(), read(), write(), finish().
  • MetalFramebuffer: texture(), the GLFramebuffer methods.
  • VenusianMetalServiceProvider::extend(DrawingManager).

Behaviour

  • Readback waits for the frame.
  • A pixel call between frames costs a readback and an upload and, with anti-aliased edges, a restore pass.
  • present() skips the copy while the previous one is in flight, answering false.
  • The engine's own framebuffer as an image source shows the frame before the current one.
  • A re-made target is a new MetalFramebuffer; the old one keeps its own texture.
  • A layer with no view hands out drawables; the copy is proven there, the window cells are slice 8b.

Measured

Apple M1 Pro, 320 × 240, 100 ellipses and a line of text, anti-aliased: whole redraw 1.25 ms a frame, partial frames 4.57 ms, peak memory 4.0 MB. (Velvet, both extensions, same scene shape: 6.8 ms.)

Testing

Real GPU only; nothing skipped. tests/Pest.php requires Surface's GpuParity from the sibling checkout; set SURFACE_TESTS to a Surface checkout's tests directory to move it. Install with a temporary path repository to <surface>/src/Surface/*, run php -d memory_limit=128M vendor/bin/pest on php84 and ZTS. Runbook: .okf/runbooks/testing.md.

Security

See SECURITY.md.

License

MIT.