Repository navigation
host/vi: HDR output for Switch 2 games on screens that can show EDR - #1176
maxdavidson wants to merge 1 commit into
Conversation
4f40096 to
79dc8ca
Compare
70395e9 to
f94b279
Compare
a0c3511 to
e863278
Compare
Port PR seedds#1176 at a360806f onto oot-hyper-main 64a163ba. Preserve SDR tagging, quality filtering, AOT hooks and absolute pacing. Keep HDR off by default and bypass the SDR MetalFX scaler for PQ input. Static Ocarina v131072 modules and resources contain the HDR status calls, paper-white controls, PQ constants and BT.2020 output matrix. Native present and quality checks, settings/docs tests and the host build pass. Gameplay, displays and HDR pipeline archive coverage remain unverified. Front-ends: new: Mac Switch 2 builds can offer default-off DM_HDR; HDR layer formats override SDR colorspace tagging. Co-authored-by: seedds <seedds@gmail.com> Co-authored-by: beecrepaldi-afk <beecrepaldi@gmail.com>
a360806 to
173e358
Compare
24e84ef to
69676b9
Compare
|
Tested this branch on a screen with EDR headroom, which the description lists as untested. Setup: MacBook Pro M2 Pro, built-in Liquid Retina XDR (potential headroom 16, about 1600 nits), macOS 27.2. Ocarina of Time (0400486024DEC000, v131072), the branch cherry-picked onto main 88c7e03 with #1331, #1447, #1462 and #1368. Result: it works. Status answers HDR, format 1. The game switches its window from 0x25 to 0x3d and declares 0..203 / 0..710 nits; the PQ layer gets the HDR10 metadata. The title holds 60 fps with no new red line, and HDR looks right by eye. How the game uses the two numbers (one relaunch each):
Ocarina declares min(peak, 3.5 × paper white). The paper white is what lifts its highlights; the default peak never limits it. Testing gotcha: a window behind other windows reads a current headroom of 1.2 whatever it declares. In front, the same window reads 16 at low brightness and 3.2 at full brightness (1600 / 500 nits). Calibration proposal, working locally on top of this branch (about 100 lines with tests and docs; happy to open a PR or share the patch):
Minor: the MetalFX present scaler running on PQ codes looked over-sharpened next to Standard (bilinear) with the 3D at full resolution. Subjective. |
69676b9 to
ec134cc
Compare
|
Found and worked around a fullscreen HDR flicker with this PR's PQ layer, in case it helps others. Symptom: MacBook Pro M4 Pro, macOS 27.2, Ocarina of Time (v131072, oot2 fork + this PR), Reproduced outside the game: a ~300-line standalone Cocoa app reproduces it:
Without the GPU load, or with a drawable smaller than the screen, it did not reproduce. What removed it (one change at a time):
What did not: the RGBA16Float layer ( On-screen times: Workaround: in |
|
@Amost- and @b1tdr3amer24, please open up PRs toward this branch in https://github.com/maxdavidson/nxnative/tree/feat/switch2-hdr-output |
hello, done |
|
This is needed to fix fullscreen HDR output on my Vizio 4k TV. Otherwise I have magenta aritficating near the top of the screen unless the menu bar is shown. |
Know-It-All-Brother
left a comment
There was a problem hiding this comment.
Tested this on an M5 Max (macOS 27.0.1) with Ocarina of Time 0400486024DEC000 v131072, on an LG UltraFine with no EDR (potential headroom 1.00), using DM_HDR=2.
- It works. The status answers HDR, format 1. The game switches its window from 0x25 to 0x3d and declares 0..203 / 0..710 nits, the same as @Amost- saw. The PQ layer gets the metadata, and the picture looks fine in a window and in fullscreen. There's no new red line over the 200 s smoke route.
- In fullscreen the present after the GPU turned on as expected. On this fixed 60 Hz screen, every frame landed a refresh late. With
DM_PRESENT_SHOWN=1, display-link target to on screen was p50 16.66 ms in fullscreen, against -0.02 ms in the window, over one 300-frame window. So at a fixed 60 Hz it's a frame of lag all the time, not just when the game drops below 60. - This conflicts with main now, only in the Ocarina of Time README (I kept both sides). I merged main
7ffa63cchere: https://github.com/Know-It-All-Brother/nxnative/tree/hdr-output-main.t_present_hdrpasses 27/27,t_present_crop112/112 andt_present_alloc13/13, and the Mac host builds.
I'm also suggesting tagging the SDR layer sRGB below. Right now it's untagged, so macOS shows it in the screen's own colour space. I haven't confirmed yet that it fixes the magenta band on my TV with HDR off.
| @@ -1449,6 +1476,24 @@ void front_run(void (*guest)(void)) { | |||
| g_layer = [CAMetalLayer layer]; | |||
| g_layer.device = MTLCreateSystemDefaultDevice(); | |||
| g_layer.pixelFormat = MTLPixelFormatBGRA8Unorm; | |||
There was a problem hiding this comment.
Tag the SDR layer as sRGB, which is what the game's SDR picture is. Untagged, macOS takes it as the screen's colour space. The HDR block below still sets its own colour space.
| g_layer.pixelFormat = MTLPixelFormatBGRA8Unorm; | |
| g_layer.pixelFormat = MTLPixelFormatBGRA8Unorm; | |
| { CGColorSpaceRef srgb = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); g_layer.colorspace = srgb; CGColorSpaceRelease(srgb); } |
There was a problem hiding this comment.
I've left this line as it is: setting pixelFormat to BGRA8Unorm already tags the layer sRGB on macOS 27.2. A fresh CAMetalLayer has no color space, and after the pixel format is set it reads kCGColorSpaceSRGB. An extended-range screen capture (an LG C3 in macOS HDR mode, windowed) gives the same colors for this layer and an explicitly sRGB-tagged one: sRGB red reads 0.8223 0.0332 0.0171 in both. So the explicit tag wouldn't change the picture here.
If the magenta band still shows with HDR off on 27.0.1, could you log what g_layer.colorspace reads on your setup? If it comes back NULL there, the explicit tag is worth adding, and I'll take this suggestion.
— posted by Claude (Opus 5.5) on behalf of maxdavidson
# Conflicts: # dm/games/0400486024DEC000/README.md # dm/gfx/hle_vi.c # dm/gfx/nvn/nvn_mtl.m
…one (PR seedds#1176, the SDR layer tagged sRGB), VRR present pacing, controller and window changes, movie and video codec fixes, and the bigger guest stacks
e1277c9 to
8c265de
Compare
…one (PR seedds#1176, the SDR layer tagged sRGB), VRR present pacing, controller and window changes, movie and video codec fixes, and the bigger guest stacks
8c265de to
cf2ca59
Compare
|
Thanks for testing on the M5 Max and the TVs, and for the timing. The sRGB tag: setting a The frame a refresh late in fullscreen: the branch no longer puts HDR10 metadata on the layer by default. The rectangles came with the metadata, of any range, also in a standalone app, and without it there are none, so the present keeps Apple's order, which landed on time in our measurements; the present after the GPU now runs only in fullscreen with Defaults now: HDR output on (automatic: only on a screen that can show EDR), paper white 100, the peak automatic (100 nits times the screen's current headroom), on the Mac and iOS. On a screen without EDR the SDR picture goes out at the screen's white whatever the paper white. Could you retest fullscreen HDR on the Vizio with the new build? The magenta artifacts near the top you saw without the menu bar are what I'd most like to know about: our fullscreen measurements were on a MacBook and an LG C3, not on your TV. |
cf2ca59 to
4ed504f
Compare
d945160 to
65af3ec
Compare
3b04b7c to
0e44b7c
Compare
A Switch 2 game asks vi::QueryDisplayHdrStatus for the display's HDR state. The host answered SDR, so the game never made its HDR10 window (RGB10A2, PQ in BT.2020) and the present had no decode for one. - hle_vi.c: with DM_HDR=1 (the HDR row, on by default) the status answers HDR on, format 1, while the window's screen can show EDR, and SDR elsewhere, where the game draws its own SDR picture. The games ask every frame, so the answer follows the window to another screen, and the iOS in-game menu turns HDR on and off while the game runs. - The front ends (front_mac.m, front_ios.m) make the layer RGB10A2 in BT.2100 PQ. The present (present.metal, nvn_mtl.m present_mode) writes the game's HDR10 window as it is, and encodes an SDR or float source at the paper white while the status answers HDR, at 100 nits while it answers SDR. - The paper white (DM_HDR_PAPER_WHITE) is 100 nits by default: macOS shows a PQ layer at EDR = nits / 100 (WWDC21 10161, and an extended-range screen capture), so 100 is the screen's own white and the game's normal picture matches SDR. The peak (DM_HDR_PEAK) is automatic by default: 100 nits times the screen's current headroom, in steps of about 6 % held for a second, at least twice the paper white, so a game tone-maps into what the screen shows. Donkey Kong Bananza, Ocarina of Time and Mario Kart World keep their pixels within the peak they are told. - No HDR10 metadata on the layer by default (DM_HDR_META=1 adds it). With metadata above 100 times the headroom, Core Animation compresses the whole picture, mid-tones included, and Mario Kart World declares about 1.8 times the peak it is told. In fullscreen, a layer with metadata of any range shows rectangles of older frames under GPU load, also in a standalone app with no game. A fixed peak above the screen's is rolled off in the present instead (dm_hdr_rolloff). - With metadata in fullscreen on the Mac, the drawable is handed over once its present pass has completed, without signalDrawable (DM_PRESENT_AFTER_GPU): that removes the rectangles but makes frames late when a pass ends close to the refresh, so without metadata the present keeps Apple's order. - A 10-bit window's screenshot is written: the PNG holds each channel's top 8 bits, and <path>.raw the words. Checked: t_present_hdr (53 checks), t_present_alloc, t_present_rescue --metal, t_present_snap, t_present_crop; test_settings, test_docs, test_env_switches; dm/ios/tools/compile_check.py. Ocarina of Time, Mario Kart World and Donkey Kong Bananza on the Mac, Mario Kart World on iOS. regress.py runs leave DM_HDR unset, so they take the SDR path as before. Three reference entries in docs/reference/rendering.md, and the three games' notes. Front-ends: new: HDR output for Switch 2 games (DM_HDR, DM_HDR_PAPER_WHITE, DM_HDR_PEAK rows): the front end makes the HDR layer RGB10A2 in kCGColorSpaceITUR_2100_PQ with wantsExtendedDynamicRangeContent (front_mac.m, front_ios.m) Front-ends: new: the HDR row defaults to on; the host's own default, DM_HDR unset, stays SDR, so a front end that does not pass the row's value starts the game in SDR Front-ends: new: front_hdr_screen() (the window's screen can show EDR) and front_hdr_headroom() (its current EDR headroom) are weak hooks in hle_vi.c; without front_hdr_headroom the automatic peak answers 1000 and nothing is rolled off Front-ends: new: a front end calls dm_hdr_layer_meta_set() with whether it gave the layer HDR10 metadata, and receives the game's declared range through front_hdr_content_range() for that metadata Co-authored-by: beecrepaldi-afk <beecrepaldi@gmail.com> Co-authored-by: b1tdr3amer24 <240231594+b1tdr3amer24@users.noreply.github.com> Co-authored-by: Amost <6445689+Amost-@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0e44b7c to
5074a70
Compare
What and why
A Switch 2 game asks
vi::QueryDisplayHdrStatusfor the display's HDR state. The host answered SDR, so the game never made its HDR10 window (RGB10A2, PQ in BT.2020). With this change, on the Mac and iOS:DM_HDR) is on by default.DM_HDR_META=1adds it). With a range above the screen's headroom Core Animation compresses the whole picture, mid-tones included. In fullscreen, a layer with metadata of any range shows rectangles of older frames under GPU load, a macOS fault. A fixed peak above the screen's is rolled off in the present instead.The reference entries are in
docs/reference/rendering.md(HDR output), and each game's findings are in its note. Built on beecrepaldi-afk's PQ layer, b1tdr3amer24's present after the GPU, and Amost's paper white and peak rows; they are co-authors of the commit.Checked
make -C dm/gfx/tests GAME=0400486024DEC000:t_present_hdr(53 checks),t_present_alloc,t_present_rescue --metal,t_present_snap(all six modes),t_present_crop;test_docs,test_settings,test_env_switches;dm/ios/tools/compile_check.py(263 files).0400486024DEC000v131072, Mario Kart World0400C3F00006E000v0, Donkey Kong Bananza040074A01BF12000v458752. Compared: the windows each game makes and the ranges it declares, its pixels read back off-screen against the peak it was told, the paper white 100 and 203 against SDR by eye while the game ran, fullscreen with and without metadata, and a window moved between an HDR and an SDR screen.lint_c.py tidyandlint_python.pylocally (CI runs them).t_dlss_mfx(32 checks),t_upscaler_mfx(12).regress.py check --runs 2against main (DM_HDRunset, so the SDR path; Mac with an M1 Pro, macOS 27.2.1). Shaders identical in all five games (no MSL changed, none newly failing), no new red lines:0100000000010000: CHANGED, one screenshot moved within the baseline's noise (13.7 against 53.7); 59.5 fps both, recorded invalidated.tsv.040074A01BF12000: PASS, 59.6–59.7 fps both, 21,225 pipelines built (a provisional boot route, not committed; a build with derived shader tables, so not recorded).01007EF00011E000: fps flagged 36.3 → 32.4, within the baseline's own spread (36.3 and 31.4 against 32.0 and 32.4; p50 33.3 ms in all four).0400C3F00006E000: fps flagged 57.2 → 50.6, within the baseline's own spread (57.2 and 33.9 against 50.2 and 50.6; the race differs every run).0400486024DEC000: not comparable, the baseline's pipeline prebuild reached its 1,200 s limit with a cold archive and the game did not start.Front ends
Front-ends: new: HDR output for Switch 2 games (DM_HDR, DM_HDR_PAPER_WHITE, DM_HDR_PEAK rows): the front end makes the HDR layer RGB10A2 in kCGColorSpaceITUR_2100_PQ with wantsExtendedDynamicRangeContent (front_mac.m, front_ios.m)
Front-ends: new: the HDR row defaults to on; the host's own default, DM_HDR unset, stays SDR, so a front end that does not pass the row's value starts the game in SDR
Front-ends: new: front_hdr_screen() (the window's screen can show EDR) and front_hdr_headroom() (its current EDR headroom) are weak hooks in hle_vi.c; without front_hdr_headroom the automatic peak answers 1000 and nothing is rolled off
Front-ends: new: a front end calls dm_hdr_layer_meta_set() with whether it gave the layer HDR10 metadata, and receives the game's declared range through front_hdr_content_range() for that metadata
Docs
docs/reference/<subsystem>.mdentry, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index)🤖 Generated with Claude Code