Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions config/dependencies.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
"commit": "11926860e878e68626ea99ec88562ce2b8badc4f",
"path": "tools/Plume",
"license": "MIT",
"patches": ["patches/plume-optional-extensions.patch", "patches/plume-pipeline-cache.patch", "patches/plume-swapchain-usage.patch", "patches/plume-swapchain-image-count.patch", "patches/plume-buffer-device-address-khr.patch", "patches/plume-dynamic-constant-buffers.patch", "patches/plume-descriptor-indexing-test-switch.patch", "patches/plume-optional-scalar-layout-device-address.patch", "patches/plume-d24s8-depth.patch", "patches/plume-bundled-vulkan-driver.patch"],
"patch_rationale": "Makes VK_EXT_robustness2 and VK_KHR_sampler_mirror_clamp_to_edge optional (Plume already has fallbacks for null descriptors), as some Android Vulkan drivers, including the emulator's, lack them; and frees the null-descriptor placeholder buffer before the allocator it came from. Adds an optional device-owned pipeline cache and a generation counter for application-managed persistence. Restricts swap-chain image usages to surface capabilities, stops creation after a failed capability query, and logs creation parameters on failure. Preserves the requested swap-chain image count across resizes instead of accumulating driver-provided extra images, and reapplies current surface count limits. Takes buffer device addresses through the KHR entry point where the core one is null (a Vulkan 1.1 device with VK_KHR_buffer_device_address, Issue #85). Adds dynamic uniform-buffer descriptor ranges so shader constants are bound per draw by offset instead of read through buffer addresses, which lets the Adreno compiler move them into its preamble. Adds a test switch that creates the device without the descriptor-indexing features, so the renderer's fallback for drivers without them (Issue #134) can run, and be checked by the validation layer, on a GPU that has them. Makes VK_EXT_scalar_block_layout and VK_KHR_buffer_device_address optional (no shader or buffer of the renderer needs them; a Vulkan 1.1 Mali-G76 lacks both, Issue #169), with test switches that leave them disabled and one that asks for a Vulkan 1.1 instance. Adds the D24_UNORM_S8_UINT depth format (appended to RenderFormat), for drivers without D32_SFLOAT_S8_UINT such as an old Mali's. Lets the app hand the Vulkan interface the vkGetInstanceProcAddr of a driver it loaded itself, so the macOS/iOS app uses its bundled MoltenVK even where a Vulkan SDK put a loader in /usr/local/lib (Issue #207).",
"patches": ["patches/plume-optional-extensions.patch", "patches/plume-pipeline-cache.patch", "patches/plume-swapchain-usage.patch", "patches/plume-swapchain-image-count.patch", "patches/plume-buffer-device-address-khr.patch", "patches/plume-dynamic-constant-buffers.patch", "patches/plume-descriptor-indexing-test-switch.patch", "patches/plume-optional-scalar-layout-device-address.patch", "patches/plume-d24s8-depth.patch", "patches/plume-bundled-vulkan-driver.patch", "patches/plume-present-result-layer-size.patch"],
"patch_rationale": "Makes VK_EXT_robustness2 and VK_KHR_sampler_mirror_clamp_to_edge optional (Plume already has fallbacks for null descriptors), as some Android Vulkan drivers, including the emulator's, lack them; and frees the null-descriptor placeholder buffer before the allocator it came from. Adds an optional device-owned pipeline cache and a generation counter for application-managed persistence. Restricts swap-chain image usages to surface capabilities, stops creation after a failed capability query, and logs creation parameters on failure. Preserves the requested swap-chain image count across resizes instead of accumulating driver-provided extra images, and reapplies current surface count limits. Takes buffer device addresses through the KHR entry point where the core one is null (a Vulkan 1.1 device with VK_KHR_buffer_device_address, Issue #85). Adds dynamic uniform-buffer descriptor ranges so shader constants are bound per draw by offset instead of read through buffer addresses, which lets the Adreno compiler move them into its preamble. Adds a test switch that creates the device without the descriptor-indexing features, so the renderer's fallback for drivers without them (Issue #134) can run, and be checked by the validation layer, on a GPU that has them. Makes VK_EXT_scalar_block_layout and VK_KHR_buffer_device_address optional (no shader or buffer of the renderer needs them; a Vulkan 1.1 Mali-G76 lacks both, Issue #169), with test switches that leave them disabled and one that asks for a Vulkan 1.1 instance. Adds the D24_UNORM_S8_UINT depth format (appended to RenderFormat), for drivers without D32_SFLOAT_S8_UINT such as an old Mali's. Lets the app hand the Vulkan interface the vkGetInstanceProcAddr of a driver it loaded itself, so the macOS/iOS app uses its bundled MoltenVK even where a Vulkan SDK put a loader in /usr/local/lib (Issue #207). Keeps the VkResult of the last present and acquire for the application to log, and on macOS sizes the swap chain from the window's Metal layer (bounds times contents scale, as MoltenVK does) instead of the window's backing scale, which made every present suboptimal on Retina and scaled displays (Issue #191).",
"submodules": [
{ "path": "contrib/D3D12MemoryAllocator", "commit": "9ef66bc14edd10dee0de3a545b98578363552f66" },
{ "path": "contrib/Vulkan-Headers", "commit": "89268a6d17fc87003b209a1422c17ab288be99a0" },
Expand Down
21 changes: 21 additions & 0 deletions docs/vulkan.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,27 @@ M1(Metal System Trace,比賽中 8 秒):頂點 144.6 → 6.4 ms/幀,GPU
`NATIVE_DISPATCH_LOOP removed=0|1`;`SFR_TEST_DISPATCH_LOOP=0` 在 Apple 上保留迴圈,`=1` 在任何
Vulkan 裝置上拿掉(驗證層用)。

### macOS:Retina 與縮放顯示器上的 present

v0.10.14 以前,macOS 的遊戲視窗不是高 DPI:SDL 的 Metal layer 的 contents scale 是 1,1280x720
的視窗 layer 就是 1280x720 像素;Plume 的 `CocoaWindow` 卻用視窗的 backing scale(Retina 或縮放
顯示器上是 2)算 swap chain,做成 2560x1440。MoltenVK 只認 layer 的自然大小(bounds 乘
contents scale),大小不同的 swap chain 每次 present 都回 `VK_SUBOPTIMAL_KHR`,Plume 在 Apple 上把它
當失敗,重建八次後遊戲停在 `failed to present native swap-chain texture`(Issue #191,M1 MacBook
Pro 的內建螢幕和縮放成 2560x1440 的外接螢幕都會,1x 的 1920x1080 螢幕不會)。

- macOS 的遊戲視窗改成高 DPI(`native_graphics.cpp`,和 iOS 一樣),layer 的 scale 就是螢幕的。
- `patches/plume-present-result-layer-size.patch`:macOS 的 swap chain 大小改取視窗裡 Metal layer
的 bounds 乘 contents scale(截斷,和 MoltenVK 一樣),兩者不會再不同;swap chain 也記下最後一次
present 與 acquire 的 `VkResult`。
- `game.log` 的 `NATIVE_PRESENTATION present_refused` 與 `acquire_refused` 都記
`result=<名稱>(<數字>)`(所有平台);重建八次仍是 `VK_SUBOPTIMAL_KHR` 時記一次
`NATIVE_PRESENTATION suboptimal_kept`,繼續執行(MoltenVK 仍會把這張圖縮放顯示),不再結束遊戲。

在只有 1x 螢幕的 Mac 上,可以用私有的 `CGVirtualDisplay` 建一個虛擬螢幕,設成 1280x720 HiDPI
(2560x1440 像素)模式來重現。注意:macOS 27 上建立它的程序結束後,虛擬螢幕不會消失(WindowServer
記錄 `CGXVirtualDisplayDestroy ... not found`),要登出或重開機才會移除。

### 圖形自我測試的繪製

`graphics_selftest.cpp`(Android 由 `GraphicsTestActivity` 啟動,桌面設 `SFR_GRAPHICS_SELFTEST=1`
Expand Down
147 changes: 147 additions & 0 deletions patches/plume-present-result-layer-size.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
diff --git a/plume_apple.mm b/plume_apple.mm
index 64e4dc9..fd78064 100644
--- a/plume_apple.mm
+++ b/plume_apple.mm
@@ -10,6 +10,7 @@
#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
#import <IOKit/IOKitLib.h>
+#import <QuartzCore/CAMetalLayer.h>

static uint32_t plumeGetEntryProperty(io_registry_entry_t entry, CFStringRef propertyName) {
uint32_t value = 0;
@@ -48,19 +49,57 @@ namespace plume {

// MARK: - CocoaWindow

+ // The CAMetalLayer drawn into (a Metal view's, such as SDL's) in the window's views.
+ static CAMetalLayer *findMetalLayer(NSView *view, int depth) {
+ if ([view.layer isKindOfClass:CAMetalLayer.class]) {
+ return (CAMetalLayer *)view.layer;
+ }
+
+ if (depth <= 0) {
+ return nil;
+ }
+
+ for (NSView *child in view.subviews) {
+ if (CAMetalLayer *layer = findMetalLayer(child, depth - 1)) {
+ return layer;
+ }
+ }
+
+ return nil;
+ }
+
+ // The window's size in pixels. With a Metal layer in it, the layer's own size in pixels
+ // (bounds times contents scale, truncated): the size MoltenVK holds the swap chain to.
+ // The window's backing scale differs from the layer's when the view is not high
+ // resolution (a Retina or scaled display), and a swap chain of the window's size then
+ // made every present suboptimal.
+ static CocoaWindowAttributes readWindowAttributes(NSWindow *nsWindow) {
+ NSRect contentFrame = [[nsWindow contentView] frame];
+ CocoaWindowAttributes attributes;
+ attributes.x = (int)round(contentFrame.origin.x);
+ attributes.y = (int)round(contentFrame.origin.y);
+
+ CAMetalLayer *layer = findMetalLayer([nsWindow contentView], 4);
+ if ((layer != nil) && (layer.bounds.size.width >= 1.0) && (layer.bounds.size.height >= 1.0)) {
+ const CGFloat layerScale = layer.contentsScale;
+ attributes.width = (int)trunc(layer.bounds.size.width * layerScale);
+ attributes.height = (int)trunc(layer.bounds.size.height * layerScale);
+ return attributes;
+ }
+
+ CGFloat scaleFactor = [nsWindow backingScaleFactor];
+ attributes.width = (int)round(contentFrame.size.width * scaleFactor);
+ attributes.height = (int)round(contentFrame.size.height * scaleFactor);
+ return attributes;
+ }
+
CocoaWindow::CocoaWindow(void* window)
: windowHandle(window), cachedRefreshRate(0) {
cachedAttributes = {0, 0, 0, 0};

if ([NSThread isMainThread]) {
NSWindow *nsWindow = (__bridge NSWindow *)windowHandle;
- NSRect contentFrame = [[nsWindow contentView] frame];
- CGFloat scaleFactor = [nsWindow backingScaleFactor];
-
- cachedAttributes.x = (int)round(contentFrame.origin.x);
- cachedAttributes.y = (int)round(contentFrame.origin.y);
- cachedAttributes.width = (int)round(contentFrame.size.width * scaleFactor);
- cachedAttributes.height = (int)round(contentFrame.size.height * scaleFactor);
+ cachedAttributes = readWindowAttributes(nsWindow);

NSScreen *screen = [nsWindow screen];
if (@available(macOS 12.0, *)) {
@@ -77,14 +116,10 @@ namespace plume {
void CocoaWindow::updateWindowAttributesInternal(bool forceSync) {
auto updateBlock = ^{
NSWindow *nsWindow = (__bridge NSWindow *)windowHandle;
- NSRect contentFrame = [[nsWindow contentView] frame];
- CGFloat scaleFactor = [nsWindow backingScaleFactor];
+ const CocoaWindowAttributes attributes = readWindowAttributes(nsWindow);

std::lock_guard<std::mutex> lock(attributesMutex);
- cachedAttributes.x = (int)round(contentFrame.origin.x);
- cachedAttributes.y = (int)round(contentFrame.origin.y);
- cachedAttributes.width = (int)round(contentFrame.size.width * scaleFactor);
- cachedAttributes.height = (int)round(contentFrame.size.height * scaleFactor);
+ cachedAttributes = attributes;
};

if (forceSync) {
@@ -113,15 +148,11 @@ namespace plume {
void CocoaWindow::getWindowAttributes(CocoaWindowAttributes* attributes) const {
if ([NSThread isMainThread]) {
NSWindow *nsWindow = (__bridge NSWindow *)windowHandle;
- NSRect contentFrame = [[nsWindow contentView] frame];
- CGFloat scaleFactor = [nsWindow backingScaleFactor];
+ const CocoaWindowAttributes current = readWindowAttributes(nsWindow);

{
std::lock_guard<std::mutex> lock(attributesMutex);
- const_cast<CocoaWindow*>(this)->cachedAttributes.x = (int)round(contentFrame.origin.x);
- const_cast<CocoaWindow*>(this)->cachedAttributes.y = (int)round(contentFrame.origin.y);
- const_cast<CocoaWindow*>(this)->cachedAttributes.width = (int)round(contentFrame.size.width * scaleFactor);
- const_cast<CocoaWindow*>(this)->cachedAttributes.height = (int)round(contentFrame.size.height * scaleFactor);
+ const_cast<CocoaWindow*>(this)->cachedAttributes = current;

*attributes = cachedAttributes;
}
diff --git a/plume_vulkan.cpp b/plume_vulkan.cpp
index a2e3b1b..cb03fea 100644
--- a/plume_vulkan.cpp
+++ b/plume_vulkan.cpp
@@ -2319,6 +2319,7 @@ namespace plume {
const std::scoped_lock queueLock(*commandQueue->queue->mutex);
res = vkQueuePresentKHR(commandQueue->queue->vk, &presentInfo);
}
+ lastPresentResult = res;

#if defined(__APPLE__)
// Under MoltenVK, VK_SUBOPTIMAL_KHR does not result in a valid state for rendering. We intentionally
@@ -2523,6 +2524,7 @@ namespace plume {

VulkanCommandSemaphore *interfaceSemaphore = static_cast<VulkanCommandSemaphore *>(signalSemaphore);
VkResult res = vkAcquireNextImageKHR(commandQueue->device->vk, vk, UINT64_MAX, interfaceSemaphore->vk, VK_NULL_HANDLE, textureIndex);
+ lastAcquireResult = res;
if ((res != VK_SUCCESS) && (res != VK_SUBOPTIMAL_KHR)) {
return false;
}
diff --git a/plume_vulkan.h b/plume_vulkan.h
index 1ff774c..da1968c 100644
--- a/plume_vulkan.h
+++ b/plume_vulkan.h
@@ -245,6 +245,10 @@ namespace plume {
uint64_t currentPresentId = 0;
bool immediatePresentModeSupported = false;
uint32_t maxFrameLatency = 0;
+ // What the last vkQueuePresentKHR and vkAcquireNextImageKHR returned,
+ // for the application to report or act on when they are refused.
+ VkResult lastPresentResult = VK_SUCCESS;
+ VkResult lastAcquireResult = VK_SUCCESS;

VulkanSwapChain(VulkanCommandQueue *commandQueue, RenderWindow renderWindow, uint32_t textureCount, RenderFormat format, uint32_t maxFrameLatency);
~VulkanSwapChain() override;
6 changes: 5 additions & 1 deletion src/native_graphics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,11 @@ void NativeGraphics::initialize() {
// MoltenVK refused every present as suboptimal (Issue #185).
SDL_WINDOW_METAL | SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
#elif defined(__APPLE__)
SDL_WINDOW_METAL | SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE);
// High DPI here too: on a Retina or scaled display the
// layer was 1280x720 at scale 1 while the swap chain took
// the window's backing scale (2560x1440), and MoltenVK
// refused every present as suboptimal (Issue #191).
SDL_WINDOW_METAL | SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
#else
SDL_WINDOW_VULKAN | SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE);
#endif
Expand Down
Loading
Loading