Skip to content

HDR on the Mac: an RGB10A2 BT.2100 PQ layer (the RGBA16Float layer showed black blocks on an M5) - #1

Merged
maxdavidson merged 2 commits into
maxdavidson:feat/switch2-hdr-outputfrom
beecrepaldi-afk:hdr-pq-layer
Nov 5, 2026
Merged

maxdavidson merged 2 commits into
maxdavidson:feat/switch2-hdr-outputfrom
beecrepaldi-afk:hdr-pq-layer

Conversation

@beecrepaldi-afk

Copy link
Copy Markdown

What and why

A follow-up to seedds#1176, against its branch. On a MacBook Air M5 (macOS 27.0.1, built-in screen, potential EDR headroom 2), the RGBA16Float extended linear BT.2020 layer showed black blocks with coloured squares in them over flat areas: Mario Kart World's water on the course map. They flashed for a frame now and then, and for a moment every time the window came back to the front (⌘-Tab). With DM_HDR=0 there were none.

The game's frames were clean:

  • DM_NVN_MAGSCAN found no non-finite pixel in any pass in about 1,400 frames of HDR play;
  • 100 frames saved through DM_CTL shot while the blocks showed were clean;
  • present snapshots (DM_PRESENT_SNAP=2) did not remove them;
  • neither did guarding present_pq_to_linear's pow against a zero base (the present library is built with fast math) or clipping the layer's colours to Display P3.

With the game's HDR10 window passed as it is to an RGB10A2 layer in BT.2100 PQ (kCGColorSpaceITUR_2100_PQ), the blocks are gone and the colours look the same. This makes that layer the Mac's default:

  • present_mode: into an RGB10A2 drawable, the HDR10 window goes in as it is (mode 0); any other source is gamma-coded BT.709 and fs encodes it as HDR10 with its white at the paper white (new mode 3, present_linear_to_pq), for the game's SDR window before it switches.
  • DM_HDR_LAYER=float keeps the RGBA16Float layer. The iPhone is unchanged.

