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

Retain a frame until completion

A metal-cpp clear renderer with three frame slots, diagnostic readback, and drained cleanup.

132 lines window_renderer.cpp
// Generate Apple's implementation once in this executable, not in the bridge.
#define NS_PRIVATE_IMPLEMENTATION
#define MTL_PRIVATE_IMPLEMENTATION
#define CA_PRIVATE_IMPLEMENTATION
#include <Foundation/Foundation.hpp>
#include <Metal/Metal.hpp>
#include <QuartzCore/QuartzCore.hpp>

#include "window_renderer.hpp"

#include <array>
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>

namespace {
// Own exactly one Cocoa +1 reference. unique_ptr calls this deleter only for
// non-null pointers; no C++ member call is made through a null pointer.
// The pointee may still be retained by Metal or other framework owners.
template<class T> struct Release {
    void operator()(T* object) const noexcept { object->release(); }
};
template<class T> using MetalOwner = std::unique_ptr<T, Release<T>>;
template<class T> MetalOwner<T> retain(T* borrowed) noexcept {
    if (borrowed) borrowed->retain();
    return MetalOwner<T>(borrowed);
}
}

struct OrchardRenderer::Impl {
    struct Slot {
        MetalOwner<MTL::CommandBuffer> commands;
        MetalOwner<MTL::Buffer> readback;
    };

    MetalOwner<MTL::Device> device;
    MetalOwner<MTL::CommandQueue> queue;
    std::array<Slot, 3> slots;
    std::size_t frames{};
    std::size_t verified_frames{};
    bool verify{};

    Impl(MTL::Device* borrowed_device, bool verify_pixels)
        : device(retain(borrowed_device)), verify(verify_pixels) {
        if (!device) throw std::runtime_error("No Metal device");
        queue.reset(device->newCommandQueue()); // Adopt the new-returned +1 reference.
        if (!queue) throw std::runtime_error("newCommandQueue failed");
        if (verify) {
            for (auto& slot : slots) {
                // One pixel with an explicitly aligned row; kept until completion.
                slot.readback.reset(device->newBuffer(256, MTL::ResourceStorageModeShared));
                if (!slot.readback) throw std::runtime_error("readback allocation failed");
            }
        }
        std::cout << "Metal device: " << device->name()->utf8String() << '\n';
    }

    ~Impl() {
        // Covers early exceptions as well as normal exit. No callbacks capture us.
        // Normal rendering polls completed slots instead of blocking each frame.
        for (auto& slot : slots) {
            if (slot.commands) slot.commands->waitUntilCompleted();
        }
    }

    bool reclaim(Slot& slot, bool wait) {
        if (!slot.commands) return true;
        if (wait) slot.commands->waitUntilCompleted();
        const auto status = slot.commands->status();
        if (status == MTL::CommandBufferStatusError) {
            const auto* error = slot.commands->error();
            throw std::runtime_error(std::string("Metal command failed: ") +
                (error ? error->localizedDescription()->utf8String() : "no diagnostic"));
        }
        if (status != MTL::CommandBufferStatusCompleted) return false;
        if (verify) {
            const auto* pixel = static_cast<const std::uint8_t*>(slot.readback->contents());
            // BGRA8Unorm, clear RGBA=(.125,.25,.5,1). Allow one quantization unit.
            constexpr std::array<int, 4> expected{128, 64, 32, 255};
            for (std::size_t channel = 0; channel < expected.size(); ++channel) {
                const int delta = static_cast<int>(pixel[channel]) - expected[channel];
                if (delta < -1 || delta > 1) throw std::runtime_error("clear-color pixel oracle failed");
            }
            ++verified_frames;
        }
        slot.commands.reset();
        return true;
    }
};

OrchardRenderer::OrchardRenderer(MTL::Device* device, bool verify_pixels)
    : impl_(std::make_unique<Impl>(device, verify_pixels)) {}
OrchardRenderer::~OrchardRenderer() = default;

bool OrchardRenderer::has_capacity() {
    return impl_->reclaim(impl_->slots[impl_->frames % impl_->slots.size()], false);
}

void OrchardRenderer::draw(MTL::RenderPassDescriptor* pass, CA::MetalDrawable* drawable) {
    if (!pass || !drawable) return; // Drawable scarcity is an ordinary skipped frame.
    if (!has_capacity()) return;
    auto& slot = impl_->slots[impl_->frames % impl_->slots.size()];
    auto commands = retain(impl_->queue->commandBuffer()); // Acquire +1 from an autoreleased result.
    if (!commands) throw std::runtime_error("commandBuffer failed");
    auto* attachment = pass->colorAttachments()->object(0);
    attachment->setLoadAction(MTL::LoadActionClear);
    attachment->setStoreAction(MTL::StoreActionStore);
    attachment->setClearColor(MTL::ClearColor::Make(0.125, 0.25, 0.5, 1.0));
    auto* encoder = commands->renderCommandEncoder(pass);
    if (!encoder) throw std::runtime_error("renderCommandEncoder failed");
    encoder->endEncoding(); // Clearing attachments does not require a shader pipeline.
    if (impl_->verify) {
        auto* blit = commands->blitCommandEncoder();
        if (!blit) throw std::runtime_error("blitCommandEncoder failed");
        blit->copyFromTexture(drawable->texture(), 0, 0, MTL::Origin(0, 0, 0),
            MTL::Size(1, 1, 1), slot.readback.get(), 0, 256, 256);
        blit->endEncoding();
    }
    commands->presentDrawable(drawable);
    slot.commands = std::move(commands); // Transfer the owned reference before commit.
    slot.commands->commit(); // No throwing operations between commit and recording the submission.
    ++impl_->frames;
}

void OrchardRenderer::finish() {
    for (auto& slot : impl_->slots) static_cast<void>(impl_->reclaim(slot, true));
}
std::size_t OrchardRenderer::submitted() const noexcept { return impl_->frames; }
std::size_t OrchardRenderer::verified() const noexcept { return impl_->verified_frames; }

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

Taking your work offline? Download all course companions