Skip to content

ios: honour Conf::high_dpi on the Metal path - #654

Open
benface wants to merge 1 commit into
not-fl3:masterfrom
benface:metal-view-high-dpi
Open

ios: honour Conf::high_dpi on the Metal path#654
benface wants to merge 1 commit into
not-fl3:masterfrom
benface:metal-view-high-dpi

Conversation

@benface

@benface benface commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

create_metal_view takes _high_dpi and never uses it. MTKView derives its drawable from bounds * contentScaleFactor, which defaults to the screen's native scale, so Conf::high_dpi: false has no effect on the Metal path: the drawable is always native resolution.

The display-size code compounds it:

let (screen_width, screen_height) = if high_dpi {
    let scale: f64 = msg_send![main_screen, scale];
    (…width * scale…, …height * scale…)
} else {
    let content_scale_factor: f64 = msg_send![payload.view, contentScaleFactor];
    (…width * content_scale_factor…, …height * content_scale_factor…)
};

With high_dpi: false, the else branch reads a contentScaleFactor that was never set down, so it reports the native size too. The reported size and the drawable agree only because neither honours the flag.

Where the fix goes

Not in create_metal_view, which is where I tried it first. Two revisions failed there on device, so it is worth recording why the placement is not arbitrary:

  • It crashes on the !high_dpi path. The resize triggers drawableSizeWillChange:, which reaches for NATIVE_DISPLAY before the backend has initialized it.
  • It also has no effect even when it does not crash. MTKView does not keep a contentScaleFactor set before it has a window, because moving it in re-derives the scale from that window's screen.

So it is applied after makeKeyAndVisible, once the view is in a window and the backend is up.

GLKView does not behave that way: it keeps what create_opengl_view sets at creation. I checked that on device by removing this patch's call and running the OpenGL backend with high_dpi: false, which still reported a scale of 1.0 on a screen at 2.0. So the new call is a no-op on that path, and create_opengl_view is left alone.

Why it matters

Found on tvOS. An Apple TV 4K reports bounds of 1920×1080 at 2.0x, so the drawable is 3840×2160, four times the pixels of 1080p. On a first-generation Apple TV 4K (A10X) a 2D game was running at roughly 10–15 fps, with no way to opt out short of reaching into the view from the app's own main.m:

window.rootViewController.view.contentScaleFactor = 1.0;

With that, the same build is smooth. This patch makes it high_dpi: false instead, which is what the setting is for.

iOS is affected in the same way: any app that sets high_dpi: false on the Metal backend silently gets a native-resolution drawable.

Checks

  • cargo check --target aarch64-apple-ios and --target aarch64-apple-tvos, both ok.
  • On device: high_dpi: true on iPhone and the iPad simulator, no visual difference, since UIScreen.scale is what UIKit was defaulting the view to anyway. high_dpi: false on a first-generation Apple TV 4K, drawable 3840×2160 down to 1920×1080, and roughly 10–15 fps up to smooth.

I deliberately did not run cargo fmt; master isn't rustfmt-clean and it rewrites a dozen unrelated files. The added lines follow the surrounding style.

@benface

benface commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Checked on device with an app pointed at this branch.

high_dpi: true (iPhone, iPad simulator): no visual difference. UIScreen.scale is the value UIKit was already defaulting the view to, so nothing changes for apps that don't opt out.

high_dpi: false on a first-generation Apple TV 4K: the drawable goes from 3840x2160 to 1920x1080, taking a 2D game from roughly 10-15 fps to smooth.

high_dpi: false on the OpenGL backend, with this patch's call removed: still 1.0 on a screen reporting 2.0. GLKView keeps a contentScaleFactor set at creation, unlike MTKView, so the new call is a no-op on that path and create_opengl_view is left alone.

Placement is covered in the description: two earlier revisions set the scale inside create_metal_view and both were wrong on device, one of them a crash on what is actually the default path.

@benface
benface force-pushed the metal-view-high-dpi branch 2 times, most recently from 5e1ffea to 4fb2ac3 Compare July 26, 2026 03:58
`create_metal_view` took `_high_dpi` and never used it. `MTKView`
sizes its drawable from `bounds * contentScaleFactor`, which
defaults to the screen's native scale, so `high_dpi: false` had no
effect and the drawable was always native resolution.

The display-size branch that reads `contentScaleFactor` then
reported that same native size, so the two agreed only because
neither honoured the flag.

Applied once the view is in a window rather than at creation, for
two reasons. Moving a view into a window re-derives its
`contentScaleFactor` from that window's screen, so a value set at
creation does not survive. And changing it resizes the drawable,
whose `drawableSizeWillChange:` reaches for `NATIVE_DISPLAY` —
which at creation time the backend has not initialized yet.

Placed after `makeKeyAndVisible`, this covers the OpenGL view as
well, which sets the scale at creation and loses it the same way.
@benface
benface force-pushed the metal-view-high-dpi branch from 4fb2ac3 to f0a0882 Compare July 26, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant