Skip to content
Merged
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
45 changes: 44 additions & 1 deletion dm/games/0400E88025B1A000/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,49 @@ On an M5 Pro with 24 GB, macOS 27.0, handheld mode and Resolution 100% give 1920

`python3 scripts/build_ipa_from_nsp.py <folder> --platform mac` builds the Mac shaders and the host and prints the run command. Headless, through `DM_CTL`: the title is up about 40 s after boot and `A` starts the game. The prologue cinematic follows, about 4 minutes on every run (a save holds no progress yet, and skipping it was not tried), then the tutorial's first question, about 300 s after boot: `LDown`, `A` answers no. Shots of the swap chain hold the picture in their top-left quarter. A `shot` takes about 165 ms: a hitch every few seconds in a run that takes them is the shot's.

## Experimental Mac test modes

The optional selector in `mac_test_launcher.m` has two independent dropdowns:
**Resolution: 1080p / 4K** and **Frame rate: 60 fps / 120 fps / Unlocked**.
All six combinations are available in one existing test app. All modes use
one RomFS, shader set and save folder. `scripts/configure_starfox_test.py` updates
only that app's executables and rewritten main, backing up changed files only.
It deliberately accepts the isolated `org.nxnative.starfox.120hz-test` bundle,
not the normal app.

The build-gated output stubs use state +0xa4: -1 (unset) runs the original game
paths, 0 selects its zero desired-frame-time path and current-clock update,
and 60 or 120 selects its positive 1/60- or 1/120-second interval. The uncapped path bypasses
both output-stage waits; it does not remove a simulation-thread sleep or scale
the game clock. The two refresh fallbacks use 120 only during the experiment;
correct SDK VI refresh/capability service semantics remain open.

Uncapped modes also set `DM_PRESENT_HZ=0`, `DM_PRESENT_LINK=0` and
`DM_PRESENT_VSYNC=0`. This removes software and Metal synchronization limits;
the number of frames the physical screen can display is still finite. 4K uses
docked mode, `DM_VI_BUFFER_MAX=3` and scene Resolution 100%, then scales the
3840x2160 output to the Mac's screen. The 1080p modes use handheld mode and
`DM_VI_BUFFER_MAX=1`. Render-pass dimensions, not screenshot size alone, establish
the scene resolution.

The source is based on the verified Mac integration so the existing shader
libraries remain usable. Build the host with `make -C dm/host GAME=0400E88025B1A000`,
rewrite main with this profile, then run:

```sh
python3 scripts/configure_starfox_test.py '/path/Star Fox 120 Hz Test.app' \
--patched /path/to/rewritten-main --backup /path/to/new-backup-folder
```

On the M5 Pro 24 GB, macOS 27.0, the player reported roughly 300 fps in the
1080p uncapped flight and confirmed normal movement, aiming and audio. Pass
captures showed 1920x1080 scene color/depth targets in that mode and 3840x2160
scene targets in 4K mode. This is opening-tutorial evidence, not a campaign or
4K frame-rate guarantee.

The test modes remain experimental: frame throughput does not prove correct
movement speed, physics, audio/movie sync or later-level stability.

## Found on this game

- The avatar settings present the virtual camera's picture into an NVN window of their own; the host makes it an off-screen window: [GameChat queries from the title screen (SDK 21)](../../../docs/reference/services.md#gamechat-queries-from-the-title-screen-sdk-21).
Expand All @@ -62,7 +105,7 @@ On an M5 Pro with 24 GB, macOS 27.0, handheld mode and Resolution 100% give 1920

## Open

- Native 120 Hz: the host has no takeover for `nn::vi::QueryDisplayCurrentRefreshRate` or `SetDisplay120HzRefreshRateCapability`. SDK 21 rejects the host's display handle, and the game's refresh queries keep their prefilled 60 Hz answer. Exposing 120 Hz still needs correct display semantics and simulation, audio, movie and input checks. No frame-rate unlock is enabled in this fork.
- Native 120 Hz: the host has no takeover for `nn::vi::QueryDisplayCurrentRefreshRate` or `SetDisplay120HzRefreshRateCapability`. SDK 21 rejects the host's display handle, and the game's refresh queries keep their prefilled 60 Hz answer by default. The optional test selector uses build-gated refresh fallbacks and output timing stubs; correct SDK display semantics and later-level checks remain open.

