#import #import #include "window_renderer.hpp" #include #include #include #include #include #include #include namespace { struct Options { std::optional frames; std::optional 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 { @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 drawable = view.currentDrawable; if (!pass || !drawable) return; renderer->draw(static_cast((__bridge void*)pass), static_cast((__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 device = MTLCreateSystemDefaultDevice(); if (!device) throw std::runtime_error("No Metal device; run in a graphical macOS session"); OrchardRenderer renderer(static_cast((__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; } } }