From 8c646e2f25b12fcfcf3c8d7aada1817212797470 Mon Sep 17 00:00:00 2001 From: Oliver Ames Date: Sat, 7 Nov 2026 10:22:14 -0500 Subject: [PATCH 1/2] deploy: the layers peak-HDR metadata calibrates to the device model (front_ios.m reads the hardware model; 1,600 nits on the modern panels), and DM_HDR_EDR_MAX overrides --- dm/gfx/nvn/nvn_hdr.h | 3 +++ dm/gfx/nvn/nvn_hdr_layer.m | 16 ++++++++++++++-- dm/ios/Sources/front_ios.m | 17 +++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/dm/gfx/nvn/nvn_hdr.h b/dm/gfx/nvn/nvn_hdr.h index 90f9faf741..e9eabf31ee 100644 --- a/dm/gfx/nvn/nvn_hdr.h +++ b/dm/gfx/nvn/nvn_hdr.h @@ -14,5 +14,8 @@ void dm_hdr_luminance(int extended, uint16_t low, uint16_t high); void dm_hdr_luminance_range(int extended, uint16_t *low, uint16_t *high); // Called on the UI thread before the guest starts, and when its screen changes. void dm_hdr_configure_layer(void *layer, double potential_headroom); +// The device's own peak HDR brightness in nits (front ends publish it by the hardware model); +// the layers' mastering metadata tops out at it when it is below the game's own peak +void dm_hdr_peak(double peak_nits); // Called by the present owner; applies content metadata for following drawables. void dm_hdr_layer_metadata(void *layer, int hdr_content); diff --git a/dm/gfx/nvn/nvn_hdr_layer.m b/dm/gfx/nvn/nvn_hdr_layer.m index e87ce47b87..9ef5d59bad 100644 --- a/dm/gfx/nvn/nvn_hdr_layer.m +++ b/dm/gfx/nvn/nvn_hdr_layer.m @@ -3,6 +3,18 @@ #import #include "nvn_hdr.h" #include +// The device's own peak HDR brightness (nits), set by the front end from the hardware model +// (front_ios.m); 0 until then, in which case the layer keeps the mastering metadata it would +// have had. The environmental DM_HDR_EDR_MAX (a ratio of paper white) overrides it. +static double g_hdr_peak; +void dm_hdr_peak(double peak_nits) { if (peak_nits >= 0) g_hdr_peak = peak_nits; } +static double hdr_peak_clamp(double nits) { + const char *em = getenv("DM_HDR_EDR_MAX"); + double clamp = em && *em ? strtod(em, NULL) : 0; + if (isfinite(clamp) && clamp > 1.0) { double cap = DM_HDR_PAPER_WHITE * clamp; return nits < cap ? nits : cap; } + if (g_hdr_peak > 0 && nits > g_hdr_peak) return g_hdr_peak; + return nits; +} void dm_hdr_configure_layer(void *ptr, double headroom) { CAMetalLayer *layer = (__bridge CAMetalLayer *)ptr; int requested = dm_hdr_requested(); @@ -14,7 +26,7 @@ void dm_hdr_configure_layer(void *ptr, double headroom) { CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceExtendedLinearITUR_2020); layer.colorspace = cs; CGColorSpaceRelease(cs); layer.wantsExtendedDynamicRangeContent = YES; - if (!layer.EDRMetadata) layer.EDRMetadata = [CAEDRMetadata HDR10MetadataWithMinLuminance:0 maxLuminance:DM_HDR_MASTERING_PEAK opticalOutputScale:DM_HDR_LINEAR_UNIT]; + if (!layer.EDRMetadata) layer.EDRMetadata = [CAEDRMetadata HDR10MetadataWithMinLuminance:0 maxLuminance:hdr_peak_clamp(DM_HDR_MASTERING_PEAK) opticalOutputScale:DM_HDR_LINEAR_UNIT]; } void dm_hdr_layer_metadata(void *ptr, int hdr_content) { CAMetalLayer *layer = (__bridge CAMetalLayer *)ptr; @@ -25,6 +37,6 @@ void dm_hdr_layer_metadata(void *ptr, int hdr_content) { static uint32_t last_range; uint32_t range = lo | (uint32_t)hi << 16; if (last_layer == layer && last_range == range) return; - layer.EDRMetadata = [CAEDRMetadata HDR10MetadataWithMinLuminance:lo maxLuminance:hi opticalOutputScale:DM_HDR_LINEAR_UNIT]; + layer.EDRMetadata = [CAEDRMetadata HDR10MetadataWithMinLuminance:lo maxLuminance:hdr_peak_clamp(hi) opticalOutputScale:DM_HDR_LINEAR_UNIT]; last_layer = layer; last_range = range; } diff --git a/dm/ios/Sources/front_ios.m b/dm/ios/Sources/front_ios.m index 02fe9d3729..d207916f2a 100644 --- a/dm/ios/Sources/front_ios.m +++ b/dm/ios/Sources/front_ios.m @@ -6,6 +6,7 @@ #import #import #include +#include #include "front.h" #include "nvn_hdr.h" #include "hle.h" @@ -45,6 +46,22 @@ - (void)didMoveToWindow { // the phone's screen or a TV's (DmTVSceneDelegate): [super didMoveToWindow]; UIScreen *s = self.window.windowScene.screen; if (!s) { dm_hdr_display(0); return; } + // The device's own peak HDR brightness (nits), from Apple's published display specs by the + // hardware model: the mastering metadata the layers publish tops out where the display reads, + // so the PQ range is calibrated to the screen instead of always 1,000 nits (the foldable's + // panel is of the 1,600-nit class like the Pro phones). A model this table does not know keeps + // the layer's own mastering peak, and DM_HDR_EDR_MAX (a ratio of paper white) still overrides. + struct utsname un; uname(&un); + const char *m = un.machine; NSString *model = m ? [NSString stringWithUTF8String:m] : @""; + double peak = 0; // 0 = unknown: the layer keeps the mastering metadata it would have had + if ([model hasPrefix:@"iPhone"] && [model length] > 6) { + NSUInteger gen = [[model substringWithRange:NSMakeRange(6, [model length] - 6)] integerValue]; + if (gen >= 12) peak = 1600; // 1,600 nits on the 12 Pro and later, the modern peak the user's panels read + else if (gen >= 10) peak = 1200; + } else if ([model hasPrefix:@"iPad14"]) { + peak = 1600; // the M-series iPad XDR panels + } + dm_hdr_peak(peak); // nvn_hdr_layer.m: the layers' mastering metadata peak is the model's own // Advertise HDR only after configuring the actual phone/TV drawable. The // shared present pass already decodes negotiated PQ/scRGB into this layer. dm_hdr_configure_layer((__bridge void *)self.layer, s.potentialEDRHeadroom); From acaedf150a1c5a788f584c734674e63223ab4813 Mon Sep 17 00:00:00 2001 From: Oliver Ames Date: Sat, 7 Nov 2026 12:34:25 -0500 Subject: [PATCH 2/2] The game is told the device's HDR peak, not a fixed 1,000 nits QueryDisplayHdrStatus and the layer's mastering metadata now report the front end's model peak when it published one (1,600 nits for the iPhone 12 Pro and later), else the virtual 1,000. DM_HDR_EDR_MAX still caps it. Stacked on #1383. --- dm/games/0400C3F00006E000/README.md | 2 ++ dm/gfx/hle_vi.c | 4 ++-- dm/gfx/nvn/nvn_hdr.h | 1 + dm/gfx/nvn/nvn_hdr_layer.m | 8 +++++++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/dm/games/0400C3F00006E000/README.md b/dm/games/0400C3F00006E000/README.md index d1c100289d..02eebce4eb 100644 --- a/dm/games/0400C3F00006E000/README.md +++ b/dm/games/0400C3F00006E000/README.md @@ -117,6 +117,8 @@ Current, 2026-11-03: an isolated iOS build defaults to handheld mode and request The guest's fixed heap and virtual reservations are preserved. iOS uses 32 MiB upload batches ([mechanism and fixture](../../../docs/reference/memory.md#upload-staging-grows-within-one-rendering-submission)). `memory-budget.tsv` in Documents records footprint, available memory, their sum, peak footprint and graphics ledger every second from preload. No Metal device is initialized by this monitor. The TSV and `log.txt` are needed to establish the actual MKW high-water mark on the phone. No arbitrary guest-page discard or host swap mechanism is enabled. Fitting the device budget, frame pacing and gameplay remain unverified on iOS. +HDR peak by device (2026-11-07, an iPhone 19,4): the game is told the phone's own peak (1,600 nits for the iPhone 12 Pro and later, `front_ios.m`) instead of a fixed 1,000 in `QueryDisplayHdrStatus`, and the layer metadata carries the same value (`dm_hdr_mastering_peak`, `nvn_hdr_layer.m`); `DM_HDR_EDR_MAX` (a ratio of paper white) still caps it. On the phone it looks good in a race; no comparison against the 1,000-nit value was measured, and a model absent from the table keeps 1,000. + ## Open - `nvnCommandBufferShadingRateImagePalette` and `BindShadingRateImage` are not implemented (about 2,800 calls a race): the game draws at the full pixel rate. Unknown names: `nvn*CooperativeVectorMatrix*`, `nvn*PoolWriteDescriptorTableData`. diff --git a/dm/gfx/hle_vi.c b/dm/gfx/hle_vi.c index 74678d7e57..3f8b0f7858 100644 --- a/dm/gfx/hle_vi.c +++ b/dm/gfx/hle_vi.c @@ -83,8 +83,8 @@ HLE_FORCE("_ZN2nn2vi8FinalizeEv", "display service shutdown; the host has no vi HLE_FORCE("_ZN2nn2vi21QueryDisplayHdrStatusEPbPNS0_15HdrOutputFormatEPtPKNS0_7DisplayE", "display HDR status: EDR capability of the current host screen", Result, h_vi_HdrStatus, uint8_t *on, uint32_t *fmt, uint16_t *peak, const void *disp) { (void)disp; int enabled = dm_hdr_query(); - if (on) *on = enabled; if (fmt) *fmt = enabled ? 1 : 0; if (peak) *peak = enabled ? DM_HDR_MASTERING_PEAK : 0; - static int last = -1; if (last != enabled) { LOG("vi HDR: %s, virtual peak %d nits, paper white %d nits", enabled ? "enabled (EDR display)" : "off", enabled ? DM_HDR_MASTERING_PEAK : 0, DM_HDR_PAPER_WHITE); last = enabled; } + if (on) *on = enabled; if (fmt) *fmt = enabled ? 1 : 0; if (peak) *peak = enabled ? (uint16_t)dm_hdr_mastering_peak() : 0; + static int last = -1; if (last != enabled) { LOG("vi HDR: %s, virtual peak %d nits, paper white %d nits", enabled ? "enabled (EDR display)" : "off", enabled ? dm_hdr_mastering_peak() : 0, DM_HDR_PAPER_WHITE); last = enabled; } return RES_OK; } HLE_FORCE("_ZN2nn2vi25QueryDisplayHdrPaperWhiteEPtPKNS0_7DisplayE", "display HDR paper white: 203 nits (BT.2408)", Result, h_vi_PaperWhite, uint16_t *nits, const void *disp) { (void)disp; if (nits) *nits = DM_HDR_PAPER_WHITE; return RES_OK; } diff --git a/dm/gfx/nvn/nvn_hdr.h b/dm/gfx/nvn/nvn_hdr.h index e9eabf31ee..de6a2f0f13 100644 --- a/dm/gfx/nvn/nvn_hdr.h +++ b/dm/gfx/nvn/nvn_hdr.h @@ -17,5 +17,6 @@ void dm_hdr_configure_layer(void *layer, double potential_headroom); // The device's own peak HDR brightness in nits (front ends publish it by the hardware model); // the layers' mastering metadata tops out at it when it is below the game's own peak void dm_hdr_peak(double peak_nits); +int dm_hdr_mastering_peak(void); // what the game is told: the device peak when known, else DM_HDR_MASTERING_PEAK // Called by the present owner; applies content metadata for following drawables. void dm_hdr_layer_metadata(void *layer, int hdr_content); diff --git a/dm/gfx/nvn/nvn_hdr_layer.m b/dm/gfx/nvn/nvn_hdr_layer.m index 9ef5d59bad..04cac50635 100644 --- a/dm/gfx/nvn/nvn_hdr_layer.m +++ b/dm/gfx/nvn/nvn_hdr_layer.m @@ -15,6 +15,12 @@ static double hdr_peak_clamp(double nits) { if (g_hdr_peak > 0 && nits > g_hdr_peak) return g_hdr_peak; return nits; } +// The peak the game is told its display reaches (QueryDisplayHdrStatus): the device's own peak when the front end published one, else the virtual 1,000 nits. A game tone-maps to +// this number, so the 1,600-nit phones were shown a 1,000-nit display; DM_HDR_EDR_MAX (a ratio of paper white) still caps it +int dm_hdr_mastering_peak(void) { + double p = g_hdr_peak > 0 ? g_hdr_peak : DM_HDR_MASTERING_PEAK; + return (int)hdr_peak_clamp(p); +} void dm_hdr_configure_layer(void *ptr, double headroom) { CAMetalLayer *layer = (__bridge CAMetalLayer *)ptr; int requested = dm_hdr_requested(); @@ -26,7 +32,7 @@ void dm_hdr_configure_layer(void *ptr, double headroom) { CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceExtendedLinearITUR_2020); layer.colorspace = cs; CGColorSpaceRelease(cs); layer.wantsExtendedDynamicRangeContent = YES; - if (!layer.EDRMetadata) layer.EDRMetadata = [CAEDRMetadata HDR10MetadataWithMinLuminance:0 maxLuminance:hdr_peak_clamp(DM_HDR_MASTERING_PEAK) opticalOutputScale:DM_HDR_LINEAR_UNIT]; + if (!layer.EDRMetadata) layer.EDRMetadata = [CAEDRMetadata HDR10MetadataWithMinLuminance:0 maxLuminance:dm_hdr_mastering_peak() opticalOutputScale:DM_HDR_LINEAR_UNIT]; } void dm_hdr_layer_metadata(void *ptr, int hdr_content) { CAMetalLayer *layer = (__bridge CAMetalLayer *)ptr;