SDL/Cocoa embedding

Hello,

I’ve been trying in vain to integrate an SDL renderer into an existing Cocoa window. I’m struggling to find information in the SDL3 documentation.

The programme runs without a hitch but displays two windows. This makes sense, as the programme is supposed to create two, as the code below shows. However, it must also embed one within the other.

#include "SDL3/SDL_error.h"
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_init.h"
#include "SDL3/SDL_log.h"
#include "SDL3/SDL_properties.h"
#include "SDL3/SDL_render.h"
#include "SDL3/SDL_video.h"
#import <Cocoa/Cocoa.h>
#import <CoreGraphics/CoreGraphics.h>
#include <Foundation/Foundation.h>
#include <cstddef>
#include <cstdlib>

#include <SDL3/SDL.h>

extern "C" void sdl_render_frame(SDL_Renderer *renderer, void *user);

static bool g_running = false;

static SDL_Window *g_sdlWindow = nullptr;
static SDL_Renderer *g_sdlRenderer = nullptr;

@interface BridgeView : NSView
@property(nonatomic, assign) NSTimer *sdlTimer;
@end

@implementation BridgeView
- (void)dealloc {
  if (_sdlTimer) {
    [_sdlTimer invalidate];
    _sdlTimer = nil;
  }
  [super dealloc];
}
- (void)viewDidMoveToWindow {
  [super viewDidMoveToWindow];
  if (!self.sdlTimer) {
    self.sdlTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 120.0)
                                                     target:self
                                                   selector:@selector(sdlTick:)
                                                   userInfo:nil
                                                    repeats:YES];
  }
}
- (void)sdlTick:(NSTimer *)t {
  SDL_Event ev;
  while (SDL_PollEvent(&ev)) {
    if (ev.type == SDL_EVENT_QUIT) {
      g_running = false;
      [NSApp terminate:nil];
      return;
    }
  }
  if (g_sdlRenderer) {
    SDL_SetRenderDrawColor(g_sdlRenderer, 50, 50, 50, 255);
    SDL_RenderClear(g_sdlRenderer);
    extern void sdl_render_frame(SDL_Renderer * renderer, void *user);
    sdl_render_frame(g_sdlRenderer, NULL);
    SDL_RenderPresent(g_sdlRenderer);
  }
}
@end

static SDL_PropertiesID make_propes_for_ns_view(void *nsViewPtr, int w, int h,
                                                const char *title) {
  SDL_PropertiesID props = SDL_CreateProperties();
  if (!props)
    return 0;
  SDL_SetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER,
                         nsViewPtr);
  SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, w);
  SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, h);
  if (title)
    SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, title);
  return props;
}

#pragma mark - Public API

extern "C" int app_bridge_init_and_run(int width, int height,
                                       const char *title) {
  @autoreleasepool {
    [NSApplication sharedApplication];
    [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];

    NSRect frame = NSMakeRect(0, 0, width, height);
    NSUInteger style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
                       NSWindowStyleMaskResizable |
                       NSWindowStyleMaskMiniaturizable;
    NSWindow *win = [[NSWindow alloc] initWithContentRect:frame
                                                styleMask:style
                                                  backing:NSBackingStoreBuffered
                                                    defer:NO];
    if (title)
      [win setTitle:[NSString stringWithUTF8String:title]];
    [win center];

    NSRect contentFrame = NSMakeRect(0, 0, width, height);
    BridgeView *view = [[BridgeView alloc] initWithFrame:contentFrame];
    view.wantsLayer = YES;
    view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;

    NSView *content = [win contentView];
    [content addSubview:view];

    [win makeKeyAndOrderFront:nil];
    [NSApp activateIgnoringOtherApps:YES];

    if (SDL_Init(SDL_INIT_EVENTS | SDL_INIT_VIDEO) != 1) {
      NSLog(@"SDL_Init failed: %s", SDL_GetError());
      return EXIT_FAILURE;
    }

    void *nativeViewPtr = (__bridge void *)view;
    SDL_PropertiesID props =
        make_propes_for_ns_view(nativeViewPtr, width, height, title);
    if (!props) {
      NSLog(@"Failed to create SDL props");
      SDL_Quit();
      return EXIT_FAILURE;
    }

    g_sdlWindow = SDL_CreateWindowWithProperties(props);
    SDL_DestroyProperties(props);
    if (!g_sdlWindow) {
      NSLog(@"SDL_CreateWindowWithProperties failed: %s", SDL_GetError());
      SDL_Quit();
      return EXIT_FAILURE;
    }

    g_sdlRenderer = SDL_CreateRenderer(g_sdlWindow, title);

    [NSApp run];

    if (g_sdlRenderer) {
      SDL_DestroyRenderer(g_sdlRenderer);
      g_sdlRenderer = NULL;
    }
    if (g_sdlWindow) {
      SDL_DestroyWindow(g_sdlWindow);
      g_sdlWindow = NULL;
    }

    SDL_Quit();
    return EXIT_SUCCESS;
  }
}

extern "C" void request_redraw() {
  @autoreleasepool {
    NSWindow *win = [NSApp mainWindow];
    if (!win)
      return;
    NSView *v = [win contentView];
    if (v)
      [v setNeedsDisplay:YES];
  }
}

Could you help me?

I’m not familiar with Cocoa so I’m not sure but the docs seem to be saying you should pass the window …

SDL_PROP_WINDOW_COCOA_WINDOW_POINTER: the (__unsafe_unretained) NSWindow associated with the window

… but it looks like you’re passing a BridgeView/NSView instead.