From 45da4e19c864b92acf216a88cc286f9d3a8e8397 Mon Sep 17 00:00:00 2001 From: actuallyitsnathaniel Date: Mon, 2 Nov 2026 12:24:26 -0800 Subject: [PATCH 1/2] fix HUD resize on macOS Metal present-thread window size --- patches/manifest.json | 4 + .../0004-live-cocoa-window-attributes.patch | 96 +++++++++++++++++++ .../v1.0.7/source-capsule-r2/manifest.json | 16 ++-- runtime-recomp/src/game/runtime_ui.cpp | 16 ++-- .../tests/live_lod_pipeline_tests.py | 10 ++ .../tests/macos_metal_overlay_tests.mm | 9 +- 6 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 patches/plume/0004-live-cocoa-window-attributes.patch diff --git a/patches/manifest.json b/patches/manifest.json index f8030845..03eb7308 100644 --- a/patches/manifest.json +++ b/patches/manifest.json @@ -321,6 +321,10 @@ { "path": "patches/plume/0003-compile-msl-source-shaders.patch", "sha256": "80947b240d7a3bd5e3f9434df7501fc54d130d4e06942e36705f2238d8fb3565" + }, + { + "path": "patches/plume/0004-live-cocoa-window-attributes.patch", + "sha256": "2cb8a57e9f0d9d344a9a2b4693aa4a8f98b0bf562a4b0d413b345f09c6be144b" } ] }, diff --git a/patches/plume/0004-live-cocoa-window-attributes.patch b/patches/plume/0004-live-cocoa-window-attributes.patch new file mode 100644 index 00000000..04f9567a --- /dev/null +++ b/patches/plume/0004-live-cocoa-window-attributes.patch @@ -0,0 +1,96 @@ +--- a/plume_apple.mm ++++ b/plume_apple.mm +@@ -95,28 +95,20 @@ + } + + void CocoaWindow::getWindowAttributes(CocoaWindowAttributes* attributes) const { +- if ([NSThread isMainThread]) { +- UIWindow *uiWindow = (__bridge UIWindow *)windowHandle; +- CGRect bounds = [uiWindow bounds]; +- CGFloat scaleFactor = getScaleFactor(uiWindow); ++ // Always read live bounds. The present/resize path calls this off the ++ // main thread; returning a stale cache (and only scheduling async refresh) ++ // leaves the Metal swapchain and overlay target at the pre-resize size. ++ UIWindow *uiWindow = (__bridge UIWindow *)windowHandle; ++ CGRect bounds = [uiWindow bounds]; ++ CGFloat scaleFactor = getScaleFactor(uiWindow); + +- { +- std::lock_guard lock(attributesMutex); +- const_cast(this)->cachedAttributes.x = 0; +- const_cast(this)->cachedAttributes.y = 0; +- const_cast(this)->cachedAttributes.width = (int)round(bounds.size.width * scaleFactor); +- const_cast(this)->cachedAttributes.height = (int)round(bounds.size.height * scaleFactor); ++ std::lock_guard lock(attributesMutex); ++ const_cast(this)->cachedAttributes.x = 0; ++ const_cast(this)->cachedAttributes.y = 0; ++ const_cast(this)->cachedAttributes.width = (int)round(bounds.size.width * scaleFactor); ++ const_cast(this)->cachedAttributes.height = (int)round(bounds.size.height * scaleFactor); + +- *attributes = cachedAttributes; +- } +- } else { +- { +- std::lock_guard lock(attributesMutex); +- *attributes = cachedAttributes; +- } +- +- const_cast(this)->updateWindowAttributesInternal(false); +- } ++ *attributes = cachedAttributes; + } + + int CocoaWindow::getRefreshRate() const { +@@ -257,28 +249,20 @@ + } + + void CocoaWindow::getWindowAttributes(CocoaWindowAttributes* attributes) const { +- if ([NSThread isMainThread]) { +- NSWindow *nsWindow = (__bridge NSWindow *)windowHandle; +- NSRect contentFrame = [[nsWindow contentView] frame]; +- CGFloat scaleFactor = getScaleFactor(nsWindow); ++ // Always read live content size. RT64's present thread calls needsResize ++ // / resize off the main thread; a cached size plus async refresh leaves ++ // the drawable and ImGui target stuck until something else refreshes. ++ NSWindow *nsWindow = (__bridge NSWindow *)windowHandle; ++ NSRect contentFrame = [[nsWindow contentView] frame]; ++ CGFloat scaleFactor = getScaleFactor(nsWindow); + +- { +- std::lock_guard lock(attributesMutex); +- const_cast(this)->cachedAttributes.x = (int)round(contentFrame.origin.x); +- const_cast(this)->cachedAttributes.y = (int)round(contentFrame.origin.y); +- const_cast(this)->cachedAttributes.width = (int)round(contentFrame.size.width * scaleFactor); +- const_cast(this)->cachedAttributes.height = (int)round(contentFrame.size.height * scaleFactor); ++ std::lock_guard lock(attributesMutex); ++ const_cast(this)->cachedAttributes.x = (int)round(contentFrame.origin.x); ++ const_cast(this)->cachedAttributes.y = (int)round(contentFrame.origin.y); ++ const_cast(this)->cachedAttributes.width = (int)round(contentFrame.size.width * scaleFactor); ++ const_cast(this)->cachedAttributes.height = (int)round(contentFrame.size.height * scaleFactor); + +- *attributes = cachedAttributes; +- } +- } else { +- { +- std::lock_guard lock(attributesMutex); +- *attributes = cachedAttributes; +- } +- +- const_cast(this)->updateWindowAttributesInternal(false); +- } ++ *attributes = cachedAttributes; + } + + int CocoaWindow::getRefreshRate() const { + +--- a/plume_apple.h ++++ b/plume_apple.h +@@ -31,7 +31,7 @@ + CocoaWindow(void* window); + ~CocoaWindow(); + +- // Get cached window attributes, may trigger async update ++ // Live window attributes (safe to call from the present thread) + void getWindowAttributes(CocoaWindowAttributes* attributes) const; + + // Get cached refresh rate, may trigger async update diff --git a/patches/release-prep/v1.0.7/source-capsule-r2/manifest.json b/patches/release-prep/v1.0.7/source-capsule-r2/manifest.json index afcbe6af..f3101e3e 100644 --- a/patches/release-prep/v1.0.7/source-capsule-r2/manifest.json +++ b/patches/release-prep/v1.0.7/source-capsule-r2/manifest.json @@ -2338,9 +2338,9 @@ "base_sha256": "033bae61b4d30a0f94177182dfa20da43d8d3296ddd68f1ae58c77f66d2a54dd" }, "src/game/runtime_ui.cpp": { - "sha256": "a7605d72848020da28f2fae1b75b55c524a997c021a660b50b4acc51c05fa4fe", + "sha256": "0007cb4ab66bb03d65a5b70aa430ccc492ab9f4e0733dc307f1662a559ed90b3", "base": "runtime-recomp/src/game/runtime_ui.cpp", - "base_sha256": "a7605d72848020da28f2fae1b75b55c524a997c021a660b50b4acc51c05fa4fe" + "base_sha256": "0007cb4ab66bb03d65a5b70aa430ccc492ab9f4e0733dc307f1662a559ed90b3" }, "src/game/runtime_ui.hpp": { "sha256": "ad7c87220d85b9c354590931036d6deeee5ca2492066c9becc1f052d91cf4d7d", @@ -5093,7 +5093,7 @@ "src/game/runtime_ui.cpp": { "sha256": "07f88dd1aa60690c4967bf104427773b9abc808cc5f90a7b968fe3171507d40b", "base": "runtime-recomp/src/game/runtime_ui.cpp", - "base_sha256": "a7605d72848020da28f2fae1b75b55c524a997c021a660b50b4acc51c05fa4fe", + "base_sha256": "0007cb4ab66bb03d65a5b70aa430ccc492ab9f4e0733dc307f1662a559ed90b3", "patch": "2c20ef4f18f9e2e62bdc92143d226e7bf5e851949b7a13d67f898b3c6a35fd19.patch" }, "src/game/runtime_ui.hpp": { @@ -5653,7 +5653,7 @@ "src/game/runtime_ui.cpp": { "sha256": "dfb596090da7b860f358d7a5fc686e275b2f86557fe741460a3f4868201e04fa", "base": "runtime-recomp/src/game/runtime_ui.cpp", - "base_sha256": "a7605d72848020da28f2fae1b75b55c524a997c021a660b50b4acc51c05fa4fe", + "base_sha256": "0007cb4ab66bb03d65a5b70aa430ccc492ab9f4e0733dc307f1662a559ed90b3", "patch": "9119f8ab41db037f855ba836434393feb85abf5a1995ee83058177f1d38050dd.patch" }, "launcher-ui-receipt.json": { @@ -8282,7 +8282,7 @@ "native/src/game/runtime_ui.cpp": { "sha256": "1930980ebeec7e07d8619c68b37495ef73e743299da0aa91f698f0b6bbb7bd75", "base": "runtime-recomp/src/game/runtime_ui.cpp", - "base_sha256": "a7605d72848020da28f2fae1b75b55c524a997c021a660b50b4acc51c05fa4fe", + "base_sha256": "0007cb4ab66bb03d65a5b70aa430ccc492ab9f4e0733dc307f1662a559ed90b3", "patch": "cb29ff8fb40932821e7bd5cb665dc8ceaced72d35617c816bf066f333e62fad4.patch" }, "native/src/game/runtime_ui.hpp": { @@ -8930,7 +8930,7 @@ "native/src/game/runtime_ui.cpp": { "sha256": "64ec197f04746fa22cffd258f3b388198cb6d8857daae8f009d11177a9659c4e", "base": "runtime-recomp/src/game/runtime_ui.cpp", - "base_sha256": "a7605d72848020da28f2fae1b75b55c524a997c021a660b50b4acc51c05fa4fe", + "base_sha256": "0007cb4ab66bb03d65a5b70aa430ccc492ab9f4e0733dc307f1662a559ed90b3", "patch": "516011edbe66c7101c15b9b4744f2d85ec33a6fd2b823a6d3e63d5ea7715e6fa.patch" }, "native/src/game/store_account_page.inl": { @@ -9048,7 +9048,7 @@ "native/src/game/runtime_ui.cpp": { "sha256": "4363f93073d81fb89ca1c3646fd65f7eea7f82936235a007de1b2a34ef2aa38d", "base": "runtime-recomp/src/game/runtime_ui.cpp", - "base_sha256": "a7605d72848020da28f2fae1b75b55c524a997c021a660b50b4acc51c05fa4fe", + "base_sha256": "0007cb4ab66bb03d65a5b70aa430ccc492ab9f4e0733dc307f1662a559ed90b3", "patch": "1d06998488c2b1ecdef3151d1a21cf7b808ea56c33b5eec034e1e541e55f3c92.patch" }, "native/src/game/save_manager.cpp": { @@ -12170,7 +12170,7 @@ "native/src/game/runtime_ui.cpp": { "sha256": "1930980ebeec7e07d8619c68b37495ef73e743299da0aa91f698f0b6bbb7bd75", "base": "runtime-recomp/src/game/runtime_ui.cpp", - "base_sha256": "a7605d72848020da28f2fae1b75b55c524a997c021a660b50b4acc51c05fa4fe", + "base_sha256": "0007cb4ab66bb03d65a5b70aa430ccc492ab9f4e0733dc307f1662a559ed90b3", "patch": "cb29ff8fb40932821e7bd5cb665dc8ceaced72d35617c816bf066f333e62fad4.patch" }, "native/src/game/runtime_ui.hpp": { diff --git a/runtime-recomp/src/game/runtime_ui.cpp b/runtime-recomp/src/game/runtime_ui.cpp index 3c8b48d9..92d51491 100644 --- a/runtime-recomp/src/game/runtime_ui.cpp +++ b/runtime-recomp/src/game/runtime_ui.cpp @@ -9501,6 +9501,15 @@ void dkr::runtime::ui::draw(RT64::Application& application) { if (application.presentQueue == nullptr || application.framebufferGraphicsWorker == nullptr) { return; } + // HUD Fit gutters read g_frame_state viewport aspect. Publish every present + // even when the ImGui overlay is closed — otherwise Expand world + stale HUD. + if (auto* hud_window = static_cast(dkr::runtime::platform::sdl_window())) { + int hud_viewport_width = 0; + int hud_viewport_height = 0; + SDL_GetWindowSize(hud_window, &hud_viewport_width, &hud_viewport_height); + dkr::runtime::hud::set_viewport_extent(hud_viewport_width, + hud_viewport_height); + } PumpFriendPresence(); const bool show_overlay = g_overlay_visible.load(std::memory_order_acquire); @@ -9563,13 +9572,6 @@ void dkr::runtime::ui::draw(RT64::Application& application) { } #endif inspector->newFrame(application.framebufferGraphicsWorker.get()); - if (auto* hud_window = static_cast(dkr::runtime::platform::sdl_window())) { - int hud_viewport_width = 0; - int hud_viewport_height = 0; - SDL_GetWindowSize(hud_window, &hud_viewport_width, &hud_viewport_height); - dkr::runtime::hud::set_viewport_extent(hud_viewport_width, - hud_viewport_height); - } ApplyStyle(); ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad; diff --git a/runtime-recomp/tests/live_lod_pipeline_tests.py b/runtime-recomp/tests/live_lod_pipeline_tests.py index 93490dbf..68fea614 100644 --- a/runtime-recomp/tests/live_lod_pipeline_tests.py +++ b/runtime-recomp/tests/live_lod_pipeline_tests.py @@ -20,6 +20,16 @@ def test_ui_publishes_before_hidden_overlay_early_return(self): self.assertIn("RT64::setDefaultSamplerMipLODBias(", before_return) self.assertIn("dkr::runtime::enhancements::effective_texture_lod_bias()", before_return) + def test_hud_viewport_publishes_before_overlay_closed_early_return(self): + # Fit gutters read published extent; with the menu closed, ui::draw must + # still refresh it every present (see Mac resize / Expand+Fit mismatch). + draw = read("runtime-recomp/src/game/runtime_ui.cpp").split( + "void dkr::runtime::ui::draw(RT64::Application& application) {", 1 + )[1] + before_overlay_return = draw.split("!show_overlay && !show_fps", 1)[0] + self.assertIn("dkr::runtime::hud::set_viewport_extent(", before_overlay_return) + self.assertEqual(draw.count("dkr::runtime::hud::set_viewport_extent("), 1) + def test_raster_pass_uses_live_value_not_constructor_snapshot(self): renderer = read("extern/rt64/src/render/rt64_framebuffer_renderer.cpp") self.assertIn("rasterParams.mipLODBias = getDefaultSamplerMipLODBias();", renderer) diff --git a/runtime-recomp/tests/macos_metal_overlay_tests.mm b/runtime-recomp/tests/macos_metal_overlay_tests.mm index 65638ceb..9ddf805f 100644 --- a/runtime-recomp/tests/macos_metal_overlay_tests.mm +++ b/runtime-recomp/tests/macos_metal_overlay_tests.mm @@ -42,7 +42,14 @@ int main() { ImGui::GetIO().IniFilename = nullptr; auto check_frame = [&] { SDL_PumpEvents(); - swapchain->resize(); + // RT64 presents off the main thread. needsResize/resize must see + // the live Cocoa size there — main-thread-only checks miss the bug. + bool present_resized = false; + std::thread([&] { + if (swapchain->needsResize()) present_resized = swapchain->resize(); + else present_resized = true; + }).join(); + passed &= present_resized; int pixels_w = 0, pixels_h = 0; SDL_GL_GetDrawableSize(window, &pixels_w, &pixels_h); CAMetalLayer* layer = (__bridge CAMetalLayer*)native.view; From 3f8e6112d386c1a50e9b55d4a3c94bb460c4b068 Mon Sep 17 00:00:00 2001 From: actuallyitsnathaniel Date: Mon, 2 Nov 2026 13:48:29 -0800 Subject: [PATCH 2/2] lintfix: stop clang-tidy-diff from analyzing tidy-skip TUs without compile commands --- .github/tidy-skip.txt | 6 ++++-- .github/workflows/tooling.yml | 27 +++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/.github/tidy-skip.txt b/.github/tidy-skip.txt index c1a641af..22398a08 100644 --- a/.github/tidy-skip.txt +++ b/.github/tidy-skip.txt @@ -1,8 +1,10 @@ # First-party translation units the `host` preset does not compile, so the # `tidy` job has no compile command for them. A changed .c/.cpp that is neither # in build/host/compile_commands.json nor listed here fails the job, which keeps -# gaps in tidy coverage visible. Remove a line once its TU builds in `host` -# (most of these need RT64 or generated code; see host-full-nogen in the plan). +# gaps in tidy coverage visible. Listed paths are also excluded from +# clang-tidy-diff (analyzing them without compile flags fails on third-party +# includes). Remove a line once its TU builds in `host` (most of these need +# RT64 or generated code; see host-full-nogen in the plan). # Paths are repo-relative, one per line, exact match. modding/examples/ai-boss-difficulty/main.c modding/examples/core-boot/main.c diff --git a/.github/workflows/tooling.yml b/.github/workflows/tooling.yml index 6759fd4c..9c8cb921 100644 --- a/.github/workflows/tooling.yml +++ b/.github/workflows/tooling.yml @@ -152,9 +152,32 @@ jobs: env: MERGE_BASE: ${{ steps.changes.outputs.merge-base }} run: | - mapfile -t changed <"$RUNNER_TEMP/changed.txt" + # tidy-skip TUs are absent from the host compile database. The gate + # above allows them; clang-tidy-diff must not analyze them, or it + # invents flags and fails on missing third-party includes (SDL3, + # ultramodern, …). + python3 - "$RUNNER_TEMP/changed.txt" "$RUNNER_TEMP/tidied.txt" <<'EOF' + import sys + skipped = { + line.strip() + for line in open(".github/tidy-skip.txt") + if line.strip() and not line.startswith("#") + } + tidied = [ + line.strip() + for line in open(sys.argv[1]) + if line.strip() and line.strip() not in skipped + ] + open(sys.argv[2], "w").write("\n".join(tidied) + ("\n" if tidied else "")) + print(f"{len(tidied)} tidiable changed files ({len(skipped)} skip-listed)") + EOF + mapfile -t tidied <"$RUNNER_TEMP/tidied.txt" + if [[ ${#tidied[@]} -eq 0 ]]; then + echo 'No tidiable changed files (all listed in tidy-skip.txt).' + exit 0 + fi set +e - git diff -U0 "$MERGE_BASE" HEAD -- "${changed[@]}" | + git diff -U0 "$MERGE_BASE" HEAD -- "${tidied[@]}" | python3 /usr/lib/llvm-18/share/clang/clang-tidy-diff.py -p1 -path build/host \ -clang-tidy-binary clang-tidy-18 -iregex '.*\.(c|cc|cpp|h|hpp)' -j 4 -quiet >"$RUNNER_TEMP/tidy.log" 2>&1 status=$?