- [The picture](#the-picture): 768x432 docked until #1304 and #1305 are in; the Resolution setting raises the scene above the game's own scale.
- `[RED] DLSS: the game's bias mask (a reactive mask for MetalFX) is not passed on`.
Expand Down
60 changes: 60 additions & 0 deletions dm/games/0400E88025B1A000/mac_test_launcher.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// The optional Star Fox experiment selector. All modes share the app's data, shaders and save.
#import <Cocoa/Cocoa.h>
#include <unistd.h>

int main(int argc, char **argv) {
@autoreleasepool {
NSArray<NSString *> *resolutions = @[@"1080p", @"4k"];
NSArray<NSString *> *rates = @[@"60", @"120", @"uncapped"];
NSUserDefaults *prefs = NSUserDefaults.standardUserDefaults;
NSString *mode = [prefs stringForKey:@"test.mode"] ?: @"1080p-uncapped";
BOOL show = YES;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--mode") && i + 1 < argc) { mode = @(argv[++i]); show = NO; }
else if (!strcmp(argv[i], "--no-launcher")) show = NO;
else { fprintf(stderr, "usage: nxnative [--mode <1080p|4k>-<60|120|uncapped>]\n"); return 2; }
}
NSArray<NSString *> *parts = [mode componentsSeparatedByString:@"-"];
if (parts.count != 2 || ![resolutions containsObject:parts[0]] || ![rates containsObject:parts[1]]) { fprintf(stderr, "Unknown Star Fox test mode\n"); return 2; }
if (show) {
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp finishLaunching];
NSAlert *alert = [NSAlert new];
alert.messageText = @"Star Fox test settings";
alert.informativeText = @"Choose the resolution and frame rate. 4K renders at 3840 × 2160 and scales to your screen.";
[alert addButtonWithTitle:@"Play"]; [alert addButtonWithTitle:@"Cancel"];
NSView *controls = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 320, 84)];
NSTextField *resLabel = [NSTextField labelWithString:@"Resolution"];
resLabel.frame = NSMakeRect(0, 52, 120, 22); [controls addSubview:resLabel];
NSPopUpButton *res = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(130, 48, 190, 32) pullsDown:NO];
[res addItemsWithTitles:@[@"1080p", @"4K"]]; [res selectItemAtIndex:[resolutions indexOfObject:parts[0]]];
res.accessibilityLabel = @"Resolution"; [controls addSubview:res];
NSTextField *fpsLabel = [NSTextField labelWithString:@"Frame rate"];
fpsLabel.frame = NSMakeRect(0, 12, 120, 22); [controls addSubview:fpsLabel];
NSPopUpButton *fps = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(130, 8, 190, 32) pullsDown:NO];
[fps addItemsWithTitles:@[@"60 fps", @"120 fps", @"Unlocked"]]; [fps selectItemAtIndex:[rates indexOfObject:parts[1]]];
fps.accessibilityLabel = @"Frame rate"; [controls addSubview:fps];
alert.accessoryView = controls;
[NSApp activateIgnoringOtherApps:YES];
if ([alert runModal] != NSAlertFirstButtonReturn) return 0;
mode = [NSString stringWithFormat:@"%@-%@", resolutions[res.indexOfSelectedItem], rates[fps.indexOfSelectedItem]];
[prefs setObject:mode forKey:@"test.mode"];
[NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited]; // the host owns the game's windows, as in Launcher.swift
}
parts = [mode componentsSeparatedByString:@"-"];
BOOL fourK = [parts[0] isEqualToString:@"4k"], uncapped = [parts[1] isEqualToString:@"uncapped"];
setenv("DM_STARFOX_RENDER_HZ", uncapped ? "0" : parts[1].UTF8String, 1);
setenv("DM_PRESENT_VSYNC", uncapped ? "0" : "1", 1);
setenv("DM_VI_BUFFER_MAX", fourK ? "3" : "1", 1);
setenv("DM_OPMODE", fourK ? "1" : "0", 1);
setenv("DM_RESOLUTION_SCALE", "1", 1);
setenv("DM_PRESENT_HZ", "0", 1); setenv("DM_PRESENT_LINK", "0", 1);
setenv("DM_PRESENT_INTERVAL", "game", 1);
setenv("DM_LAUNCHER", "0", 1); setenv("DM_SAVED_CHOICES", "0", 1);
setenv("DM_METAL_HUD", "1", 1);
NSString *core = [NSBundle.mainBundle.executablePath.stringByDeletingLastPathComponent stringByAppendingPathComponent:@"nxnative-core"];
execl(core.fileSystemRepresentation, core.fileSystemRepresentation, "--no-launcher", NULL);
perror("Starting Star Fox"); return 1;
}
}
47 changes: 47 additions & 0 deletions dm/games/0400E88025B1A000/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,50 @@ def frame_step(ctx):
emit(B(here(), 0xb4498b0)) # done
put(0xb4498ac, [B(0xb4498ac, start)], 'scene resolution scale from the host (DM_RESOLUTION_SCALE; 0 = the game\'s own)')

