Status: future · Priority: high
Note: This is a future plan, not a commitment. The syntax and API shown here are proposals — they can be completely different when actually implemented.
Morph renders through OpenGL 3.3 today. The future is a pluggable graphics layer: Vulkan, Metal (macOS), and DirectX 11/12 (Windows) backends behind one internal abstraction — with the API selected per app or auto-detected per platform.
wgpu, which wraps Vulkan/Metal/D3D12/GL in one crate — nearly all backends for free on the Rust side// morph.config.json
{ "graphics": "auto" } // default: pick per platform (metal on macOS, d3d12 on Windows, opengl/vulkan on Linux)
{ "graphics": "vulkan" } // force a specific backend
{ "graphics": "opengl" } // today's behavior, unchangedconstexpr dispatch, unselected backends eliminated — zero dead code, no runtime branch)"graphics": "auto" resolves at build time based on the target platformThe low-level GPU layer (runtime/render/ — GLRenderer: rects/text/borders, batching + flush, clips, stencil) is already the natural seam. It becomes an interface with one backend per API:
runtime/render/
├── gpu.h # GpuDevice / GpuTexture / GpuBuffer / GpuPipeline / GpuSwapchain
├── gpu.cpp
├── backends/
│ ├── gl/ # today's GLRenderer, moved behind the interface
│ ├── vulkan/ # swapchain, command buffers, descriptor pools
│ ├── metal/ # MTLDevice, render pass encoders (macOS)
│ └── d3d12/ # command lists, root signatures (Windows)// gpu.h (sketch — the exact surface is free-form)
struct GpuDevice;
struct GpuTexture;
struct GpuSwapchain;
struct GpuDevice {
virtual GpuTexture* createTexture(int w, int h, const void* pixels) = 0;
virtual GpuBuffer* createVertexBuffer(const void* data, size_t size) = 0;
virtual void beginFrame(GpuSwapchain&) = 0;
virtual void draw(const Batch&, const GpuPipeline&) = 0;
virtual void present(GpuSwapchain&) = 0;
};Everything above the interface is unchanged: node tree, layout, style, the batched draw list, texture atlas, SDF math, stencil clipping.
The fragment shaders (rounded-rect SDF, borders, text, transforms) are the shared core:
This guarantees pixel-identical rendering across all backends — the same layout math, the same shader math.
| Concept | OpenGL | Vulkan | Metal | D3D12 |
|---|---|---|---|---|
| Frame buffer | FBO | swapchain + render pass | CAMetalLayer + pass | swapchain + command list |
| Batched quads | one VAO/VBO | one vertex buffer + descriptor | one buffer | one buffer + root sig |
| Texture atlas | GL texture | image view + sampler | MTLTexture | SRV heap |
| Clipping | stencil ops | stencil attachment | stencil in pass | stencil in PSO |
| Compositor thread | GL context owner | command pool + queue | render pass queue | command queue |
The compositor-thread model stays: each backend owns its device context on the compositor thread and presents at vsync.
| Building block | State |
|---|---|
OpenGL 3.3 renderer (runtime/render/) |
✅ Shipped |
| Renderer seam pattern (Flash/Forge compile-time + dev toggle) | ✅ Shipped — the blueprint to copy |
| SDF shader stack (rounded rects, borders, text) | ✅ Shipped |
graphics config key |
❌ Not built |
GPU abstraction (gpu.h) |
❌ Not built |
| Vulkan / Metal / D3D backends | ❌ Not started |
GLRenderer behind gpu.h; zero behavior change (the Flash/Forge Phase-1 pattern, reused)"graphics": "auto" per platform + forced overrides + dev runtime switchwgpu, the abstraction is inherited; should the C++ side mirror wgpu's surface (hal-like) for symmetry?gpu.h seam + GLRenderer extraction, smoke-tested pixel-identicalexamples/calculator running pixel-identical"graphics": "auto" resolution + config override + docs