diff --git a/dm/games/0400486024DEC000/README.md b/dm/games/0400486024DEC000/README.md index 267a4edafe..eb2a0562e7 100644 --- a/dm/games/0400486024DEC000/README.md +++ b/dm/games/0400486024DEC000/README.md @@ -70,6 +70,7 @@ With the HDR output setting, on the Mac app to the opening movie (M4 Pro, macOS - Like [Donkey Kong Bananza](../040074A01BF12000/README.md#hdr-output), the game boots an RGBA8 window (format 0x25, 2560x1440), queries `vi::QueryDisplayHdrStatus`, then makes an RGB10A2 window (0x3d). It asks every frame (1,000 queries in 38 s) and queries the paper white (203 nits). - It declares SDR 0 to 203 nits and extended 0 to 710, gamut 0 and tone mapping 0. No new `[RED]` line with HDR on. - On an LG HDR 4K monitor with HDR off in macOS (potential headroom 1.00), `DM_HDR=1` answered SDR and the game kept its RGBA8 window. `DM_HDR=2` (HDR on any screen) made it switch. Not run on a screen with EDR, and the picture was not compared between the modes. +- Fullscreen on a MacBook Pro M4 Pro (macOS 27.2, built-in screen, 2026-11-07; the oot2 fork with this HDR output): with the menu bar hidden, rectangles of older frames flickered over play and the opening movie looked like its frames were out of order, until the present waited for the GPU ([A fullscreen HDR layer shows rectangles of older frames](../../../docs/reference/rendering.md#a-fullscreen-hdr-layer-shows-rectangles-of-older-frames)). Fine since, with ProMotion and at a fixed 60 Hz. ## Open @@ -105,6 +106,7 @@ With the HDR output setting, on the Mac app to the opening movie (M4 Pro, macOS - [A texture in a sparse pool came out CPU-written at its first use](../../../docs/reference/memory.md#a-texture-in-a-sparse-pool-came-out-cpu-written-at-its-first-use): found by the unit test, on this profile's `tex_dirty_upload`. - [Textures made for GPU use kept from their CPU rewrites](../../../docs/reference/nvn-objects.md#textures-made-for-gpu-use-kept-from-their-cpu-rewrites): the terrain tiles' height, blend and hole maps rewritten as tile slots are reused; flat blue holes in the ground after an hour of play. - [The status query and the present for an HDR window](../../../docs/reference/rendering.md#the-status-query-and-the-present-for-an-hdr-window): the same HDR window as Donkey Kong Bananza. +- [A fullscreen HDR layer shows rectangles of older frames](../../../docs/reference/rendering.md#a-fullscreen-hdr-layer-shows-rectangles-of-older-frames): with the menu bar hidden on a MacBook Pro. - [`EventFd` needs no network](../../../docs/reference/services.md#eventfd-needs-no-network) - [No virtual audio input devices (SDK 22)](../../../docs/reference/audio.md#no-virtual-audio-input-devices-sdk-22) - [`nn::ndve`'s movie enhancement file](../../../docs/reference/shaders.md#nnndves-movie-enhancement-file) diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index a0107fd50f..1042fd0d14 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -14470,6 +14470,20 @@ static void present_shown_note(double shown, double call, double target, CAMetal } pthread_mutex_unlock(&mu); } +// DM_PRESENT_AFTER_GPU: hand the drawable to the system only once the display command buffer has completed on the GPU, not right after signalDrawable. +// Default on the Mac for an extended-range (HDR) layer, off otherwise and on iOS; 0 / 1 force it. A workaround: on a MacBook Pro M4 Pro (macOS 27.2) a fullscreen PQ layer +// with HDR10 metadata showed rectangles of older frames whenever nothing covered it, and a standalone app with the same present reproduced it; why the system shows a +// signalled drawable's older contents is open (docs/reference/rendering.md, "A fullscreen HDR layer shows rectangles of older frames") +#if !TARGET_OS_SIMULATOR +static int present_after_gpu(CAMetalLayer *layer) { + static int env = -2, was = -1; + if (env == -2) { const char *e = getenv("DM_PRESENT_AFTER_GPU"); env = e && *e ? atoi(e) != 0 : -1; } + int on = env >= 0 ? env : TARGET_OS_OSX && layer.wantsExtendedDynamicRangeContent; + if (on != was) { was = on; MLOG("present: the drawable goes to the system %s (DM_PRESENT_AFTER_GPU%s)", on ? "after its GPU work completes" : "right after signalDrawable", + env >= 0 ? "" : on ? " unset: on for the Mac's HDR layer" : " unset: off"); } + return on; +} +#endif static int present_now(const tex_t *t, double period, uint64_t frame, uint64_t ticket, const int crop[4], id supplied, double target_ts) { if (g_present_stall_ns) { struct timespec ts = {0, (long)g_present_stall_ns}; nanosleep(&ts, NULL); } g_present_n++; @@ -14553,9 +14567,32 @@ static int present_now(const tex_t *t, double period, uint64_t frame, uint64_t t if (!kind) { [cb endCommandBuffer]; [g_q4 signalDrawable:dr]; return 0; } int fx_kind = kind == PRESENT_METALFX; [cb endCommandBuffer]; + // The drawable's hand-over to the system: right after signalDrawable, or with DM_PRESENT_AFTER_GPU only once the display command buffer has completed (present_after_gpu) +#if TARGET_OS_SIMULATOR // The iOS simulator SDK has no presentation-time callback and no minimum display duration (used only for picture acceptance, display-link ticks are not counted) + int late = 0; + dispatch_block_t show = ^{ (void)period; [dr present]; }; +#else + int late = present_after_gpu(layer); + double shown_call = CACurrentMediaTime(), shown_target = target_ts; // DM_PRESENT_SHOWN: when the frame was handed to the system, and the display-link tick it was meant for + uint64_t pn = ticket; // Record the present time by the game's present sequence number (ticket number): hle's present statistics are taken by "the game's nth present"; in the display link path the actual present count != the game's present count (old frames get merged and dropped) + [dr addPresentedHandler:^(id d) { // Actual presentation time (the time this frame is really put on screen, CACurrentMediaTime base) + CFTimeInterval tt = d.presentedTime; + g_pres[pn % PRING] = (pres_ent){pn, tt > 0 ? tt : -1}; + if (present_shown_on()) present_shown_note(tt, shown_call, shown_target, layer); + }]; + // Each frame is displayed for at least one present period: when the GPU completion time jitters around a 120 Hz refresh boundary, the paired "25 ms + 8.33 ms" stutter no longer appears + dispatch_block_t show = ^{ + if (target_ts > 0) [dr present]; + else if (period > 0) [dr presentAfterMinimumDuration:period]; else [dr present]; + }; +#endif + // Late: whichever of signalDrawable (this thread) and the GPU completion (feedback handler) comes second presents, so the drawable is never presented before it is signalled + _Atomic int *need = late ? malloc(sizeof *need) : NULL; + if (need) atomic_init(need, 2); MTL4CommitOptions *o = [MTL4CommitOptions new]; [o addFeedbackHandler:^(id f) { if (f.error) { MLOG("[RED] display command buffer error: %s", f.error.description.UTF8String); gpu_lost_check(f.error, "display"); } + if (need && atomic_fetch_sub(need, 1) == 1) { free(need); show(); } pthread_mutex_lock(&g_pq_mu); if (ticket > g_pq_done) g_pq_done = ticket; if (ticket > g_present_alloc_done[alloc_i]) g_present_alloc_done[alloc_i] = ticket; g_pq_inflight--; pthread_cond_broadcast(&g_pq_cv); present_gpu_note(f.GPUEndTime - f.GPUStartTime, fx_kind); pthread_mutex_unlock(&g_pq_mu); }]; @@ -14579,20 +14616,8 @@ static int present_now(const tex_t *t, double period, uint64_t frame, uint64_t t } [g_q4 signalDrawable:dr]; g_present_alloc_fence[alloc_i] = ticket; -#if TARGET_OS_SIMULATOR // The iOS simulator SDK has no presentation-time callback and no minimum display duration (used only for picture acceptance, display-link ticks are not counted) - (void)period; [dr present]; -#else - double shown_call = CACurrentMediaTime(), shown_target = target_ts; // DM_PRESENT_SHOWN: when the frame was handed to the system, and the display-link tick it was meant for - uint64_t pn = ticket; // Record the present time by the game's present sequence number (ticket number): hle's present statistics are taken by "the game's nth present"; in the display link path the actual present count != the game's present count (old frames get merged and dropped) - [dr addPresentedHandler:^(id d) { // Actual presentation time (the time this frame is really put on screen, CACurrentMediaTime base) - CFTimeInterval tt = d.presentedTime; - g_pres[pn % PRING] = (pres_ent){pn, tt > 0 ? tt : -1}; - if (present_shown_on()) present_shown_note(tt, shown_call, shown_target, layer); - }]; - // Each frame is displayed for at least one present period: when the GPU completion time jitters around a 120 Hz refresh boundary, the paired "25 ms + 8.33 ms" stutter no longer appears - if (target_ts > 0) [dr present]; - else if (period > 0) [dr presentAfterMinimumDuration:period]; else [dr present]; -#endif + if (!need) show(); + else if (atomic_fetch_sub(need, 1) == 1) { free(need); show(); } (void)frame; return 1; } diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index 33ce7eae93..5d631910b8 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -675,6 +675,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - a pass on a very large target costs about the same however little it draws; `DM_NVN_GPUPROF` shows time proportional to the target's area. → [A render area for passes on very large targets](../reference/rendering.md#a-render-area-for-passes-on-very-large-targets) - `[RED] NVN unimplemented function called: nvnCommandBufferShadingRateImagePalette`, or `nvnCommandBufferBindShadingRateImage` or `nvnCommandBufferSetShadingRatePaletteMode`, every frame. → [Variable rate shading ignored](../reference/rendering.md#variable-rate-shading-ignored) - no HDR output on a Switch 2 game that asks for its HDR status: its window stays RGBA8 and the picture is SDR. → [The status query and the present for an HDR window](../reference/rendering.md#the-status-query-and-the-present-for-an-hdr-window) +- with HDR output in fullscreen and nothing over the game's layer (the menu bar hidden, no frame-rate counter), big rectangles of the picture show an older frame for a frame; a movie looks like its frames are out of order. Showing the menu bar, or any view over the layer, stops it at once. SDR is clean, and the host's frame times are clean throughout. → [A fullscreen HDR layer shows rectangles of older frames](../reference/rendering.md#a-fullscreen-hdr-layer-shows-rectangles-of-older-frames) - angle-dependent character smearing, or vertex-only pipeline failures with `invalid depth/stencil pipeline formats 16843012/260`, although the pass began with valid `260/260` formats. → [Ten attachment records for eight colors and depth-stencil](../reference/rendering.md#ten-attachment-records-for-eight-colors-and-depth-stencil) - Metal aborts at pipeline creation: `depthAttachmentPixelFormat (MTLPixelFormatInvalid) and stencilAttachmentPixelFormat (MTLPixelFormatDepth32Float_Stencil8) must match`. → [Combined depth-stencil formats stay paired in passes and pipeline keys](../reference/rendering.md#combined-depth-stencil-formats-stay-paired-in-passes-and-pipeline-keys) - undefined values blended into attachments the fragment shader leaves alone. → [No writes to colour outputs the shader does not write](../reference/rendering.md#no-writes-to-colour-outputs-the-shader-does-not-write) diff --git a/docs/env_switches.tsv b/docs/env_switches.tsv index 2a8ecd9f33..bf33576ac7 100644 --- a/docs/env_switches.tsv +++ b/docs/env_switches.tsv @@ -243,6 +243,7 @@ DM_PHONE_MOTION switch On iOS, whether the phone's own device motion drives play DM_PICTURE_FIT player `fill` scales the 16:9 picture on the iPhone and iPad until it covers the screen in landscape and crops the edges (a portrait or tabletop split and a TV stay fitted); `fit` or unset, the largest 16:9 rectangle with black bars. Launcher and in-game menu setting DM_PREEMPT switch `0` / `1` overrides the profile's `core_preempt`: also check already-runnable higher-priority peers on the caller's core after every thread SVC DM_PRELOAD diagnostic Set: on the Mac, runs the host's early background loading (`dm_host_preload_start`) as the iOS app does, to exercise that path +DM_PRESENT_AFTER_GPU switch `1` hands each drawable to the system only once its display command buffer has completed, `0` right after signalDrawable (default: on for the Mac's HDR layer, off otherwise and on iOS); without it a fullscreen HDR layer showed rectangles of older frames on an M4 Pro (macOS 27.2) while nothing covered it DM_PRESENT_FIFO switch `0` makes the display link take the newest queued frame each tick, as before; by default it shows frames in order, at most one tick behind DM_PRESENT_HZ switch `=`: the frequency of presentation pacing (default 60); `0` means no frame limit and is for performance profiling only DM_PRESENT_INTERVAL switch `display` presents one frame per display-link tick whatever interval the game sets; `game` follows the game's present interval (default: the profile's `present_interval`) diff --git a/docs/reference/rendering.md b/docs/reference/rendering.md index 4b06387962..c58e2eada3 100644 --- a/docs/reference/rendering.md +++ b/docs/reference/rendering.md @@ -708,9 +708,18 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E - The layer carries HDR10 metadata (`CAEDRMetadata`: the range is the one the game declares), so Core Animation maps the highlights into the screen's current headroom, following brightness and the screen's modes. The present writes them as they are. The metadata's `opticalOutputScale` (the paper white) counts only for the float layer, whose 1.0 it says the luminance of: for a normalized format such as RGB10A2, Core Animation takes it as 10,000 (`CAEDRMetadata.h`), so the PQ layer's codes are absolute nits. The SDR white is at the paper white in both. Apple's Metal HUD (`DM_METAL_HUD`) is drawn into the layer with SDR values unconverted: on the PQ layer its white is a code of 1.0, 10,000 nits, so it shows at the screen's peak. A screenshot of an HDR frame brings each highlight's largest channel to SDR white and keeps its hue. - Before this, the present rolled the highlights off into the headroom itself. On [Mario Kart World](../../dm/games/0400C3F00006E000/README.md#hdr-output) the two looked alike by eye and reached the screen in the same time in a window, so the system's mapping replaced it. - **Check:** `make -C dm/gfx/tests GAME= run` runs `t_present_hdr`: the status answer on a screen with and without EDR, PQ codes from 0 to 10,000 nits, SDR sources, screenshots with a highlight's hue, the PQ layer (the HDR10 window as it is, SDR and float sources at the paper white), and a 10-bit window of a game that never asked for HDR. `DM_PRESENT_SHOWN=1` logs how long frames take to reach the screen. -- **Open:** fullscreen (where the system may show the layer without compositing it) is not timed. On an iPhone 17 Pro (iOS 27.2, headroom 8), Donkey Kong Bananza shows HDR on the PQ layer; there, with HDR on only, the picture jumps back a frame now and then while the frame rate cannot hold 60 or 30 (with HDR off it holds a frame instead), cause not found. Whether the float layer's blocks are in its contents or in the compositor: a readback of the float layer while they show would tell. The window's primaries are settled in the Donkey Kong Bananza note. +- **Open:** fullscreen (where the system may show the layer without compositing it) is not timed; it showed rectangles of older frames on a MacBook Pro until the present waited for the GPU ([next entry](#a-fullscreen-hdr-layer-shows-rectangles-of-older-frames)). On an iPhone 17 Pro (iOS 27.2, headroom 8), Donkey Kong Bananza shows HDR on the PQ layer; there, with HDR on only, the picture jumps back a frame now and then while the frame rate cannot hold 60 or 30 (with HDR off it holds a frame instead), cause not found. Whether the float layer's blocks are in its contents or in the compositor: a readback of the float layer while they show would tell. The window's primaries are settled in the Donkey Kong Bananza note. - **Found on:** [Donkey Kong Bananza](../../dm/games/040074A01BF12000/README.md#hdr-output); [Mario Kart World](../../dm/games/0400C3F00006E000/README.md#hdr-output), the same window, to the title. [Ocarina of Time](../../dm/games/0400486024DEC000/README.md#hdr-output), the same window, to the opening movie. +### A fullscreen HDR layer shows rectangles of older frames + +- **Symptom:** with HDR output in fullscreen and nothing over the game's layer (the menu bar hidden, no frame-rate counter), big rectangles of the picture show an older frame for a frame; a movie looks like its frames are out of order. Showing the menu bar, or any view over the layer, stops it at once. SDR is clean, and the host's frame times are clean throughout. +- **Cause:** the frames the host hands over are complete; the system shows older contents of the drawable. A standalone Cocoa app with this present reproduces it with no game: the PQ layer with HDR10 metadata (`CAEDRMetadata`), a `CAMetalDisplayLink` at 60 Hz with frame latency 2, the Metal 4 present (`waitForDrawable`, commit, `signalDrawable`, `present`), the drawable sized to the view in pixels, and about 12 ms of GPU work a frame. With no GPU load, or with a drawable smaller than the screen, it did not show. One change at a time removed it: a classic `presentDrawable:` on an `MTLCommandQueue`; the Metal 4 queue's work followed by an `MTLCommandQueue` that waits on a shared event and presents; no HDR10 metadata; an SDR layer; an `NSView` over the layer; `present` called only after the commit feedback reported completion. The float layer (`DM_HDR_LAYER=float`) showed garbage of its own; frame latency 1 and the link on the main thread changed nothing. The drawables' `presentedTime` stayed 16.7 ms apart in every mode; in the flickering one each frame showed a refresh after the link's target, in the others on it. So a signalled drawable reaches a screen that shows the layer without compositing before its GPU work has landed. Where the system goes wrong is **open**, and it is likely a macOS bug: the present follows Apple's documented Metal 4 order. +- **Fix:** a workaround: `present_now` (`nvn_mtl.m`) calls `present` from the display command buffer's commit feedback handler, once `signalDrawable` has been called and the GPU has completed the buffer (whichever comes second presents). `DM_PRESENT_AFTER_GPU` (`present_after_gpu`): default on the Mac when the layer wants extended range, off otherwise; `0` / `1` force it. The frames reach the screen on the link's target, and their on-screen times stay 16.7 ms apart. +- **Check:** `t_present_hdr`, `t_present_alloc`, `t_present_rescue` and `t_present_snap` pass with the default and with `DM_PRESENT_AFTER_GPU=1`. They do not show the flicker, which needs a screen showing the layer directly; checked by eye in the game and in the standalone app. +- **Open:** iOS is untested, so the default leaves it off there. The iPhone's picture that jumps back a frame with HDR on (previous entry) may be the same; `DM_PRESENT_AFTER_GPU=1` would tell. The float layer with this present is untested. +- **Found on:** [Ocarina of Time](../../dm/games/0400486024DEC000/README.md#hdr-output), MacBook Pro M4 Pro (built-in screen), macOS 27.2, with ProMotion and at a fixed 60 Hz. + ## Blend, depth and stencil ### Ten attachment records for eight colors and depth-stencil