# Experimental output timing, state +0xa4: -1 = original, 0 = native unlimited path, 120 = desired frame time 1/120.
# The active sleeper was observed at main+0xb447270 on the output-stage thread. Leave the simulation thread alone.
site(0xb444adc, 'e12342bd2820201eed000054')
ctx.area(0xb444adc)
start = here()
emit(ADRP(9, here(), state))
emit(0xb940a528) # ldr w8,[x9,#0xa4]
emit(0x37f80108) # tbnz w8,#31,original (+32)
emit(0x340000a8) # cbz w8,unlimited (+20)
emit(0x1e220101) # scvtf s1,w8
emit(0x1e2e1000) # fmov s0,#1.0
emit(0x1e211801) # fdiv s1,s0,s1
emit(B(here(), 0xb444ae8))
emit(0xf94033e0) # unlimited: ldr x0,[sp,#0x60], the engine object
emit(B(here(), 0xb444be4)) # native zero desired-time path
emit(0xbd4223e1) # original: ldr s1,[sp,#0x220]
emit(0x1e202028) # fcmp s1,#0.0
emit(0x5400004c) # b.gt positive (+8)
emit(B(here(), 0xb444b00))
emit(B(here(), 0xb444ae8))
put(0xb444adc, [B(0xb444adc, start), 0xd503201f, 0xd503201f], 'experimental native output frame time (DM_STARFOX_RENDER_HZ)')

site(0xb444c88, 'e81740b9') # ldr w8,[sp,#0x14]
start = here()
emit(ADRP(9, here(), state))
emit(0xb940a528) # ldr w8,[x9,#0xa4]
emit(0x35000048) # cbnz w8,original (+8)
emit(B(here(), 0xb444d7c)) # uncapped: original current-clock path, skipping both output waits
emit(0xb94017e8) # original ldr w8,[sp,#0x14]
emit(B(here(), 0xb444c8c))
put(0xb444c88, [B(0xb444c88, start)], 'uncapped output uses the original current-clock path; no simulation sleep removed')

for off in (0xba66464, 0xba69750):
site(off, '88078052')
ctx.area(off)
start = here()
emit(0x52800788) # original 60 Hz fallback
emit(ADRP(9, here(), state))
emit(0xb940a529) # ldr w9,[x9,#0xa4]
emit(0x37f80049) # tbnz w9,#31,done (+8)
emit(0x52800f08) # experiment's 120 Hz display fallback
emit(B(here(), off + 4))
put(off, [B(off, start)], 'experiment display refresh fallback; SDK service semantics remain unimplemented')

