Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dm/host/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ include ../mac/min_os.mk
# hle_bigstack.c's inline-assembly entry apart from its return path (SIGBUS in dm_bigstack_enter on Breath of the Wild, PR #556)
CFLAGS=-O2 -g -Wall -Wno-unused-function -mmacosx-version-min=$(MACOS_MIN)
# Service-layer takeover hle_*.c files are included by wildcard: adding a file does not touch this line (avoids merge conflicts)
SRC=main.c loader.c hle.c crash.c probe.c thread.c save_sync.c memstat.m haptics_apple.m pad_switch2_apple.m $(sort $(wildcard hle_*.c)) front_mac.m web_apple.m entry.S
SRC=main.c loader.c hle.c crash.c probe.c thread.c save_sync.c memstat.m haptics_apple.m pad_switch2_apple.m switch2_ops_apple.m $(sort $(wildcard hle_*.c)) front_mac.m sw2_panel_mac.m web_apple.m entry.S
# NVN null device + nn::vi + the Vulkan layer (dm/gfx/)
# Sources under dm/gfx, dm/gfx/nvn, dm/gfx/tex, dm/gfx/vk and dm/gfx/gl are included by wildcard (tests/ and metal/ do not go into the host): adding a file does not touch this line
GFX=$(sort $(wildcard ../gfx/*.c ../gfx/nvn/*.c ../gfx/nvn/*.m ../gfx/nvn/*.S ../gfx/tex/*.c ../gfx/tex/*.m ../gfx/vk/*.m ../gfx/gl/*.m))
Expand Down Expand Up @@ -79,7 +79,7 @@ endif
.PHONY: FORCE
FORCE:
# Makefile: a change to CFLAGS (or the compile line) rebuilds the host
dmhost: Makefile $(SRC) $(GFX) ../third_party/stb_vorbis/stb_vorbis.c dm.h hle.h romfs_image.h save_sync.h dm_input.h pad_slots.h hle_haptics.h haptics_apple.h pad_switch2_apple.h switch2_proto.h motion_fusion.h motion_apple.h motion_pro.h motion_pro_mac.h pad_apple.h pad_hid.h pad_hid_mac.h home_quit.h home_quit_mac.h ctrl_prompt.h front.h $(GFX_H) $(GAME_H) $(GAME_STAMP) $(VPX_LIB) $(DAV1D_LIB) $(MSL_H)
dmhost: Makefile $(SRC) $(GFX) ../third_party/stb_vorbis/stb_vorbis.c dm.h hle.h romfs_image.h save_sync.h dm_input.h pad_slots.h hle_haptics.h haptics_apple.h pad_switch2_apple.h sw2_panel_mac.h switch2_ops_apple.h switch2_proto.h motion_fusion.h motion_apple.h motion_pro.h motion_pro_mac.h pad_apple.h pad_hid.h pad_hid_mac.h home_quit.h home_quit_mac.h ctrl_prompt.h front.h $(GFX_H) $(GAME_H) $(GAME_STAMP) $(VPX_LIB) $(DAV1D_LIB) $(MSL_H)
clang $(CFLAGS) -I../gfx/nvn/include -I$(dir $(MSL_H)) -I../third_party/vulkan-headers/include -I$(dir $(GAME_H)) -I$(VPX) -I$(DAV1D)/include -I../third_party/stb_vorbis -fobjc-arc -o $@ $(SRC) $(GFX) $(VPX_LIB) $(DAV1D_LIB) -framework Cocoa -framework WebKit -framework UserNotifications -framework QuartzCore -framework Metal -framework MetalFX -framework GameController -framework CoreBluetooth -framework IOBluetooth -framework CoreHaptics -framework IOKit -framework AudioToolbox -framework VideoToolbox -framework CoreMedia -framework CoreVideo -framework ImageIO -framework UniformTypeIdentifiers
clean: # the host and everything make generates for it (gen/: the profile headers, the game stamp and the libvpx and dav1d builds; dmhost_* and *.dSYM beside them, as in .gitignore)
rm -rf gen dmhost dmhost_* *.dSYM
Expand Down
63 changes: 60 additions & 3 deletions dm/host/front_mac.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// Cheats (hle_cheats.c): the Cheats menu's rows toggle, reload (⌘R) and open the cheats folder; its first item "Show Cheats
// Window" (⌘⇧C, or DM_CHEATS_WINDOW=1 at launch) opens a Cheats panel beside the game: sections, search, the master switch,
// safe mode, values, details and the import of cheat files (see the code below).
// Controller (sw2_panel_mac.h): Show Controller Window (⌘⇧P, or DM_CONTROLLER_WINDOW=1 at launch) opens the Switch 2 stick tester beside the game, and the menu lists the
// connected Switch 2 controllers. While it is open the game gets no input from them; a quit waits for a calibration save it started (DmAppDelegate).
// Key map (Nintendo layout): WASD left stick, arrow keys D-pad (with Shift held: the right stick, for the camera), J=A K=B U=X I=Y, Q=L E=R, Z=ZL C=ZR, F=press left stick G=press right stick, Return=+ Tab=−, Space=shake the controller.
// Controllers: face buttons as on the iPhone, by position (right A, bottom B, top X, left Y) or on a Nintendo pad by its labels (pad_apple.h), shoulders L R, triggers ZL ZR, Menu + Options −, both sticks and their presses.
// Each controller is a player of its own (local co-op): the first one connected is player 1, together with the keyboard, the next player 2, and so on up to 8 (pad_slots.h);
Expand All @@ -24,6 +26,7 @@
#include "pad_apple.h"
#include "pad_hid_mac.h"
#include "pad_switch2_apple.h"
#include "sw2_panel_mac.h"
static int s2pad_player(s2_pad *d); // the Switch 2 pads' player slots (below, beside the raw HID pads')
static void s2pad_read(s2_pad *d);
static int g_merge_s2_rumble; // DM_PAD_MERGE: player 1's rumble goes to the Switch 2 pads (merge_rumble_to_s2)
Expand Down Expand Up @@ -399,10 +402,12 @@ static void merge_rumble_to_s2(int on) {
if (!g_merge || g_merge_s2_rumble == on) return;
g_merge_s2_rumble = on; dm_haptics_apple_set_sink(0, on ? dm_switch2_rumble : NULL);
}
static int s2pad_blocked(void) { return !g_input_on || hq_blocks_input(g_hq.phase) || cp_blocks_input(g_cp.phase); } // no input to the game (focus lost, the Home or controller prompt up): its Joy-Con states are not sent (pad_switch2_apple.m)
// No input to the game (focus lost, the Home or controller prompt up, the Controller panel open): its Joy-Con states and sensors are not sent (pad_switch2_apple.m)
static int s2pad_blocked(void) { return !g_input_on || hq_blocks_input(g_hq.phase) || cp_blocks_input(g_cp.phase) || sw2_panel_open(); }
static void s2pad_read(s2_pad *d) {
hq_poll(); // Home and the prompt's A and B come from these pads too
if (!g_input_on || hq_blocks_input(g_hq.phase)) return;
// The Controller panel has the sticks: circles drawn there never move a character. The Joy-Con gestures (split a pair, join two) still act (pad_switch2_apple.m gesture:)
if (!g_input_on || hq_blocks_input(g_hq.phase) || sw2_panel_open()) return;
int p = s2pad_player(d);
uint64_t m; int32_t s[4]; if (!s2_pad_input(&d->st, &m, s)) return;
if (p < 0) { // DM_PAD_PRESS_TO_JOIN=1: joins at its first button
Expand All @@ -413,6 +418,16 @@ static void s2pad_read(s2_pad *d) {
if (m) merge_rumble_to_s2(1);
pad_set(p, m, s[0], s[1], s[2], s[3]);
}
// The Controller panel opened: what the Switch 2 pads hold goes neutral once (buttons, sticks, each Joy-Con's own state and both sensors), as focus loss releases it; while the
// panel is open nothing more is sent, and once it closes the next report sends the pad as it is. With DM_PAD_MERGE player 1 is shared, so a GameController or HID pad on it
// reads neutral too until its next change, as after focus loss
static void s2pads_release(void) {
for (s2_pad *d = dm_switch2_pads(); d; d = d->next) {
int p = s2pad_player(d); if (p < 0) continue;
dm_pad_set_player(p, 0, 0, 0, 0, 0); dm_joy_set(p, 0, 0, 0, 0); dm_joy_set(p, 1, 0, 0, 0);
dm_motion_clear_hid(p); dm_motion_clear_joy(p, 0); dm_motion_clear_joy(p, 1);
}
}
static int s2pad_attach(s2_pad *d) { // 1: kept (a player, or waiting for its first button press); 0: no free player (pad_switch2_apple.h)
if (g_press_to_join && !g_merge) { LOG("controller '%s' (Bluetooth) connected: press a button to play (DM_PAD_PRESS_TO_JOIN)", d->name); cp_poll(); return 1; }
int p = g_merge ? 0 : pad_slot_take(&g_slots, d, d->name); char nb[16];
Expand Down Expand Up @@ -646,11 +661,47 @@ - (void)setFrameSize:(NSSize)s { [super setFrameSize:s]; [self fitDrawable]; }
- (void)viewDidChangeBackingProperties { [super viewDidChangeBackingProperties]; [self fitDrawable]; }
@end

// A quit while a Switch 2 controller's calibration save runs waits for it to end, at most 30 s, since stopping between the erase and the write-back would leave the
// controller's calibration sector blank or half written (pad_switch2_apple.h, dm_switch2_saving): ⌘Q, the Home button's quit prompt and a logout (DmAppDelegate, through
// terminate:) and the game window's close button (DmWinDelegate). One wait serves them all and ends once: AppKit's pending reply when a terminate: is among them, else exit(0)
// as the close button does. It polls every 0.1 s on the main queue (served while AppKit waits for the reply) rather than through the session's hook, which has one observer,
// the Controller panel. The guest thread's own end, a lost GPU, the game's exit and SIGTERM, SIGINT and SIGHUP (a kill, a Ctrl-C in the terminal that started the app, a session end:
// the launcher forwards them and dm_host_shutdown ends the host within 1.5 s) still end the process at once, and a save then cut off leaves the sector blank or partly written, each block
// whole or blank, with the backup file on disk (the log names it)
enum { QUIT_REPLY = 1, QUIT_EXIT = 2 };
static int g_quit_wait; // the QUIT_ bits of what waits for the save
static void quit_wait(double until) {
if (dm_switch2_saving() && CACurrentMediaTime() < until) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC / 10), dispatch_get_main_queue(), ^{ quit_wait(until); });
return;
}
LOG("front end: %s", dm_switch2_saving() ? "the Switch 2 calibration save still runs after 30 s: quitting anyway" : "the Switch 2 calibration save ended: quitting");
if (g_quit_wait & QUIT_REPLY) [NSApp replyToApplicationShouldTerminate:YES]; else exit(0);
}
static void quit_after_save(int how) {
int first = !g_quit_wait; g_quit_wait |= how;
if (!first) return; // a second quit while waiting: the first wait answers for both
LOG("front end: quitting waits for a Switch 2 controller's calibration save to end (at most 30 s)");
quit_wait(CACurrentMediaTime() + 30);
}
@interface DmAppDelegate : NSObject <NSApplicationDelegate> @end
@implementation DmAppDelegate
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)app {
(void)app;
if (!g_quit_wait && !dm_switch2_saving()) return NSTerminateNow;
quit_after_save(QUIT_REPLY);
return NSTerminateLater;
}
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app { (void)app; return YES; } // AppKit asks a delegate this and logs a warning without it; no window is restored
@end

// AppKit reports a refused fullscreen request only to the window delegate (there is no notification for it)
static void (^g_fs_failed)(void);
@interface DmWinDelegate : NSObject <NSWindowDelegate> @end
@implementation DmWinDelegate
- (void)windowDidFailToEnterFullScreen:(NSWindow *)w { (void)w; if (g_fs_failed) g_fs_failed(); }
// The close button ends the process (NSWindowWillCloseNotification → exit(0)); during a Switch 2 calibration save the window stays and the process ends once the save has
- (BOOL)windowShouldClose:(NSWindow *)w { (void)w; if (!g_quit_wait && !dm_switch2_saving()) return YES; quit_after_save(QUIT_EXIT); return NO; }
@end

typedef struct { void (*fn)(void); } guest_arg;
Expand Down Expand Up @@ -1561,14 +1612,20 @@ void front_run(void (*guest)(void)) {
[[bar addItemWithTitle:@"Cheats" action:nil keyEquivalent:@""] setSubmenu:cm];
if (getenv("DM_CHEATS_WINDOW") && atoi(getenv("DM_CHEATS_WINDOW"))) // the Cheats panel open from the start (beside the game, not key)
dispatch_async(dispatch_get_main_queue(), ^{ [g_cheats_menu showPanel:nil]; });
// Controller (sw2_panel_mac.h): the Switch 2 stick tester and the controllers connected, between Cheats and Window
sw2_panel_setup(win, s2pads_release);
[[bar addItemWithTitle:@"Controller" action:nil keyEquivalent:@""] setSubmenu:sw2_panel_menu()];
if (getenv("DM_CONTROLLER_WINDOW") && atoi(getenv("DM_CONTROLLER_WINDOW"))) // the Controller panel open from the start (beside the game, not key)
dispatch_async(dispatch_get_main_queue(), ^{ sw2_panel_show(); });
[[bar addItemWithTitle:@"Window" action:nil keyEquivalent:@""] setSubmenu:wm];
[wm addItemWithTitle:@"Enter Full Screen" action:@selector(toggleFullScreen:) keyEquivalent:@"f"].keyEquivalentModifierMask = NSEventModifierFlagControl | NSEventModifierFlagCommand;
NSApp.mainMenu = bar;
static DmAppDelegate *ad; ad = [DmAppDelegate new]; NSApp.delegate = ad; // NSApp and the window keep their delegates weakly
static DmWinDelegate *wd; wd = [DmWinDelegate new]; win.delegate = wd; // the close button's wait for a save, and fullscreen's refusals below
const char *fs = getenv("DM_FULLSCREEN"); // 1 (or any value but 0): start in fullscreen; 0, the settings window's Window, or unset: a window
if (fs && strcmp(fs, "0")) {
// A game opened from a fullscreen app starts while macOS leaves that Space. Wait for activation and for the window to reach the active Space before asking for fullscreen
// (whichever happens last), and repeat a refused startup request once. Only a request made here owns a retry; a later player request must never restart startup handling
static DmWinDelegate *wd; wd = [DmWinDelegate new]; win.delegate = wd;
__block int want = 1, retried = 0, entering = 0;
void (^enter)(void) = ^{
if (!want || (win.styleMask & NSWindowStyleMaskFullScreen) || !NSApp.isActive || !win.isOnActiveSpace) return;
Expand Down
23 changes: 23 additions & 0 deletions dm/host/pad_switch2_apple.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// DM_SW2_HOST=aa:bb:cc:dd:ee:ff names this Mac's Bluetooth address when it cannot be read; DM_SW2_VERBOSE=1 logs the first report of each second; DM_SW2_MAP=a,b,c,d,e,f and
// DM_SW2_GYRO_DPS=<degrees per second per count> adjust the motion axes and scale (switch2_proto.h s2_motion). On a Pro Controller 2, pairing, buttons, sticks and Home are checked on hardware; its motion map and scale are not measured.
#pragma once
#import "switch2_ops_apple.h" // S2CalOutcome, NSString
#include "switch2_proto.h"
#include "hle_haptics.h"

Expand All @@ -39,3 +40,25 @@ s2_pad *dm_switch2_pads(void); // the pads, a list thro
// pad's controllers as s2_rumble_pick says: every value the game sets plays once, in its own 5 ms sample (switch2_proto.h, Rumble timing), three to a packet every 15 ms while they
// buzz, then a few packets of silence (a write without response can be lost).
void dm_switch2_rumble(const dm_haptic_state *state);

// The stick tester's side (the Controller panel): what the controllers' sessions hold and the one write they allow. All calls come on the main queue.
typedef struct {
uint32_t id; // this connection's id: a reconnect gets a new one
uint16_t pid; // S2_PID_PRO, S2_PID_JOYCON_L, S2_PID_JOYCON_R
const char *kind; // "Pro Controller 2", "Joy-Con 2 (L)", "Joy-Con 2 (R)"
char serial[15];
int player; // 0-based, -1 without one
s2_cal cal[2]; // what the game uses now: the user calibration, else the factory one
s2_cal factory[2]; // the factory blocks; a copy of cal[i] when the factory block did not read
uint16_t stick[2][2]; // the latest raw 12-bit x, y of each stick (index 1 only on a Pro Controller 2)
uint32_t clock_ms; // the controller's clock at that report (the s2_clock milliseconds the session already keeps)
int saving; // a save on it is running
} dm_sw2_info;
int dm_switch2_on(void); // dm_switch2_start returned 1
int dm_switch2_controllers(dm_sw2_info *out, int max); // connected controllers whose handshake finished, oldest connection first, at most max; returns the count
// The tester's hooks (one observer; NULL blocks remove it): changed = a controller came, went, finished its handshake or a save began or ended; report = a new input report of controller id
void dm_switch2_observe(void (^changed)(void), void (^report)(uint32_t id));
// cal as controller id's user calibration of slot (s2op_save_user_cal with s2op_backup_dir()). Refused at once (S2CalNoBackup, nil path, a log line) for a slot the controller has no stick in (1 only on a Pro Controller 2), an unknown id, a save already running on it,
// or an invalid cal. After any other outcome the session re-reads that slot's calibration (user, else factory) into the pad the game reads before done runs. done on the main queue, after this returns
void dm_switch2_save(uint32_t id, int slot, s2_cal cal, void (^done)(S2CalOutcome outcome, NSString *backupPath));
int dm_switch2_saving(void); // some save is running (the quit wait)
Loading
Loading