Theme Catppuccin
Make yourself at homeCatppuccin

A softer light for long learning sessions.

Appearance

Loading your preference…

Dark palette

Choose a dark palette to try it.

Light uses Latte. Your choices stay in this browser.

Back to your source shelf

Inside the companion

Metal · Copper Orchard

Connect a Cocoa window to a C++ Metal renderer and verify GPU results.

Project files 7

Bridge the window into C++

Cocoa and MTKView setup, main-thread events, and guarded renderer callbacks.

155 lines orchard_window.mm
#import <Cocoa/Cocoa.h>
#import <MetalKit/MetalKit.h>

#include "window_renderer.hpp"

#include <charconv>
#include <chrono>
#include <exception>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string_view>

namespace {
struct Options {
    std::optional<std::size_t> frames;
    std::optional<std::size_t> fail_after;
    bool resize{};
    bool verify{};
};
std::size_t number(std::string_view text) {
    std::size_t value{};
    const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value);
    if (error != std::errc{} || end != text.data() + text.size() || value < 1 || value > 10000)
        throw std::runtime_error("frame counts must be integers from 1 to 10000");
    return value;
}
Options options(int argc, const char* const* argv) {
    Options result;
    for (int i = 1; i < argc; ++i) {
        const std::string_view arg{argv[i]};
        if ((arg == "--frames" || arg == "--fail-after") && i + 1 < argc) {
            const auto value = number(argv[++i]);
            if (arg == "--frames") result.frames = value;
            else result.fail_after = value;
        } else if (arg == "--resize") result.resize = true;
        else if (arg == "--verify") result.verify = true;
        else throw std::runtime_error("usage: orchard-window [--frames N] [--verify] [--resize] [--fail-after N]");
    }
    return result;
}
}

@interface OrchardBridge : NSObject <MTKViewDelegate, NSWindowDelegate> {
@public
    OrchardRenderer* renderer; // Borrowed; main's renderer outlives this bridge.
    std::exception_ptr failure;
    bool closed;
    std::size_t resize_events;
}
@end

@implementation OrchardBridge
- (void)drawInMTKView:(MTKView*)view {
    try {
        if (failure || !renderer->has_capacity()) return;
        MTLRenderPassDescriptor* pass = view.currentRenderPassDescriptor;
        id<CAMetalDrawable> drawable = view.currentDrawable;
        if (!pass || !drawable) return;
        renderer->draw(static_cast<MTL::RenderPassDescriptor*>((__bridge void*)pass),
            static_cast<CA::MetalDrawable*>((__bridge void*)drawable));
    } catch (...) {
        failure = std::current_exception(); // Never unwind through an Objective-C callback.
    }
}
- (void)mtkView:(MTKView*)view drawableSizeWillChange:(CGSize)size {
    (void)view;
    try {
        ++resize_events;
        std::cout << "drawable pixels: " << size.width << 'x' << size.height << '\n';
    } catch (...) {
        failure = std::current_exception();
    }
}
- (BOOL)windowShouldClose:(NSWindow*)sender {
    (void)sender;
    closed = true;
    return YES;
}
@end

int main(int argc, const char* argv[]) {
    @autoreleasepool {
        try {
            const auto config = options(argc, argv);
            [NSApplication sharedApplication];
            [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
            id<MTLDevice> device = MTLCreateSystemDefaultDevice();
            if (!device) throw std::runtime_error("No Metal device; run in a graphical macOS session");
            OrchardRenderer renderer(static_cast<MTL::Device*>((__bridge void*)device), config.verify);
            OrchardBridge* bridge = [OrchardBridge new];
            bridge->renderer = &renderer;
            NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 500)
                styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
                    NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable)
                backing:NSBackingStoreBuffered defer:NO];
            if (!window) throw std::runtime_error("NSWindow creation failed");
            window.releasedWhenClosed = NO;
            window.title = @"Parallax · Copper Orchard · clear-color checkpoint";
            window.delegate = bridge;
            MTKView* view = [[MTKView alloc] initWithFrame:window.contentView.bounds device:device];
            if (!view) throw std::runtime_error("MTKView creation failed");
            view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
            view.colorPixelFormat = MTLPixelFormatBGRA8Unorm;
            view.framebufferOnly = !config.verify; // Only the diagnostic path copies drawable pixels.
            view.paused = YES;
            view.enableSetNeedsDisplay = NO;
            view.delegate = bridge;
            window.contentView = view;
            [window center];
            [window makeKeyAndOrderFront:nil];
            [NSApp finishLaunching];
            [NSApp activateIgnoringOtherApps:YES];

            // A single main-thread pump drives Cocoa and MTKView explicitly. This
            // checkpoint has no concurrent callbacks and no captured renderer owner.
            const auto start = std::chrono::steady_clock::now();
            bool resized{};
            while (!bridge->closed) {
                @autoreleasepool {
                    NSEvent* event = [NSApp nextEventMatchingMask:NSEventMaskAny
                        untilDate:[NSDate dateWithTimeIntervalSinceNow:0.005]
                        inMode:NSDefaultRunLoopMode dequeue:YES];
                    if (event) [NSApp sendEvent:event];
                    if (bridge->closed) break;
                    if (config.frames && renderer.submitted() >= *config.frames) break;
                    if (config.fail_after && renderer.submitted() >= *config.fail_after)
                        throw std::runtime_error("injected host failure; scoped renderer drains pending work");
                    if (config.resize && !resized && renderer.submitted() >= 2) {
                        [window setContentSize:NSMakeSize(960, 540)];
                        resized = true;
                    }
                    if (!window.miniaturized && view.drawableSize.width > 0 && view.drawableSize.height > 0)
                        [view draw];
                    if (bridge->failure) std::rethrow_exception(bridge->failure);
                    if (config.frames && std::chrono::steady_clock::now() - start > std::chrono::seconds(30))
                        throw std::runtime_error("smoke run timed out without the requested frames");
                }
            }
            view.delegate = nil; // Stop callbacks before draining and destroying C++ owners.
            window.delegate = nil;
            renderer.finish();
            if (config.frames && renderer.submitted() != *config.frames)
                throw std::runtime_error("window closed before the requested frame count");
            std::cout << "submitted=" << renderer.submitted() << " verified=" << renderer.verified()
                << " resize-events=" << bridge->resize_events << " shutdown=drained\n";
            [window orderOut:nil];
            return 0;
        } catch (const std::exception& error) {
            std::cerr << "orchard-window: " << error.what() << '\n';
            return 1;
        }
    }
}

This is the file included with the course. Copy and download give you its complete source.

Taking your work offline? Download all course companions