FRAME_STEP = {'a7730d0f74d168d90422354c8317f76f': frame_step}
# The launcher offers DM_RESOLUTION_SCALE (dm/games/settings.tsv) only for this build
CAPABILITIES = {'a7730d0f74d168d90422354c8317f76f': ('RESOLUTION_SCALE',)}
Expand All @@ -32,4 +76,7 @@ def frame_step(ctx):
{'what': 'scene resolution scale', 'now': 'build', 'goal': 'game', 'purpose': 'rendering',
'why': "the game's own settings read in its renderer",
'blocked': 'frame-step stubs have no function-matched form yet'},
{'what': 'experimental output timing', 'now': 'build', 'goal': 'game', 'purpose': 'frame rate',
'why': 'the native desired-frame-time and current-clock paths in the output-stage loop',
'blocked': 'addresses belong to this build; simulation and audio correctness above 60 fps remain unverified'},
]
9 changes: 9 additions & 0 deletions dm/gfx/hle_vi.c
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ HLE_FORCE("_ZN2nn2vi31QueryDisplayBufferMaxResolutionEPNS0_19BufferMaxResolution
}
static uint32_t buf_max(int *dw, int *dh) { // the index into k_buf_max, and the display's pixels (0 x 0 when not known)
uint32_t r = 1; int w = 0, h = 0;
const char *override = getenv("DM_VI_BUFFER_MAX");
if (override && *override) {
char *end; long v = strtol(override, &end, 10);
if (!*end && v >= 0 && v < 4) {
*dw = k_buf_max[v][0]; *dh = k_buf_max[v][1];
static atomic_int once; if (!atomic_exchange(&once, 1)) LOG("vi: buffer maximum override %dx%d (DM_VI_BUFFER_MAX)", *dw, *dh);
return (uint32_t)v;
}
}
if (dm_opmode() == 1 && front_display_pixels(&w, &h)) {
if (w < h) { int t = w; w = h; h = t; }
r = 0; for (uint32_t i = 0; i < 4; i++) if (w >= k_buf_max[i][0] && h >= k_buf_max[i][1]) r = i;
Expand Down
5 changes: 4 additions & 1 deletion dm/gfx/nvn/nvn_mtl.m
Original file line number Diff line number Diff line change
Expand Up @@ -13401,7 +13401,10 @@ static int present_now(const tex_t *t, double period, uint64_t frame, uint64_t t
if (!layer) return 0;
if (g_present_layer_resident != layer) { [g_q4 addResidencySet:layer.residencySet]; g_present_layer_resident = layer; }
#if TARGET_OS_OSX
if (!layer.displaySyncEnabled) layer.displaySyncEnabled = YES; // Windowed mode follows the display's vsync to prevent tearing
const char *vsync = getenv("DM_PRESENT_VSYNC");
BOOL sync = !(vsync && !strcmp(vsync, "0"));
if (layer.displaySyncEnabled != sync) layer.displaySyncEnabled = sync;
static int sync_logged; if (!sync_logged) { sync_logged = 1; MLOG("displaySyncEnabled=%d (DM_PRESENT_VSYNC; default 1)", sync); }
#endif
present_pipeline(layer.pixelFormat); // Same as the original path: the first frame compiles the pipeline before taking the drawable
if (g_grab_cb) grab_present(src, crop, layer.drawableSize); // Game picture needed by the whole-window screenshot (peek without the lock first; the real take locks inside the function)
Expand Down
13 changes: 12 additions & 1 deletion dm/host/loader.c
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,11 @@ static void read_layout(module_t *m, const char *dir) {
// target, 0 = the game's own interval logic. The profile's fps_target, overridden by DM_FPS_TARGET (0, 30 or 60); the graphics layer decides the steps and paces presents
// (hle_nvn.c fstep_tick)
uint8_t *dm_fps_state;
static const char *g_fps_build;
static const char *g_fps_mod; // the module holding the state page (for the log)
static void install_fps_state(module_t *m) {
if (m->fps_state_off + 0x40 > m->size || m->fps_state_off < m->key_page_off + m->key_page_size) die("%s: frame step state page out of range", m->name);
dm_fps_state = m->base + m->fps_state_off; g_fps_mod = m->name;
dm_fps_state = m->base + m->fps_state_off; g_fps_mod = m->name; g_fps_build = m->build_id;
}
// The settings in the state page, written when the game starts (main.c, after host loading): on iOS the loading runs at app launch, before the launcher has set DM_FPS_TARGET and
// DM_RENDER_SCALE (Launcher.swift), and nothing reads the page before the game runs
Expand All @@ -318,6 +319,16 @@ void dm_fps_state_apply(void) {
if (qs && *qs) { char *end; q = strtof(qs, &end); if (end == qs || *end || !(q == 0 || (q >= 0.25f && q <= 1.0f))) { LOG("[RED] DM_RESOLUTION_SCALE=%s: must be 0 or 0.25 to 1.0; keeping the game's scale", qs); q = 0; }
else if (q > 0) LOG("%s: scene rendered at %.2f of the target (DM_RESOLUTION_SCALE; only builds with that stub follow it)", g_fps_mod, q); }
memcpy(dm_fps_state + 0xa0, &q, sizeof q);
// The Star Fox experiment's native output timer: -1 keeps the game, 0 is uncapped, 60/120 select its positive desired-frame-time path.
if (g_fps_build && !strcmp(g_fps_build, "a7730d0f74d168d90422354c8317f76f")) {
const char *hz = getenv("DM_STARFOX_RENDER_HZ"); int32_t rate = -1;
if (hz && !strcmp(hz, "0")) rate = 0;
else if (hz && !strcmp(hz, "60")) rate = 60;
else if (hz && !strcmp(hz, "120")) rate = 120;
else if (hz) LOG("[RED] DM_STARFOX_RENDER_HZ must be 0, 60 or 120; keeping the game's output timing");
memcpy(dm_fps_state + 0xa4, &rate, sizeof rate);
LOG("main: Star Fox output timer %d Hz (-1 = game's own, 0 = uncapped)", rate);
}
// Screen shape: DM_SCREEN_ASPECT, the screen's width / height between 1.3 and 1.77, makes the builds with screen-shape stubs
// (Breath of the Wild 1.9.0's patches.py) render that shape: the output keeps its width (1920) and takes the height that gives the ratio, a multiple of 48 so the
// 2/3-size layout space (1280 wide) stays whole: 1.43 → 1920x1344 and 1280x896. State +0x48 u32 output height, +0x4c u32 layout height, +0x50 its float; 0 = 16:9
Expand Down
7 changes: 7 additions & 0 deletions dm/host/tests/t_vi_display.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,12 @@ int main(void) {
uint32_t b = 99; uint64_t w = 0, h = 0;
CHECK(h_vi_BufMaxRes(&b, NULL) == RES_OK && b == c[i].buf && layer(&w, &h) && w == c[i].lw && h == c[i].lh, c[i].what);
}
setenv("DM_VI_BUFFER_MAX", "3", 1); g_mode = 1; g_ounce = 1; g_w = 1920; g_h = 1080;
uint32_t b = 99; uint64_t w = 0, h = 0;
CHECK(h_vi_BufMaxRes(&b, NULL) == RES_OK && b == 3 && layer(&w, &h) && w == 3840 && h == 2160,
"explicit 4K buffer and layer on a smaller display for supersampling");
setenv("DM_VI_BUFFER_MAX", "invalid", 1);
CHECK(h_vi_BufMaxRes(&b, NULL) == RES_OK && b == 1, "invalid override keeps the display's own buffer maximum");
unsetenv("DM_VI_BUFFER_MAX");
printf("== vi display %d checks, %d failed ==\n", checks, fails); return fails != 0;
}
3 changes: 3 additions & 0 deletions dm/tools/docs_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ def payoff(gap): # the Gaps order: open gaps, then blocked, kept and unreviewe

# The host's build-keyed rows: (table, the start of the row's label ('' = any), main build ('' = any), entry); the first that fits classifies a row
HOST_BINDING = [
('', '', 'a7730d0f74d168d90422354c8317f76f', {'what': 'experimental output timing', 'now': 'build', 'goal': 'game', 'purpose': 'frame rate',
'why': 'the host supplies the private state field read by this build\'s output-loop stubs',
'blocked': 'addresses belong to this build; simulation and audio correctness above 60 fps remain unverified'}),
('k_locks', 'Unity', '', {'what': "Unity's Application.targetFrameRate at a fixed address", 'now': 'build', 'goal': 'engine', 'purpose': 'frame rate',
'why': 'an IL2CPP player registers the setter by its icall name, so the field can be found in any Unity game; whether to raise it stays the profile\'s'}),
('k_locks', 'RE Engine', '', {'what': "RE Engine's frame limit at a fixed pointer", 'now': 'build', 'goal': 'engine', 'purpose': 'frame rate',
Expand Down
Loading