Mac, in fullscreen: a screen whose refresh rate is not a multiple of the game's frame rate switches to one that is (144 → 120 Hz), from PR #79
60 fps on a 144 Hz screen cannot be paced evenly: the display link counts a tick on the refresh nearest each 1/60 s slot (link_range's virtual ticks), so frames stay up two and three refreshes in turn (13.9 / 20.8 ms) and Apple's HUD shows the frame interval jumping between them. link_sync now tells the front end the link's rate (60, 30, or 120 with DM_PRESENT_LINK=2; front_screen_match), and in fullscreen front_mac.m switches a screen whose mode is not a multiple of it to the mode of the same size with the highest rate that is, with kCGConfigureForAppOnly, so macOS switches it back when the host quits; leaving fullscreen or that screen switches it back at once. A window keeps the screen's rate and the virtual ticks: the switch changes the whole screen for every app and most external screens go black for a second or more. DM_SCREEN_MATCH=0 keeps the rate in fullscreen too. Changes to the PR: the switch only in fullscreen, with the way back on leaving it (the PR switched in a window too); no switch without a window (the windowless host's main thread runs the guest); the rate to match is atomic; the link takes a rate change only once its object exists, so one made while its thread starts is not lost, and sets the range directly instead of logging a pacing mode -1; the screen this host switched takes its rate from its mode, which maximumFramesPerSecond may not show yet. The link now also sets its range again when the window moves to another screen, which kept the first screen's range before. Checked on this Mac's built-in 120 Hz screen (a multiple of every link rate) with a harness that drives front_mac.m asked for 50 fps: no switch in the window, 120 → 50 Hz in fullscreen, 120 Hz again on leaving it, and 120 Hz after quitting in fullscreen; at 60 fps and with DM_SCREEN_MATCH=0 nothing switched. Hades windowed and fullscreen (DM_FULLSCREEN=1) paced at 60 as before, the unchanged host alike. nvn_mtl.m passes an iOS SDK syntax check. The link's new range after a switch has not run here (it needs a 144 or 165 Hz screen); the PR's author measured the windowed switch on a 144 Hz XB273U at 2560x1440: 120 Hz, the HUD's frame interval steady at 16.7 ms, 144 Hz again after quitting. Co-authored-by: v0lumehi <34629096+v0lumehi@users.noreply.github.com>
This commit is contained in:
1 parent
86d9ba1239
commit
a6787033eb
5 files changed
+75
-6
No files matched your search
@@ -8264,8 +8264,10 @@ static int g_link_owns_layer, g_classic_busy; // Under g_pq_mu: nextDrawable t
|
||||
static int link_on(void) { return g_dm_present_link && g_link != nil; }
|
||||
// Screens whose refresh rate is not a multiple of the link's (144 or 165 Hz against 60 or 30): asked for 60 Hz, the system fires the link on every third refresh of a 165 Hz screen,
|
||||
// 55 Hz, and a game paced by the link runs at 55 fps (27.5 at 30). There the link runs at the screen's rate and only the refreshes nearest a steady clock of the wanted rate count as
|
||||
// ticks (g_link_vperiod, NvMetalDisplayLinkDelegate): 60 a second on average, each frame shown on the refreshes nearest its 1/60 s slot. DM_LINK_VTICK=0 keeps the system's choice
|
||||
// ticks (g_link_vperiod, NvMetalDisplayLinkDelegate): 60 a second on average, each frame shown on the refreshes nearest its 1/60 s slot. DM_LINK_VTICK=0 keeps the system's choice.
|
||||
// In fullscreen the Mac front end switches such a screen to a rate that is a multiple where it has one (front_screen_match), and the range is set again for it (link_sync)
|
||||
__attribute__((weak)) int front_screen_max_fps(void) { return 0; } // front_mac.m; graphics unit tests and iOS have none
|
||||
__attribute__((weak)) void front_screen_match(int fps) { (void)fps; } // front_mac.m; graphics unit tests and iOS have none
|
||||
static CAFrameRateRange link_range(int mode) {
|
||||
static int on = -1; if (on < 0) { const char *e = getenv("DM_LINK_VTICK"); on = !(e && !strcmp(e, "0")); }
|
||||
int rate = mode == 3 ? 30 : mode == 2 ? 120 : 60, hz = on ? front_screen_max_fps() : 0;
|
||||
@@ -8299,6 +8301,11 @@ static void link_sync(void) { // Called every frame by the game's present thre
|
||||
static int applied; int want = g_dm_present_link;
|
||||
if (want < 0 || want > 2) { g_dm_present_link = applied == 3 ? 1 : applied; return; }
|
||||
if (want && mtl_fps_target() == 30) want = 3; // Power-saving mode: display link at 30 Hz
|
||||
if (want) front_screen_match(want == 3 ? 30 : want == 2 ? 120 : 60);
|
||||
// The screen's rate changed (switched by front_screen_match, or another screen): the range again, virtual ticks or none. Taken only once the link object exists, so a change
|
||||
// while its thread starts (after link_main read the rate) is not lost
|
||||
static int hz_was; int hz = front_screen_max_fps(); CAMetalDisplayLink *lk = g_link;
|
||||
if (lk && hz != hz_was) { hz_was = hz; if (want && want == applied) { lk.preferredFrameRateRange = link_range(want); MLOG("display link: the screen runs at %d Hz now, range set again (frame %llu)", hz, (unsigned long long)atomic_load(&g_cur_frame)); } }
|
||||
if (want == applied) return;
|
||||
g_link_mode = want;
|
||||
if (!g_link_started) {
|
||||
|
||||
@@ -6,6 +6,9 @@ void front_run(void (*guest)(void));
|
||||
void *front_metal_layer(void);
|
||||
// The highest refresh rate of the window's screen in Hz, 0 = not known (Mac front_mac.m; none on iOS, whose screens run at multiples of 60 Hz). Any thread
|
||||
int front_screen_max_fps(void);
|
||||
// The frame rate the display link paces the game at (60, 30, 120; nvn_mtl.m link_sync): in fullscreen, a screen whose refresh rate is not a multiple of it is switched, while the game runs,
|
||||
// to a mode of the same size whose rate is, and back when the window leaves fullscreen or that screen (front_mac.m; none on iOS). Called from the present thread
|
||||
void front_screen_match(int fps);
|
||||
#include <stdint.h>
|
||||
// Text input box (for the software keyboard swkbd): the main thread shows a native input box and the calling thread blocks until the user confirms / cancels.
|
||||
// All text is UTF-8; max_chars is the maximum character count (0 = unlimited, only a hint for the front end; truncation is done uniformly by the caller hle_swkbd.c in UTF-16 units).
|
||||
|
||||
+62
-4
@@ -12,6 +12,7 @@
|
||||
#include "front.h"
|
||||
#include "hle.h"
|
||||
#include <signal.h>
|
||||
#include <math.h>
|
||||
|
||||
static CAMetalLayer *g_layer;
|
||||
static uint64_t g_btn;
|
||||
@@ -20,6 +21,62 @@ static int g_w, g_a, g_s, g_d;
|
||||
void *front_metal_layer(void) { return (__bridge void *)g_layer; }
|
||||
static _Atomic int g_screen_fps; // the window's screen's maximumFramesPerSecond, kept up to date when the window moves to another screen
|
||||
int front_screen_max_fps(void) { return g_screen_fps; }
|
||||
// The screen's refresh rate matched to the game's frame rate in fullscreen (front_screen_match, from PR #79; DM_SCREEN_MATCH=0 turns it off). 60 fps on a 144 Hz screen cannot be paced
|
||||
// evenly: the display link counts a tick on the refresh nearest each 1/60 s slot (nvn_mtl.m link_range), so frames stay up 2 and 3 refreshes in turn (13.9 / 20.8 ms) and Apple's HUD shows
|
||||
// the frame interval jumping between them. In fullscreen, a screen whose rate is not a multiple of the game's switches to the mode of the same size with the highest rate that is (144 → 120 Hz
|
||||
// for 60 and 30 fps), for this app only: macOS switches it back when the host quits, crashes included, and leaving fullscreen or that screen switches it back at once. A window keeps the
|
||||
// screen's rate, because the switch changes the whole screen for every app and most external screens go black for a second or more. A screen with no such mode keeps the virtual ticks
|
||||
static NSWindow *g_win; static _Atomic int g_match_want; // the game's window; the frame rate to match (0 = not known yet), written by the present thread only
|
||||
static CGDirectDisplayID g_match_id; static CGDisplayModeRef g_match_orig; // main thread: the screen this host switched and its own mode (NULL = none switched)
|
||||
static double mode_hz(CGDisplayModeRef m) { double hz = CGDisplayModeGetRefreshRate(m); return hz > 0 ? hz : 0; } // 0 = not known (older built-in screens report none)
|
||||
static int hz_multiple(double hz, int fps) { double k = round(hz / fps); return k >= 1 && fabs(hz / fps - k) < 0.01; } // 59.94 Hz counts as 60
|
||||
static CGError screen_set_mode(CGDirectDisplayID id, CGDisplayModeRef m) { // for this app only (kCGConfigureForAppOnly)
|
||||
CGDisplayConfigRef cfg; CGError r = CGBeginDisplayConfiguration(&cfg);
|
||||
if (r != kCGErrorSuccess) return r;
|
||||
r = CGConfigureDisplayWithDisplayMode(cfg, id, m, NULL);
|
||||
if (r != kCGErrorSuccess) { CGCancelDisplayConfiguration(cfg); return r; }
|
||||
return CGCompleteDisplayConfiguration(cfg, kCGConfigureForAppOnly);
|
||||
}
|
||||
static void screen_match_restore(int on_it) { // main thread: the screen this host switched gets its own mode again; on_it: the window is on that screen
|
||||
double hz = mode_hz(g_match_orig); CGError r = screen_set_mode(g_match_id, g_match_orig);
|
||||
LOG("front end: screen %s %.0f Hz", r == kCGErrorSuccess ? "back at" : "could not be switched back to", hz);
|
||||
if (r == kCGErrorSuccess && on_it && hz > 0) g_screen_fps = (int)round(hz);
|
||||
CGDisplayModeRelease(g_match_orig); g_match_orig = NULL;
|
||||
}
|
||||
static void screen_match_apply(void) { // main thread: on a new frame rate, on entering and leaving fullscreen and on a move to another screen
|
||||
static int off = -1; if (off < 0) { const char *e = getenv("DM_SCREEN_MATCH"); off = e && !strcmp(e, "0"); if (off) LOG("front end: the screen keeps its refresh rate in fullscreen too (DM_SCREEN_MATCH=0)"); }
|
||||
NSScreen *scr = g_win.screen; int want = g_match_want, full = (g_win.styleMask & NSWindowStyleMaskFullScreen) != 0;
|
||||
CGDirectDisplayID id = scr ? [scr.deviceDescription[@"NSScreenNumber"] unsignedIntValue] : kCGNullDirectDisplay;
|
||||
if (g_match_orig && (!full || id != g_match_id)) screen_match_restore(id == g_match_id);
|
||||
if (off || !full || want <= 0 || !scr) return;
|
||||
CGDisplayModeRef cur = CGDisplayCopyDisplayMode(id);
|
||||
if (!cur) return;
|
||||
double hz = mode_hz(cur);
|
||||
if (hz <= 0 || hz_multiple(hz, want)) { // already a multiple, or not known
|
||||
if (g_match_orig && hz > 0) g_screen_fps = (int)round(hz); // the screen this host switched: its rate from the mode, which maximumFramesPerSecond (the screen observer) may not show yet
|
||||
CGDisplayModeRelease(cur); return;
|
||||
}
|
||||
size_t w = CGDisplayModeGetWidth(cur), h = CGDisplayModeGetHeight(cur), pw = CGDisplayModeGetPixelWidth(cur), ph = CGDisplayModeGetPixelHeight(cur);
|
||||
CFArrayRef modes = CGDisplayCopyAllDisplayModes(id, (__bridge CFDictionaryRef)@{(__bridge NSString *)kCGDisplayShowDuplicateLowResolutionModes: @YES});
|
||||
CGDisplayModeRef pick = NULL; double pick_hz = 0;
|
||||
for (CFIndex i = 0; modes && i < CFArrayGetCount(modes); i++) {
|
||||
CGDisplayModeRef m = (CGDisplayModeRef)CFArrayGetValueAtIndex(modes, i); double r = mode_hz(m);
|
||||
if (CGDisplayModeGetWidth(m) != w || CGDisplayModeGetHeight(m) != h || CGDisplayModeGetPixelWidth(m) != pw || CGDisplayModeGetPixelHeight(m) != ph || !CGDisplayModeIsUsableForDesktopGUI(m)) continue;
|
||||
if (hz_multiple(r, want) && r > pick_hz) { pick = m; pick_hz = r; }
|
||||
}
|
||||
if (!pick) LOG("front end: screen at %.0f Hz has no mode of this size at a multiple of %d Hz: frames are paced on the nearest refreshes", hz, want);
|
||||
else {
|
||||
CGError r = screen_set_mode(id, pick);
|
||||
LOG("front end: screen at %.0f Hz %s %.0f Hz for %d fps in fullscreen (DM_SCREEN_MATCH=0 keeps it)", hz, r == kCGErrorSuccess ? "switched to" : "could not be switched to", pick_hz, want);
|
||||
if (r == kCGErrorSuccess) { if (!g_match_orig) { g_match_orig = CGDisplayModeRetain(cur); g_match_id = id; } g_screen_fps = (int)round(pick_hz); }
|
||||
}
|
||||
if (modes) CFRelease(modes);
|
||||
CGDisplayModeRelease(cur);
|
||||
}
|
||||
void front_screen_match(int fps) {
|
||||
if (!g_layer || g_match_want == fps) return; // no window (a windowless run's main thread runs the guest), or the rate as before
|
||||
g_match_want = fps; dispatch_async(dispatch_get_main_queue(), ^{ screen_match_apply(); });
|
||||
}
|
||||
static void push(void) {
|
||||
int32_t lx = (g_d - g_a) * 32767, ly = (g_w - g_s) * 32767;
|
||||
dm_input_set(g_btn, lx, ly, 0, 0);
|
||||
@@ -127,6 +184,7 @@ void front_run(void (*guest)(void)) {
|
||||
[win center];
|
||||
[win makeKeyAndOrderFront:nil];
|
||||
g_screen_fps = (int)(win.screen ?: NSScreen.mainScreen).maximumFramesPerSecond;
|
||||
g_win = win;
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
// Main menu: AppKit runs ⌘ shortcuts only through menu items, so ⌘Q quits and ⌃⌘F toggles fullscreen from here (AppKit shows the item as Globe-F and keeps ⌃⌘F as a hidden twin)
|
||||
NSMenu *bar = [NSMenu new], *app = [NSMenu new], *wm = [[NSMenu alloc] initWithTitle:@"Window"];
|
||||
@@ -152,11 +210,11 @@ void front_run(void (*guest)(void)) {
|
||||
usingBlock:^(NSNotification *n) { (void)n; exit(0); }];
|
||||
// Lifecycle (hle.h / dm.h): focus lost → 2, focus regained → 1; minimized → 3 + audio paused + GPU suspended, restored → resume (followed by focus regained → 1)
|
||||
NSNotificationCenter *nc = NSNotificationCenter.defaultCenter;
|
||||
[nc addObserverForName:NSWindowDidChangeScreenNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; if (win.screen) { g_screen_fps = (int)win.screen.maximumFramesPerSecond; g_layer.contentsScale = win.backingScaleFactor; } }];
|
||||
[nc addObserverForName:NSWindowDidChangeScreenNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; if (win.screen) { g_screen_fps = (int)win.screen.maximumFramesPerSecond; g_layer.contentsScale = win.backingScaleFactor; } screen_match_apply(); }];
|
||||
// Entering and leaving fullscreen: the layer is layer-backed, so its frame (and with it the drawable) follows the
|
||||
// content view by itself; only the scale factor has to be carried over to the screen the window ended up on.
|
||||
[nc addObserverForName:NSWindowDidEnterFullScreenNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; g_layer.contentsScale = win.backingScaleFactor; }];
|
||||
[nc addObserverForName:NSWindowDidExitFullScreenNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; g_layer.contentsScale = win.backingScaleFactor; }];
|
||||
// content view by itself; only the scale factor has to be carried over to the screen the window ended up on. The screen's rate is matched in fullscreen only (screen_match_apply)
|
||||
[nc addObserverForName:NSWindowDidEnterFullScreenNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; g_layer.contentsScale = win.backingScaleFactor; screen_match_apply(); }];
|
||||
[nc addObserverForName:NSWindowDidExitFullScreenNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; g_layer.contentsScale = win.backingScaleFactor; screen_match_apply(); }];
|
||||
[nc addObserverForName:NSWindowDidResignKeyNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; if (!win.miniaturized) dm_focus_set(2); }];
|
||||
[nc addObserverForName:NSWindowDidBecomeKeyNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; dm_focus_set(1); }];
|
||||
[nc addObserverForName:NSWindowDidMiniaturizeNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; dm_focus_set(3); dm_audio_suspend(1); dm_gfx_suspend(1); }];
|
||||
|
||||
@@ -113,6 +113,7 @@ So the translator leaves z alone (`QueryGraphicsState` depth mode false in `Prog
|
||||
|
||||
- **Dynamic present interval (Mario 1.2.1 on the iPhone)**: Mario times every frame (`main+0xcfd24`); after a frame that ran past one vertical sync it sets present interval 2 for the next frame *and* sets its own time step to the interval (a float at `main+0x36ceff0`), so that frame catches up 2/60 s. A user's 2-minute iPhone session switched 123 times, mostly for one frame. In `display` mode those frames were shown for 16.7 ms and the game visibly jumped ahead ("sometimes the speed increases when jumping"). In `game` mode (the default since 2026-10-15) the display-link callback keeps a frame whose period is over 25 ms on screen for its whole period before taking the next one (`g_link_hold_until` in `nvn_mtl.m`; the `display link up to tick` log line counts these ticks as `held for the present interval`), and the 30 fps target that drops the display link to 30 Hz (`nvn_game_fps` in `hle_nvn.c`) waits until interval 2 has held for a full second, so one-frame switches no longer flip the display-link mode. **Fixed: the game side of that hold (BotW on the iPhone, "when pressing B, Link's move pace is speeding up").** The acquire waited for one display-link tick per frame whatever the interval, so the game kept producing frames every 16.7 ms while the screen held each interval-2 frame for 33 ms and dropped the one in between (`old frames dropped by merging`). BotW sprinting flaps between interval 1 and 2 for runs of 6 or more frames, and its VFR time step at interval 2 is a whole 30 fps tick, so those runs went at double speed. `mtl_link_wait_tick` now waits for as many ticks as the game's present interval (times 2 on the 120 Hz link, half rounded up on the 30 Hz link), as the Switch blocks the next acquire for N vertical syncs. The windowless Mac host was never affected: there the acquire sleeps until the interval's slot. `DM_NVN_TSV` writes each frame's interval as `@itv`. `DM_TEST_PRESENT_LAG=90,30` (sleep 30 ms before every 90th present) makes Mario do this on the Mac; run it with `DM_WINDOW=1` to get the display link.
|
||||
- **Screens whose rate is not a multiple of 60 (144 or 165 Hz Mac displays; from PR #27)**: asked for 60 Hz, the system fires the display link on every third refresh of a 165 Hz screen, 55 Hz, so games paced by it ran at 55 fps (27.5 at 30). There `link_range` (`nvn_mtl.m`) runs the link at the screen's rate and counts as ticks only the refreshes nearest a steady 1/60 s clock: 60 a second on average, each frame shown 12.1 or 18.2 ms at 165 Hz. A tick lies up to half a refresh off its slot, so two ticks can be one refresh nearer each other than their slots: the hold of frames over 25 ms tolerates one refresh more (in a simulation of the callback without it, 30 fps frames held a third tick: 22.5 fps at 165 Hz, 24 at 144), and the frame step lets a frame on time show one refresh longer than its hold (`shown_slack`, `nvn_fstep.h`; a cheap 50 ms frame shows 54.5 ms at 165 Hz, and counted as late it kept Breath of the Wild at 20 fps for good). `DM_LINK_VTICK=0` keeps the system's choice. iOS screens run at multiples of 60 and are not affected. Not yet run on such a screen here; the PR's author measured at 165 Hz.
|
||||
- **In fullscreen, the screen switched to a rate the frame rate divides (from PR #79)** (`front_mac.m` `front_screen_match`, called from `link_sync` with the link's rate: 60, 30, or 120 with `DM_PRESENT_LINK=2`): virtual ticks keep the average, but a frame still stays up two or three refreshes in turn (13.9 / 20.8 ms at 144 Hz), and Apple's HUD showed the frame interval jumping between them. When the window is fullscreen on a screen whose mode is not a multiple of the link's rate, the host switches that screen to the mode of the same size with the highest rate that is (an XB273U at 2560x1440: 144 → 120 Hz for 60 fps), with `kCGConfigureForAppOnly`, so macOS switches it back when the host quits, crashes included (the PR's author checked: 144 Hz again after quitting); leaving fullscreen, or the screen, switches it back at once. A window keeps the screen's rate and the virtual ticks: the switch changes the whole screen for every app, and most external screens go black for a second or more. The link sets its range again whenever the screen's rate changes (`link_sync`; before, a move to another screen kept the first screen's range), with no virtual ticks after a switch. A screen with no such mode keeps the virtual ticks; `DM_SCREEN_MATCH=0` keeps the screen's rate. The PR's author ran Hades at 144 Hz (switched in a window then): the HUD's frame interval line steady at 16.7 ms, where it jumped before. Here (a built-in 120 Hz screen, a multiple of every link rate) a harness drove `front_mac.m` asked for 50 fps: no switch in the window, 120 → 50 Hz in fullscreen, 120 Hz again on leaving it, and 120 Hz after quitting in fullscreen; at 60 fps and with `DM_SCREEN_MATCH=0` nothing switched. The link's new range after a switch has not run here (Hades windowed and fullscreen paced at 60 as before).
|
||||
|
||||
### Seen in Engage (Unity)
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ The README covers packing and playing. This page is for working on nxnative itse
|
||||
| Host (`dm/host`) | Stands in for the Horizon kernel and the system services the games call: threads and synchronization, memory, file system with transactional saves, HID, audio, movies (VP9 through libvpx, Vorbis sound through stb_vorbis), software keyboard, focus and lifecycle |
|
||||
| Graphics (`dm/gfx`) | NVN on Metal. Shaders are extracted by `shader_split.py` from Nintendo's containers and from Unity's `Shader` objects, then translated offline. Mario: 16,266 of 16,268 shaders compile. Engage: 9,268 of 9,269. Games that render with Vulkan (Hades) or OpenGL (Digimon Story Cyber Sleuth) go through layers of their own on Metal (`dm/gfx/vk`, `dm/gfx/gl`) |
|
||||
| Audio | Games that carry the SDK's audio renderer (Mario) run it in-process; games that use AudioOut directly (Engage: Wwise) feed it straight. Output goes through Core Audio (default on iOS, `DM_AUDIO=1` on the Mac) |
|
||||
| Input | iOS: the touch gamepad and physical controllers; shaking the phone shakes the controller (its six-axis sensor, `hle_hid.c`; the accelerometer in `DmGamepad.m`, threshold `DM_SHAKE_G`, 1.5 G by default). Mac: scripted input (`DM_KEYS=<seconds>:<button>,…`, `Shake` for a shake, with `DM_KBD=<text>` answering the system keyboard) or `DM_WINDOW=1` for a window with keyboard input (Space shakes) and physical controllers (buttons by position, as on the iPhone). The window goes fullscreen from its green button or ⌃⌘F; `DM_FULLSCREEN=1` starts it that way. The drawable follows the window in pixels (fullscreen at the screen's resolution); `DM_METAL_HUD=1` shows Apple's performance HUD (on the iPhone too, from `Documents/env.txt`). The left mouse button is the touchscreen, on the 16:9 picture |
|
||||
| Input | iOS: the touch gamepad and physical controllers; shaking the phone shakes the controller (its six-axis sensor, `hle_hid.c`; the accelerometer in `DmGamepad.m`, threshold `DM_SHAKE_G`, 1.5 G by default). Mac: scripted input (`DM_KEYS=<seconds>:<button>,…`, `Shake` for a shake, with `DM_KBD=<text>` answering the system keyboard) or `DM_WINDOW=1` for a window with keyboard input (Space shakes) and physical controllers (buttons by position, as on the iPhone). The window goes fullscreen from its green button or ⌃⌘F; `DM_FULLSCREEN=1` starts it that way. In fullscreen, a screen whose refresh rate is not a multiple of the game's (144 or 165 Hz) runs at one that is (120 Hz) until the window leaves fullscreen; `DM_SCREEN_MATCH=0` keeps its rate. The drawable follows the window in pixels (fullscreen at the screen's resolution); `DM_METAL_HUD=1` shows Apple's performance HUD (on the iPhone too, from `Documents/env.txt`). The left mouse button is the touchscreen, on the 16:9 picture |
|
||||
| iOS app | One unsigned IPA for iPhone and iPad. The rewritten code is embedded in the executable; the shader libraries, the RomFS and the game icon are inside the app. A settings screen comes before the game (`Launcher.swift`, drawn from `dm/launcher.json`, which `dm/games/settings.py` writes from `dm/games/settings.tsv`); the host's loading starts at app launch meanwhile, and the frame rate and render scale go into the frame step state when the game starts (`loader.c` `dm_fps_state_apply`) |
|
||||
| Known gaps | Conditional render is always drawn; `MultiDraw*IndirectCount` waits on the CPU for the GPU-written count; geometry shaders do not translate. TBDR optimizations are off by default (they don't understand bindless textures yet). No amiibo. The bundled prewarm lists are empty. On the Mac, Mario's menu and dialogue text render blank (fine on the iPhone) |
|
||||
|
||||
|
||||
Reference in new issue
Block a user