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.
// 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
