Skip to content
Closed
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
27 changes: 25 additions & 2 deletions dm/host/hle_oe.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

// The operation mode is runtime state (the front end can change it: iOS overheat downgrade switches to handheld): the single source is here, taken once from DM_OPMODE at startup, the environment variable is not read again afterwards
#include <stdatomic.h>
#include <errno.h>
static _Atomic int g_opmode = -1;
int dm_opmode(void) {
int m = g_opmode;
Expand Down Expand Up @@ -131,11 +132,33 @@ HLE_FORCE("_ZN2nn2oe18GetPerformanceModeEv", "performance mode query; same as th
static unsigned char g_cpu_oc;
HLE_FORCE("_ZN2nn2oe21IsCpuOverclockEnabledEv", "CPU overclock state (through apm): what SetCpuOverclockEnabled set, off at start", unsigned, h_oe_CpuOc, void) { return g_cpu_oc; }
HLE_FORCE("_ZN2nn2oe22SetCpuOverclockEnabledEb", "CPU overclock switch (through apm); the host does not scale clocks", void, h_oe_SetCpuOc, unsigned on) { g_cpu_oc = on & 0xff; }
// This query is not every game's source of camera dimensions. Record actual
// callers before trying a resolution override; keep the stock Switch answer.
static _Atomic int g_aspect_trace_on = -1;
static _Atomic unsigned g_aspect_trace_count;
static void aspect_query_trace(const char *symbol, int width, int height) {
int saved_errno = errno, enabled = atomic_load(&g_aspect_trace_on);
if (enabled < 0) {
const char *e = getenv("DM_ASPECT_TRACE"); int value = e && !strcmp(e, "1"), unknown = -1;
atomic_compare_exchange_strong(&g_aspect_trace_on, &unknown, value); enabled = atomic_load(&g_aspect_trace_on);
}
if (enabled) {
unsigned n = atomic_load(&g_aspect_trace_count);
while (n < 16 && !atomic_compare_exchange_weak(&g_aspect_trace_count, &n, n + 1)) {}
if (n < 16) {
if (width && height) LOG("aspect query %u/16 %s answer=%dx%d (stock; no camera override)", n + 1, symbol, width, height);
else LOG("aspect query %u/16 %s change_event_request (stock binding)", n + 1, symbol);
}
}
errno = saved_errno;
}
HLE_FORCE("_ZN2nn2oe27GetDefaultDisplayResolutionEPiS1_", "default display resolution: 1920x1080 docked, 1280x720 handheld", void, h_oe_DefRes, int *w, int *h) {
int con = opmode() == 1; if (w) *w = con ? 1920 : 1280; if (h) *h = con ? 1080 : 720;
int con = opmode() == 1, width = con ? 1920 : 1280, height = con ? 1080 : 720;
if (w) *w = width; if (h) *h = height;
aspect_query_trace(hle_force_h_oe_DefRes.name, width, height);
}
HLE_FORCE("_ZN2nn2oe38GetDefaultDisplayResolutionChangeEventEPNS_2os11SystemEventE", "default display resolution change event: bound, never signaled (the operation mode notification carries the change)",
void, h_oe_DefResEv, void *ev) { hle_bind_system_event(ev, 0); }
void, h_oe_DefResEv, void *ev) { aspect_query_trace(hle_force_h_oe_DefResEv.name, 0, 0); hle_bind_system_event(ev, 0); }
HLE_FORCE("_ZN2nn2oe40SendServerMaintenanceOverlayNotificationENS_4time9PosixTimeES2_", "server maintenance overlay (through am); the host has no system overlay", void, h_oe_MaintOverlay, int64_t a, int64_t b) { (void)a; (void)b; }
// apm performance configurations: Normal 0x00010000 (CPU 1020 / GPU 384 / EMC 1600 MHz, the handheld default), Boost 0x00010001 (GPU 768 MHz, the docked default)
HLE_FORCE("_ZN2nn2oe27GetPerformanceConfigurationENS0_15PerformanceModeE", "query the CPU/GPU clock config (through apm); reports the default for the mode", int, h_oe_GetPerfCfg, int mode) { return mode == 1 ? 0x00010001 : 0x00010000; }
Expand Down
9 changes: 9 additions & 0 deletions dm/host/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,12 @@ tsan-threads-run: $(addprefix $(B)/,$(TSAN_T))
$(B)/t_altstack
DM_QOS_MAP=1 $(B)/t_notify_sched
DM_QOS_MAP=1 $(B)/t_notify_sched --all-peers

# Guest query tracing plus the existing VI geometry/crop boundary; no game data or GPU.
$(B)/t_aspect_trace: t_aspect_trace.c ../hle_oe.c ../../gfx/hle_vi.c ../hle.h ../dm.h $(GAME_H) | $(B)
$(CLANG) -O1 -g -Wall -Werror -Wno-unused-function -I.. $(GINC) -o $@ t_aspect_trace.c
.PHONY: aspect-trace
aspect-trace: $(B)/t_aspect_trace
$(B)/t_aspect_trace

run: aspect-trace
64 changes: 64 additions & 0 deletions dm/host/tests/t_aspect_trace.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Actual guest display-query and layer hooks: diagnostics must not override geometry.
#include "../hle_oe.c"
#include "../../gfx/hle_vi.c"
#include <mach-o/getsect.h>
#include <unistd.h>
static int bound, autoclear;
static void *event_pointer;
void dm_thread_native_wait(int entering) { (void)entering; }
uint64_t now_ns(void) { return 0; }
void dm_audio_suspend(int on) { (void)on; }
void guest_clock_freeze_for(int reason, int on) { (void)reason; (void)on; }
void dm_gfx_suspend(int on) { (void)on; }
uint64_t dm_nvn_frame_count(void) { return 0; }
void hle_bind_system_event(void *ev, int clear) { bound++; autoclear = clear; event_pointer = ev; }
void hle_signal_system_event(void *ev) { (void)ev; }
void hle_clear_system_event(void *ev) { (void)ev; }
extern const struct mach_header_64 _mh_execute_header;
static int checks, failures;
#define CHECK(c, message) do { checks++; if (!(c)) { failures++; printf("FAIL: %s\n", message); } } while (0)
static void *hook(const char *name) {
unsigned long bytes = 0; void *fn = NULL; unsigned count = 0;
const hle_force_ent *entries = (const hle_force_ent *)getsectiondata(&_mh_execute_header, "__DATA", "swforce", &bytes);
for (size_t i = 0; entries && i < bytes / sizeof *entries; i++) if (!strcmp(entries[i].name, name)) { fn = entries[i].fn; count++; }
CHECK(count == 1 && fn, name); return fn;
}
int main(void) {
unsetenv("DM_OE_SCRIPT"); unsetenv("DM_ASPECT_TRACE"); g_aspect_trace_on = -1; g_aspect_trace_count = 0;
FILE *capture = tmpfile(); int saved = dup(STDERR_FILENO);
CHECK(capture && saved >= 0, "capture available"); if (!capture || saved < 0) return 1;
fflush(stderr); dup2(fileno(capture), STDERR_FILENO);
void (*resolution)(int *, int *) = hook("_ZN2nn2oe27GetDefaultDisplayResolutionEPiS1_");
void (*resolution_event)(void *) = hook("_ZN2nn2oe38GetDefaultDisplayResolutionChangeEventEPNS_2os11SystemEventE");
Result (*create)(void **, void *, int, int) = hook("_ZN2nn2vi11CreateLayerEPPNS0_5LayerEPNS0_7DisplayEii");
Result (*crop)(void *, int, int, int, int) = hook("_ZN2nn2vi12SetLayerCropEPNS0_5LayerEiiii");
int w = -1, h = -1; g_opmode = 1; errno = EDOM; resolution(&w, &h);
CHECK(w == 1920 && h == 1080 && errno == EDOM, "default docked geometry/error unchanged");
fflush(stderr); CHECK(ftell(capture) == 0, "default query trace off");
setenv("DM_ASPECT_TRACE", "1", 1); g_aspect_trace_on = -1;
errno = ERANGE; resolution(&w, &h);
CHECK(w == 1920 && h == 1080 && errno == ERANGE, "traced docked answer remains stock");
g_opmode = 0; resolution(&w,&h);
CHECK(w == 1280 && h == 720, "traced handheld answer remains stock");
h = -1; resolution(NULL, &h); CHECK(h == 720, "null width output remains supported");
resolution(NULL, NULL); CHECK(g_aspect_trace_count == 4, "null outputs still record an actual query");
uint8_t event[0x100]; resolution_event(event);
CHECK(bound == 1 && event_pointer == event && autoclear == 0, "change event retains one unsignaled binding");
int correct = 1;
for (int i = 0; i < 200; i++) { resolution(&w,&h); correct &= w == 1280 && h == 720; }
CHECK(correct && g_aspect_trace_count == 16, "bounded query tracing never changes repeated answers");
fflush(stderr); long query_bytes = ftell(capture);
void *layer = NULL; CHECK(create(&layer, NULL, 1920, 1080) == RES_OK && layer, "actual sized layer hook still succeeds");
dm_vi_layer_size(&w,&h); CHECK(w == 1920 && h == 1080, "layer geometry comes from its guest arguments");
CHECK(crop(layer, 12, 8, 1280, 720) == RES_OK, "actual guest crop hook succeeds");
int rect[4]; dm_vi_layer_crop(rect);
CHECK(rect[0] == 12 && rect[1] == 8 && rect[2] == 1280 && rect[3] == 720, "crop is retained independently of display query");
fflush(stderr); dup2(saved, STDERR_FILENO); close(saved); rewind(capture);
char text[16384]; size_t bytes = fread(text, 1, sizeof text - 1, capture); text[bytes] = 0; fclose(capture);
int lines = 0; for (long i = 0; i < query_bytes; i++) if (text[i] == '\n') lines++;
CHECK(lines == 16, "only first sixteen actual query/event calls logged");
CHECK(strstr(text,"aspect query 1/16 _ZN2nn2oe27GetDefaultDisplayResolutionEPiS1_ answer=1920x1080") && strstr(text,"answer=1280x720"), "bounded trace identifies symbol and stock answers");
CHECK(strstr(text,"GetDefaultDisplayResolutionChangeEvent") && strstr(text,"vi::CreateLayer 1920x1080") && strstr(text,"vi::SetLayerCrop 12,8 1280x720"), "existing layer logs and new query log expose separate boundaries");
printf("aspect trace: %d checks, %d failed\n", checks, failures);
return failures != 0;
}
103 changes: 103 additions & 0 deletions dm/ios/tools/build_aspect_trace_probe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Build an unsigned native iOS probe of actual registered diagnostic hooks.

GAME selects a known source profile. No game files, keys or signing are used.
Install and run only through the user's authorized device workflow.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys

REPO = Path(__file__).resolve().parents[3]
NAME = 'AspectTraceVerification'
KIND = 'aspect'
PRODUCTION = ['dm/host/hle_oe.c', 'dm/gfx/hle_vi.c']
EXTRA = []
HOST = r'''#import <UIKit/UIKit.h>
extern int dm_trace_test_main(void);
@interface TraceScene : NSObject<UIWindowSceneDelegate>
@property(strong,nonatomic) UIWindow *window;
@end
@implementation TraceScene
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)options {
(void)session; (void)options;
self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene];
self.window.rootViewController = [UIViewController new]; [self.window makeKeyAndVisible];
dispatch_async(dispatch_get_main_queue(), ^{
int result = dm_trace_test_main();
NSURL *url = [NSBundle.mainBundle URLForResource:@"SourceProvenance" withExtension:@"json"];
NSDictionary *source = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfURL:url] options:0 error:NULL];
NSDictionary *report = @{@"status":@(result), @"registeredProductionHooksUsed":@YES,
@"eventBindingStubbed":@YES, @"gameRuntimeUsed":@NO, @"physicalNetworkOrCameraTested":@NO,
@"systemVersion":UIDevice.currentDevice.systemVersion, @"sourceProvenance":source};
NSURL *documents = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0];
[[NSJSONSerialization dataWithJSONObject:report options:NSJSONWritingPrettyPrinted error:NULL]
writeToURL:[documents URLByAppendingPathComponent:@"results.json"] atomically:YES];
fflush(stdout); fflush(stderr);
});
}
@end
@interface TraceApp : UIResponder<UIApplicationDelegate>
@end
@implementation TraceApp
@end
int main(int argc, char **argv) { @autoreleasepool {
NSURL *documents = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0];
freopen([documents URLByAppendingPathComponent:@"verification.log"].fileSystemRepresentation, "w", stdout);
freopen([documents URLByAppendingPathComponent:@"errors.log"].fileSystemRepresentation, "w", stderr);
setvbuf(stdout, NULL, _IOLBF, 0); setvbuf(stderr, NULL, _IOLBF, 0);
return UIApplicationMain(argc, argv, nil, NSStringFromClass(TraceApp.class));
} }
'''


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', required=True, type=Path, help='new scratch directory')
args = parser.parse_args()
game = os.environ.get('GAME') or os.environ.get('DM_GAME')
if not game:
parser.error('GAME or DM_GAME must select a source profile')
output = args.output.resolve()
output.mkdir(parents=True)
include = output / 'Include'
include.mkdir()
subprocess.run([sys.executable, '-B', str(REPO / 'dm/games/game.py'), 'header', game,
str(include / 'game_profile.h')], check=True)
(output / 'Host.m').write_text(HOST)
test = f'dm/host/tests/t_{KIND}_trace.c'
provenance = {'commit': subprocess.check_output(['git', '-C', str(REPO), 'rev-parse', 'HEAD'], text=True).strip(),
'sourceSHA256': {path: hashlib.sha256((REPO / path).read_bytes()).hexdigest()
for path in [test, *PRODUCTION]}, 'gameProfile': game}
provenance_file = output / 'SourceProvenance.json'
provenance_file.write_text(json.dumps(provenance, indent=2) + '\n')
project = {'name': NAME, 'options': {'deploymentTarget': {'iOS': '26.0'}}, 'targets': {NAME: {
'type': 'application', 'platform': 'iOS',
'sources': [{'path': str(output / 'Host.m')},
{'path': str(REPO / test), 'compilerFlags': '-Dmain=dm_trace_test_main'},
*[{'path': str(REPO / path)} for path in EXTRA],
{'path': str(provenance_file), 'buildPhase': 'resources'}],
'dependencies': [{'sdk': 'UIKit.framework'}, {'sdk': 'Foundation.framework'}],
'settings': {'base': {'PRODUCT_BUNDLE_IDENTIFIER': f'dev.anygame.{KIND}-trace-verification',
'ENABLE_DEBUG_DYLIB': 'NO', 'CODE_SIGN_STYLE': 'Automatic',
'TARGETED_DEVICE_FAMILY': '1', 'HEADER_SEARCH_PATHS': [str(REPO / 'dm/host'), str(include)]}},
'info': {'path': 'Info.plist', 'properties': {
'CFBundleDisplayName': NAME, 'UILaunchScreen': {}, 'UIFileSharingEnabled': True,
'UIApplicationSceneManifest': {'UIApplicationSupportsMultipleScenes': False, 'UISceneConfigurations': {
'UIWindowSceneSessionRoleApplication': [{'UISceneConfigurationName': 'Default', 'UISceneDelegateClassName': 'TraceScene'}]}}}}}}}
spec = output / 'project.yml'
spec.write_text(json.dumps(project, indent=2) + '\n')
subprocess.run(['xcodegen', 'generate', '--spec', str(spec)], check=True)
subprocess.run(['xcodebuild', '-project', str(output / f'{NAME}.xcodeproj'), '-scheme', NAME,
'-configuration', 'Debug', '-destination', 'generic/platform=iOS',
'-derivedDataPath', str(output / 'DerivedData'), '-jobs', '3',
'CODE_SIGNING_ALLOWED=NO', 'build'], check=True)
print(output / f'DerivedData/Build/Products/Debug-iphoneos/{NAME}.app')


if __name__ == '__main__':
main()
31 changes: 31 additions & 0 deletions docs/aspect-tracing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Diagnose native aspect before changing a camera

This branch adds `DM_ASPECT_TRACE=1`, off by default. It records the first 16 actual `oe::GetDefaultDisplayResolution` queries and resolution-change-event requests. Answers remain 1920x1080 docked and 1280x720 handheld. Event binding stays unchanged. The trace works on iOS because it runs inside the registered hooks rather than generating executable counting stubs.

The source already logs the later output boundaries, so this experiment adds no NVN profiler or present mode:

| Boundary | Existing source and evidence |
|---|---|
| Guest default display answer | `dm/host/hle_oe.c`, new `aspect query` lines |
| Guest sized layer and layer crop | `dm/gfx/hle_vi.c`, `vi::CreateLayer` and changed `vi::SetLayerCrop` lines |
| Swapchain and window crop | `dm/gfx/nvn/hle_nvn.c`, `WindowInitialize` and changed `WindowCrop` lines |
| Valid source rectangle and drawable canvas | `dm/gfx/nvn/nvn_mtl.m`, changed `present crop` and `present viewport` lines |
| Internal render attachments and shader bindings | existing `DM_NVN_PASSDUMP` / `DM_NVN_PASSDUMP_ALL` |
| Exact projection candidates | existing `DM_NVN_CBDUMP` / `DM_NVN_CBDUMP_KEY`, after identifying a real world vertex shader |

## Capture procedure

1. Record the exact game version, `main` and `sdk` NSO build IDs, host revision, shader-chain stamp and source hashes. Use an isolated save and a stable gameplay route. Keep game dumps and generated captures in that game's external work folder. A profile title or a matching shader name is not a substitute for the exact build.
2. Enable `DM_ASPECT_TRACE=1` before launch. Capture the stock run first, including the existing crop, swapchain and viewport lines. No query line means these particular registered hooks were not reached while tracing; it does not prove that the game has no display or camera logic. A line numbered `16/16` means the query budget is exhausted.
3. Pick a known frame in the stable world scene and set `DM_NVN_PASSDUMP` to that frame, or a short inclusive range. Set `DM_NVN_PASSDUMP_ALL=1` only for that bounded capture. Record the actual world vertex shader key from its draw and resolve it in that build's shader map. Menus and final composite triangles do not identify the world camera. On a Mac host, an existing `DM_CTL` `passdump` request can select a few frames once the scene is reached.
4. Create an output directory, set `DM_NVN_CBDUMP` to it, and set `DM_NVN_CBDUMP_KEY` to the confirmed full vertex key. Keep the same bounded `DM_NVN_PASSDUMP` range. The backend writes `F<frame>_p<pass>_st<stage>_cb<slot>_<address>.bin` blocks; for a vertex key it also records vertex streams and indices. Do not substitute an example or invented shader key.
5. Compare a fixed scene and a controlled camera change. Track which internal attachments feed the final image, which crop is valid, and which uniform block contains the projection actually used by that world draw. Trace that block's producer in the exact memory image before changing it. A changing matrix is a candidate until its consumer and producer are confirmed.
6. Only then define a build-gated experiment that covers the camera projection, culling, internal target dimensions, postprocessing and UI anchoring together. Compare world geometry and visible edges against the stock capture. A wider output texture, a scalar float hit or an absence of literal-load references does not establish that the camera widened.

`tools/scan_aspect.py` in the wrapper repository is a preliminary scalar/LDR-literal scan. It misses ADRP-based loads and computed ratios. Earlier draw-table and float-constant experiments are recorded in [anygame issue #2](https://github.com/oliverames/anygame/issues/2); their negative results should not be replaced with an assumption that the camera follows the output size.

## Verification and limits

`make -C dm/host/tests GAME=<known profile> aspect-trace` calls the actual registered resolution and layer hooks without game files or a GPU. It checks default-off logging, stock docked/handheld answers, nullable output arguments, event binding, the 16-line cap, errno preservation, and the independent guest layer/crop state. `GAME=<known profile> python3 dm/ios/tools/build_aspect_trace_probe.py --output <new scratch directory>` builds the same test in an unsigned native iOS app. The app writes `Documents/results.json`, `verification.log` and `errors.log`, with exact source hashes. Its event binding is a stub and it runs no guest game.

No camera patch, native-aspect rendering, cropping, stretching or resolution override is implemented. The procedure still needs a confirmed game build, world shader key and real capture before it can identify a safe projection change.
2 changes: 2 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,8 @@ The game's own work and its frame rate stay as they are; the host avoids work of

Each game's notes are in its profile folder, `dm/games/<id>/README.md`: what the profile needs and why, what was found on the game, Mac routes and open items. The [games table](../dm/games/README.md#games) lists them.

Diagnostic experiments: [Native-aspect boundary diagnostics](aspect-tracing.md) records existing guest boundaries without changing network or rendering behavior.

## Tools (`dm/tools`)

| Tool | What it does |
Expand Down
Loading