Why the extended-range layer shows the blocks is open (Core Animation, or the system's handling of that format on this Mac).

Checked

  • Tests: t_present_hdr (22 checks, 3 new: the PQ window's codes written as they are, SDR white at the paper white and black at code 0, SDR mid grey); test_docs, test_env_switches, test_dmsh_abi; make -C dm/host for Mario Kart World.
  • Game run, by play: Mario Kart World (0400C3F00006E000, v0), MacBook Air M5 16 GB, macOS 27.0.1, DM_HDR=1, windowed: with the float layer, the course map and free roam showed the blocks (recorded on video), now and then and after every ⌘-Tab; with the PQ layer, several minutes on the course map with repeated ⌘-Tab showed none, and the boot screens before the HDR window have their normal colours.
  • Not checked: an XDR screen, an external HDR monitor, Donkey Kong Bananza, fullscreen.

Docs

  • The finding is in the Mario Kart World note and the docs/reference/rendering.md entry, and python3 dm/tools/docs_symptoms.py was run
  • No game files, keys or data extracted from a game in the change or the attachments

🤖 Generated with Claude Code

beecrepaldi-afk and others added 2 commits November 5, 2026 14:00
…window as it is; the RGBA16Float extended-range layer showed black blocks over flat areas on an M5

On a MacBook Air M5 (macOS 27.0.1) the extended linear BT.2020 layer showed black
blocks with coloured squares over Mario Kart World's course-map water, now and then
and after the window came back to the front, while the game's frames were clean.
The layer now takes the PQ window as it is; an SDR source is encoded as HDR10 at
the paper white (present_mode 3). DM_HDR_LAYER=float keeps the old layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@maxdavidson maxdavidson left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this looks good. Passing the HDR10 window to a PQ layer as it is also halves the layer's bandwidth and skips our decode.

Checked here (M4 Pro, built-in XDR, macOS 26.7.1):

  • t_present_hdr passes, 22 of 22.
  • Donkey Kong Bananza ran with each layer: no [RED] lines, the game made its HDR window, and present-to-screen p50 was the same, 49.5–49.8 ms.

For a follow-up, after merge:

  • With RGB10A2, Core Animation ignores the metadata's opticalOutputScale. CAEDRMetadata.h says normalized formats are taken as 10,000. The picture is still right, since mode 3 puts SDR white at 203 nits, but the docs that call 1.0 the paper white need updating.
  • Some comments and env rows still describe the float layer.
  • The Metal HUD is much brighter on the PQ layer. It writes SDR values unconverted, and 1.0 in PQ is 10,000 nits.
  • Open: the root cause, which could be settled by reading back the float layer while the blocks show, and the iPhone, which still uses the float layer.

— Claude Opus 5.5, via Claude Code

@maxdavidson
maxdavidson merged this pull request into maxdavidson:feat/switch2-hdr-output Nov 5, 2026
maxdavidson pushed a commit that referenced this pull request Nov 6, 2026
…window as it is; the RGBA16Float extended-range layer showed black blocks over flat areas on an M5 (#1)

On a MacBook Air M5 (macOS 27.0.1) the extended linear BT.2020 layer showed black
blocks with coloured squares over Mario Kart World's course-map water, now and then
and after the window came back to the front, while the game's frames were clean.
The layer now takes the PQ window as it is; an SDR source is encoded as HDR10 at
the paper white (present_mode 3). DM_HDR_LAYER=float keeps the old layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maxdavidson pushed a commit that referenced this pull request Nov 6, 2026
… float source encoded for the PQ layer

Follow-up to the PQ layer (#1). The present code is shared, so the iPhone's
front end only makes the same layer: RGB10A2 in BT.2100 PQ, with DM_HDR_LAYER=float keeping the
RGBA16Float one on either platform (hle_vi.c dm_hdr_layer_float).

- present_mode: a float source (linear BT.709) into the PQ layer is encoded as HDR10 at the paper
  white (mode 4), not decoded as sRGB first; t_present_hdr checks it (24 checks).
- Docs and comments: the metadata's opticalOutputScale counts for the float layer only (Core
  Animation takes 10,000 for a normalized format), the Metal HUD shows at the screen's peak on the
  PQ layer, and the float layer's description moved to DM_HDR_LAYER. Donkey Kong Bananza's note has
  the two layers' times on an M4 Pro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maxdavidson pushed a commit that referenced this pull request Nov 7, 2026
… that would put the table in page zero, since no path of a working game carries it to the load (issue seedds#1532)

main+0x59dc90 interleaves four dispatch loops that share registers: x12 is the table base of one (adrp + add at +0x59dba4) and the constant 1 of another (mov w12,#1 at +0x59dbdc, in a block that leaves by b to its own dispatch), kept apart only by the value of w8. Since e5620b1 the base's writer walk sees a write of wN, met that mov first and gave up, and the NSP script stopped at `jump table 59dc90` on a build the profile names. patch_nso.py null_base: before the walk has found the base's add, a mov of a constant (or of wzr / xzr) that puts the whole table inside page zero is passed; Horizon never maps that page, so the load would fault through it on the console too. Any other writer still ends the chain, and a constant between the adrp and the add is the base's own input.

Checked: test_jump_copies.py NullPageBase (4 of its cases fail on the rewriter before), lint_python 0 new, test_docs. The site's ten targets are instructions of the function, three of them shared with its sibling tables in x15 and x17, and Cuphead's main.sites.tsv is line for line the one of the rewriter before e5620b1. Rewritten before and after: 34 other modules of 12 games (Okami HD, Octopath Traveler 0 and II, Three Houses, Sonic Superstars, Minecraft Dungeons II, Splatoon Raiders, Ocarina of Time, DK Bananza, Pokopia, Mario Tennis Fever, Cuphead's sdk and movie module) are byte-identical. Cuphead's NSP build completes, and the game plays from a fresh save to the Inkwell Isle map at 1.3.7 with no red line (the route does not reach this function). No regress.py run: nothing changes in any module that rewrote before.
maxdavidson pushed a commit that referenced this pull request Nov 7, 2026
…d jump_adr_offsets took the nearest cmp of the index, whatever followed it (issue seedds#977, PR seedds#1515)

guarded_count takes a compare only when, between it and the table load, the first branch on an unsigned condition comes with the compare's flags still in place: b.hi out of the dispatch reads N + 1 entries, b.hs / b.cs N (read as N + 1 before), b.ls / b.lo into it the same counts when the instruction before its target ends the path that failed. A compare with no such branch only sets the flags and the walk goes on to the check before it; with none the site is refused. Before, `cmp w18,seedds#4; b.hi exit; cmp w18,#1; ldrsw ...` read 2 entries of a table of 5 with no error.

Checked (by the author): 165 sites of 27 modules of 6 games, all cmp; b.hi, rewritten byte-identically; GuardedCheck, 8 cases, 7 failing before. Checked here, as seedds#977 asks before a stricter rule goes in: 35 modules of 12 games on the T7 (Cuphead, Three Houses, Minecraft Dungeons II, Octopath Traveler 0 and II, Okami HD, Sonic Superstars, Ocarina of Time, DK Bananza, Mario Tennis Fever, Pokopia, Splatoon Raiders) rewrite byte-identically before and after, and so do Super Smash Bros. Ultimate 13.0.5, Mario Kart 8 Deluxe and Super Mario Odyssey's main, sdk and subsdk0 (against origin/main, with the page-zero rule of the commit before); test_jump_copies 75 OK, lint_python 0 new, test_docs. No regress.py run: no module that rewrote before changes. Modules with b.hs or a branch into the dispatch are still tested only synthetically.

Co-authored-by: Atomsk-debug <135618857+Atomsk-debug@users.noreply.github.com>
maxdavidson pushed a commit that referenced this pull request Nov 7, 2026
…window as it is; the RGBA16Float extended-range layer showed black blocks over flat areas on an M5 (#1)

On a MacBook Air M5 (macOS 27.0.1) the extended linear BT.2020 layer showed black
blocks with coloured squares over Mario Kart World's course-map water, now and then
and after the window came back to the front, while the game's frames were clean.
The layer now takes the PQ window as it is; an SDR source is encoded as HDR10 at
the paper white (present_mode 3). DM_HDR_LAYER=float keeps the old layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maxdavidson pushed a commit that referenced this pull request Nov 7, 2026
… float source encoded for the PQ layer

Follow-up to the PQ layer (#1). The present code is shared, so the iPhone's
front end only makes the same layer: RGB10A2 in BT.2100 PQ, with DM_HDR_LAYER=float keeping the
RGBA16Float one on either platform (hle_vi.c dm_hdr_layer_float).

- present_mode: a float source (linear BT.709) into the PQ layer is encoded as HDR10 at the paper
  white (mode 4), not decoded as sRGB first; t_present_hdr checks it (24 checks).
- Docs and comments: the metadata's opticalOutputScale counts for the float layer only (Core
  Animation takes 10,000 for a normalized format), the Metal HUD shows at the screen's peak on the
  PQ layer, and the float layer's description moved to DM_HDR_LAYER. Donkey Kong Bananza's note has
  the two layers' times on an M4 Pro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
… float source encoded for the PQ layer

Follow-up to the PQ layer (maxdavidson#1). The present code is shared, so the iPhone's
front end only makes the same layer: RGB10A2 in BT.2100 PQ, with DM_HDR_LAYER=float keeping the
RGBA16Float one on either platform (hle_vi.c dm_hdr_layer_float).

- present_mode: a float source (linear BT.709) into the PQ layer is encoded as HDR10 at the paper
  white (mode 4), not decoded as sRGB first; t_present_hdr checks it (24 checks).
- Docs and comments: the metadata's opticalOutputScale counts for the float layer only (Core
  Animation takes 10,000 for a normalized format), the Metal HUD shows at the screen's peak on the
  PQ layer, and the float layer's description moved to DM_HDR_LAYER. Donkey Kong Bananza's note has
  the two layers' times on an M4 Pro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maxdavidson pushed a commit that referenced this pull request Nov 10, 2026
…window as it is; the RGBA16Float extended-range layer showed black blocks over flat areas on an M5 (#1)

On a MacBook Air M5 (macOS 27.0.1) the extended linear BT.2020 layer showed black
blocks with coloured squares over Mario Kart World's course-map water, now and then
and after the window came back to the front, while the game's frames were clean.
The layer now takes the PQ window as it is; an SDR source is encoded as HDR10 at
the paper white (present_mode 3). DM_HDR_LAYER=float keeps the old layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maxdavidson pushed a commit that referenced this pull request Nov 10, 2026
… float source encoded for the PQ layer

Follow-up to the PQ layer (#1). The present code is shared, so the iPhone's
front end only makes the same layer: RGB10A2 in BT.2100 PQ, with DM_HDR_LAYER=float keeping the
RGBA16Float one on either platform (hle_vi.c dm_hdr_layer_float).

- present_mode: a float source (linear BT.709) into the PQ layer is encoded as HDR10 at the paper
  white (mode 4), not decoded as sRGB first; t_present_hdr checks it (24 checks).
- Docs and comments: the metadata's opticalOutputScale counts for the float layer only (Core
  Animation takes 10,000 for a normalized format), the Metal HUD shows at the screen's peak on the
  PQ layer, and the float layer's description moved to DM_HDR_LAYER. Donkey Kong Bananza's note has
  the two layers' times on an M4 Pro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maxdavidson pushed a commit that referenced this pull request Nov 10, 2026
…en that can show EDR, the game's HDR10 window goes as it is into an RGB10A2 BT.2100 PQ layer, and the peak follows the screen's current headroom with no HDR10 metadata on the layer, on the Mac and iOS

- hle_vi.c: with DM_HDR=1 (the HDR output row, on by default) vi::QueryDisplayHdrStatus answers HDR on, format 1, while the window's screen can show EDR (DM_HDR=2: on any screen), and the game asks every frame, so its window follows the screen; the iOS in-game menu turns it on and off while the game runs. The paper white (DM_HDR_PAPER_WHITE, the HDR paper white row) is 100 nits by default, the screen's own white: macOS shows a PQ layer at EDR = nits / 100, measured with an extended-range screen capture, so the game's normal picture matches SDR and the headroom is left to its highlights (by eye, Mario Kart World at 100 had SDR's base brightness with brighter highlights; at 203 the whole picture was brighter).
- The peak (DM_HDR_PEAK, the HDR peak row) is automatic by default: 100 nits times the screen's current headroom (front_hdr_headroom: NSScreen's maximumExtendedDynamicRangeColorComponentValue on the Mac, UIScreen's currentEDRHeadroom on iOS), in steps of about 6 % taken once they have held for a second, at least twice the paper white, so the game tone-maps into what the screen shows. Donkey Kong Bananza and Ocarina of Time declare within the peak they are told, and all three games' pixels stay within it.
- No HDR10 metadata on the layer (DM_HDR_META=1 adds it): with metadata above 100 x headroom Core Animation compresses the whole picture, mid-tones included (measured), and Mario Kart World declares about 1.8 times the peak it is told; and in fullscreen a layer with metadata of any range showed rectangles of older frames under GPU load, in the host and in a standalone app with no game, judged by eye without knowing the mode (dm/gfx/tools/hdr_rects.mm), none without. A fixed peak above the screen's is rolled off in the present above three quarters of the screen's held peak (present.metal mode 4), keyed on the smaller of the declared range and the peak told.
- present.metal and nvn_mtl.m present_mode: the HDR10 window as it is into the PQ layer, an SDR or float source encoded at the paper white while the status answers HDR and at 100 nits, the screen's SDR white, while it answers SDR (at 203 an SDR screen showed the SDR picture twice as bright and clipped), an HDR frame brought to SDR white with its hue for screenshots and SDR drawables; RGB10A2 windows are PQ only once the status has answered HDR. A PQ layer rather than an extended-range float one, which showed black blocks over flat areas on a MacBook Air M5 while the game's frames were clean.
- With metadata in fullscreen (DM_PRESENT_AFTER_GPU), the drawable is handed to the system once its present pass has completed, without signalDrawable, which with the pass completed made frames a refresh late; it costs frames a refresh late when the pass ends close to the refresh, so it is off without metadata and in a window.
- dm/gfx/tools/edr_probe.m measures how macOS maps a PQ layer into EDR; dm/gfx/tools/hdr_rects.mm reproduces the rectangles with no game.
- t_present_hdr: the status answer, the automatic peak and its hold, the PQ decode and encode, the roll-off's curve, SDR and float sources, screenshots; test_settings: the rows and their per-platform defaults.
- Notes: Donkey Kong Bananza, Mario Kart World and Ocarina of Time's HDR windows and what each declares; three reference entries in docs/reference/rendering.md.

The PQ layer is beecrepaldi-afk's (#1), the present after the GPU and the standalone app that found the rectangles b1tdr3amer24's (#2), the paper white and peak rows and the automatic peak's front end hook Amost's (#3).

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 output 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>
maxdavidson pushed a commit that referenced this pull request Nov 11, 2026
…en that can show EDR, the game's HDR10 window goes as it is into an RGB10A2 BT.2100 PQ layer, and the peak follows the screen's current headroom with no HDR10 metadata on the layer, on the Mac and iOS

- hle_vi.c: with DM_HDR=1 (the HDR output row, on by default) vi::QueryDisplayHdrStatus answers HDR on, format 1, while the window's screen can show EDR (DM_HDR=2: on any screen), and the game asks every frame, so its window follows the screen; the iOS in-game menu turns it on and off while the game runs. The paper white (DM_HDR_PAPER_WHITE, the HDR paper white row) is 100 nits by default, the screen's own white: macOS shows a PQ layer at EDR = nits / 100, measured with an extended-range screen capture, so the game's normal picture matches SDR and the headroom is left to its highlights (by eye, Mario Kart World at 100 had SDR's base brightness with brighter highlights; at 203 the whole picture was brighter).
- The peak (DM_HDR_PEAK, the HDR peak row) is automatic by default: 100 nits times the screen's current headroom (front_hdr_headroom: NSScreen's maximumExtendedDynamicRangeColorComponentValue on the Mac, UIScreen's currentEDRHeadroom on iOS), in steps of about 6 % taken once they have held for a second, at least twice the paper white, so the game tone-maps into what the screen shows. Donkey Kong Bananza and Ocarina of Time declare within the peak they are told, and all three games' pixels stay within it.
- No HDR10 metadata on the layer (DM_HDR_META=1 adds it): with metadata above 100 x headroom Core Animation compresses the whole picture, mid-tones included (measured), and Mario Kart World declares about 1.8 times the peak it is told; and in fullscreen a layer with metadata of any range showed rectangles of older frames under GPU load, in the host and in a standalone app with no game, judged by eye without knowing the mode (dm/gfx/tools/hdr_rects.mm), none without. A fixed peak above the screen's is rolled off in the present above three quarters of the screen's held peak (present.metal mode 4), keyed on the smaller of the declared range and the peak told.
- present.metal and nvn_mtl.m present_mode: the HDR10 window as it is into the PQ layer, an SDR or float source encoded at the paper white while the status answers HDR and at 100 nits, the screen's SDR white, while it answers SDR (at 203 an SDR screen showed the SDR picture twice as bright and clipped), an HDR frame brought to SDR white with its hue for screenshots and SDR drawables; RGB10A2 windows are PQ only once the status has answered HDR. A PQ layer rather than an extended-range float one, which showed black blocks over flat areas on a MacBook Air M5 while the game's frames were clean.
- With metadata in fullscreen (DM_PRESENT_AFTER_GPU), the drawable is handed to the system once its present pass has completed, without signalDrawable, which with the pass completed made frames a refresh late; it costs frames a refresh late when the pass ends close to the refresh, so it is off without metadata and in a window.
- dm/gfx/tools/edr_probe.m measures how macOS maps a PQ layer into EDR; dm/gfx/tools/hdr_rects.mm reproduces the rectangles with no game.
- t_present_hdr: the status answer, the automatic peak and its hold, the PQ decode and encode, the roll-off's curve, SDR and float sources, screenshots; test_settings: the rows and their per-platform defaults.
- Notes: Donkey Kong Bananza, Mario Kart World and Ocarina of Time's HDR windows and what each declares; three reference entries in docs/reference/rendering.md.

The PQ layer is beecrepaldi-afk's (#1), the present after the GPU and the standalone app that found the rectangles b1tdr3amer24's (#2), the paper white and peak rows and the automatic peak's front end hook Amost's (#3).

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 output 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>
maxdavidson pushed a commit that referenced this pull request Nov 12, 2026
…eedds#1352)

Civilization VII 1.5.0.1 crashed on the Mac with CRASH sig=10 addr=0
pc=main+0x4e8a18 on its UI's second screen change: four hand-written
blocks in its main save x1-x17 in a 0x98-byte frame around svc #0x29, which
leaves sp 8 bytes off. Horizon allows that, Apple's kernel faults on it, as
with sub sp, sp, xN (site_sp_align). odd_sp_frames rounds both immediates of
such a pair up to 16 in place, only when the matching add follows within 64
instructions and nothing between them branches, writes or copies sp, uses
writeback or reaches past the frame.

On top of the PR: a frame of 0xff1-0xfff rounds to 0x1000, which the
unshifted 12-bit field cannot hold; written as before it became
sub sp, sp, #0, lsl seedds#12, a frame of nothing whose stores land on the
caller's stack. sp_adjust writes it as #1, lsl seedds#12, and test_sp_frame.py
covers it. The search now finds candidates by a one-byte regex like
exclusive_order_sites instead of walking every word: 2.71 s -> 0.08 s on
PRAGMATA's 134 MiB main.

Checked: test_sp_frame.py (8), the 16 test files that use patch_nso.py,
test_docs.py, lint_python.py --against origin/main (nothing new). The
search finds no odd sub/add sp, #imm word in 42 modules (main, rtld, sdk,
subsdk*) of 10 local games, so their rewrites are unchanged; the PR author
measured TotK and MGS4 byte-identical and Civilization VII 8 words changed,
14 minutes (49,800 frames) without a crash. regress.py not run: no local
game has such a frame. Supersedes PR seedds#1666 (every odd immediate rounded
unpaired, which would move incoming stack arguments).

Co-authored-by: Xiaofeng Han <15362031+xiaofeng-han@users.noreply.github.com>
maxdavidson pushed a commit that referenced this pull request Nov 12, 2026
…en that can show EDR, the game's HDR10 window goes as it is into an RGB10A2 BT.2100 PQ layer, and the peak follows the screen's current headroom with no HDR10 metadata on the layer, on the Mac and iOS

- hle_vi.c: with DM_HDR=1 (the HDR output row, on by default) vi::QueryDisplayHdrStatus answers HDR on, format 1, while the window's screen can show EDR (DM_HDR=2: on any screen), and the game asks every frame, so its window follows the screen; the iOS in-game menu turns it on and off while the game runs. The paper white (DM_HDR_PAPER_WHITE, the HDR paper white row) is 100 nits by default, the screen's own white: macOS shows a PQ layer at EDR = nits / 100, measured with an extended-range screen capture, so the game's normal picture matches SDR and the headroom is left to its highlights (by eye, Mario Kart World at 100 had SDR's base brightness with brighter highlights; at 203 the whole picture was brighter).
- The peak (DM_HDR_PEAK, the HDR peak row) is automatic by default: 100 nits times the screen's current headroom (front_hdr_headroom: NSScreen's maximumExtendedDynamicRangeColorComponentValue on the Mac, UIScreen's currentEDRHeadroom on iOS), in steps of about 6 % taken once they have held for a second, at least twice the paper white, so the game tone-maps into what the screen shows. Donkey Kong Bananza and Ocarina of Time declare within the peak they are told, and all three games' pixels stay within it.
- No HDR10 metadata on the layer (DM_HDR_META=1 adds it): with metadata above 100 x headroom Core Animation compresses the whole picture, mid-tones included (measured), and Mario Kart World declares about 1.8 times the peak it is told; and in fullscreen a layer with metadata of any range showed rectangles of older frames under GPU load, in the host and in a standalone app with no game, judged by eye without knowing the mode (dm/gfx/tools/hdr_rects.mm), none without. A fixed peak above the screen's is rolled off in the present above three quarters of the screen's held peak (present.metal mode 4), keyed on the smaller of the declared range and the peak told.
- present.metal and nvn_mtl.m present_mode: the HDR10 window as it is into the PQ layer, an SDR or float source encoded at the paper white while the status answers HDR and at 100 nits, the screen's SDR white, while it answers SDR (at 203 an SDR screen showed the SDR picture twice as bright and clipped), an HDR frame brought to SDR white with its hue for screenshots and SDR drawables; RGB10A2 windows are PQ only once the status has answered HDR. A PQ layer rather than an extended-range float one, which showed black blocks over flat areas on a MacBook Air M5 while the game's frames were clean.
- With metadata in fullscreen (DM_PRESENT_AFTER_GPU), the drawable is handed to the system once its present pass has completed, without signalDrawable, which with the pass completed made frames a refresh late; it costs frames a refresh late when the pass ends close to the refresh, so it is off without metadata and in a window.
- dm/gfx/tools/edr_probe.m measures how macOS maps a PQ layer into EDR; dm/gfx/tools/hdr_rects.mm reproduces the rectangles with no game.
- t_present_hdr: the status answer, the automatic peak and its hold, the PQ decode and encode, the roll-off's curve, SDR and float sources, screenshots; test_settings: the rows and their per-platform defaults.
- Notes: Donkey Kong Bananza, Mario Kart World and Ocarina of Time's HDR windows and what each declares; three reference entries in docs/reference/rendering.md.

The PQ layer is beecrepaldi-afk's (#1), the present after the GPU and the standalone app that found the rectangles b1tdr3amer24's (#2), the paper white and peak rows and the automatic peak's front end hook Amost's (#3).

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 output 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>
maxdavidson pushed a commit that referenced this pull request Nov 13, 2026
…en that can show EDR, the game's HDR10 window goes as it is into an RGB10A2 BT.2100 PQ layer, and the peak follows the screen's current headroom with no HDR10 metadata on the layer, on the Mac and iOS

- hle_vi.c: with DM_HDR=1 (the HDR output row, on by default) vi::QueryDisplayHdrStatus answers HDR on, format 1, while the window's screen can show EDR (DM_HDR=2: on any screen), and the game asks every frame, so its window follows the screen; the iOS in-game menu turns it on and off while the game runs. The paper white (DM_HDR_PAPER_WHITE, the HDR paper white row) is 100 nits by default, the screen's own white: macOS shows a PQ layer at EDR = nits / 100, measured with an extended-range screen capture, so the game's normal picture matches SDR and the headroom is left to its highlights (by eye, Mario Kart World at 100 had SDR's base brightness with brighter highlights; at 203 the whole picture was brighter).
- The peak (DM_HDR_PEAK, the HDR peak row) is automatic by default: 100 nits times the screen's current headroom (front_hdr_headroom: NSScreen's maximumExtendedDynamicRangeColorComponentValue on the Mac, UIScreen's currentEDRHeadroom on iOS), in steps of about 6 % taken once they have held for a second, at least twice the paper white, so the game tone-maps into what the screen shows. Donkey Kong Bananza and Ocarina of Time declare within the peak they are told, and all three games' pixels stay within it.
- No HDR10 metadata on the layer (DM_HDR_META=1 adds it): with metadata above 100 x headroom Core Animation compresses the whole picture, mid-tones included (measured), and Mario Kart World declares about 1.8 times the peak it is told; and in fullscreen a layer with metadata of any range showed rectangles of older frames under GPU load, in the host and in a standalone app with no game, judged by eye without knowing the mode (dm/gfx/tools/hdr_rects.mm), none without. A fixed peak above the screen's is rolled off in the present above three quarters of the screen's held peak (present.metal mode 4), keyed on the smaller of the declared range and the peak told.
- present.metal and nvn_mtl.m present_mode: the HDR10 window as it is into the PQ layer, an SDR or float source encoded at the paper white while the status answers HDR and at 100 nits, the screen's SDR white, while it answers SDR (at 203 an SDR screen showed the SDR picture twice as bright and clipped), an HDR frame brought to SDR white with its hue for screenshots and SDR drawables; RGB10A2 windows are PQ only once the status has answered HDR. A PQ layer rather than an extended-range float one, which showed black blocks over flat areas on a MacBook Air M5 while the game's frames were clean.
- With metadata in fullscreen (DM_PRESENT_AFTER_GPU), the drawable is handed to the system once its present pass has completed, without signalDrawable, which with the pass completed made frames a refresh late; it costs frames a refresh late when the pass ends close to the refresh, so it is off without metadata and in a window.
- dm/gfx/tools/edr_probe.m measures how macOS maps a PQ layer into EDR; dm/gfx/tools/hdr_rects.mm reproduces the rectangles with no game.
- t_present_hdr: the status answer, the automatic peak and its hold, the PQ decode and encode, the roll-off's curve, SDR and float sources, screenshots; test_settings: the rows and their per-platform defaults.
- Notes: Donkey Kong Bananza, Mario Kart World and Ocarina of Time's HDR windows and what each declares; three reference entries in docs/reference/rendering.md.

The PQ layer is beecrepaldi-afk's (#1), the present after the GPU and the standalone app that found the rectangles b1tdr3amer24's (#2), the paper white and peak rows and the automatic peak's front end hook Amost's (#3).

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 output 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>
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.

2 participants