From debe5438fe1667929862827d283943fad15ad7c4 Mon Sep 17 00:00:00 2001 From: Mitchell Date: Sun, 19 Apr 2026 01:03:20 -0500 Subject: [PATCH] fix: imgui/SDL Vulkan present mode issues closes #84 --- build.zig | 1 + src/ui/ui.zig | 77 ++++++++++++++--- src/ui/wayland_present_gate.zig | 148 ++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 12 deletions(-) create mode 100644 src/ui/wayland_present_gate.zig diff --git a/build.zig b/build.zig index 20978a4..dbef348 100644 --- a/build.zig +++ b/build.zig @@ -107,6 +107,7 @@ fn add_linux_dependencies( exe.root_module.linkSystemLibrary("gio-2.0", .{}); exe.root_module.linkSystemLibrary("gobject-2.0", .{}); exe.root_module.linkSystemLibrary("portal", .{}); + exe.root_module.linkSystemLibrary("wayland-client", .{}); // Vulkan is linked directly, because it is required that the // system has the libs installed. diff --git a/src/ui/ui.zig b/src/ui/ui.zig index 0a5300b..1b9eb05 100644 --- a/src/ui/ui.zig +++ b/src/ui/ui.zig @@ -3,7 +3,6 @@ const std = @import("std"); const c = @import("imguiz").imguiz; const vk = @import("vulkan"); const rc = @import("zigrc"); -const util = @import("../util.zig"); const sdl = @import("./sdl.zig"); const VulkanCapturePreviewTexture = @import("../vulkan/vulkan_capture_preview_texture.zig").VulkanCapturePreviewTexture; @@ -13,6 +12,7 @@ const API_VERSION = @import("../vulkan/vulkan.zig").API_VERSION; const draw_left_column = @import("./draw_left_column.zig").draw_left_column; const draw_video_preview = @import("./draw_video_preview.zig").draw_video_preview; const VulkanImageBuffer = @import("../vulkan/vulkan_image_buffer.zig").VulkanImageBuffer; +const WaylandPresentGate = @import("./wayland_present_gate.zig").WaylandPresentGate; // TODO: save and restore window size const WIDTH = 1600; @@ -33,6 +33,7 @@ pub const UI = struct { surface: ?c.VkSurfaceKHR = null, descriptor_pool: ?vk.DescriptorPool = null, swapchain_rebuild: bool = false, + wayland_present_gate: ?WaylandPresentGate = null, /// Init SDL and return new UI instance pub fn init( @@ -71,6 +72,9 @@ pub const UI = struct { c.cImGui_ImplVulkan_Shutdown(); c.cImGui_ImplSDL3_Shutdown(); + if (self.wayland_present_gate) |*wayland_present_gate| { + wayland_present_gate.deinit(); + } if (self.descriptor_pool) |descriptor_pool| { self.vulkan.device.destroyDescriptorPool(descriptor_pool, null); @@ -138,6 +142,7 @@ pub const UI = struct { if (!c.SDL_ShowWindow(self.window.?)) { return error.SDLShowWindowFailure; } + self.wayland_present_gate = WaylandPresentGate.init(self.window.?); // Setup Dear ImGui context if (c.ImGui_CreateContext(null) == null) return error.ImGuiCreateContextFailure; @@ -268,12 +273,8 @@ pub const UI = struct { else => {}, } } - - // NOTE: SDL_WINDOW_MINIMIZED - this event does not get invoked on wayland. - // TODO: Test this out on windows. We may not want this at all? - if ((c.SDL_GetWindowFlags(self.window.?) & c.SDL_WINDOW_MINIMIZED) > 0) { - c.SDL_Delay(10); - continue; + if (self.wayland_present_gate) |*wayland_present_gate| { + wayland_present_gate.dispatch_pending(); } // Resize swap chain? @@ -282,6 +283,14 @@ pub const UI = struct { if (!c.SDL_GetWindowSizeInPixels(self.window.?, &fb_width, &fb_height)) { return error.SDLGetWindowSizeInPixelsFailure; } + const framebuffer_zero_sized = fb_width <= 0 or fb_height <= 0; + const run_ui_frame = self.should_run_ui_frame(framebuffer_zero_sized); + + if (!run_ui_frame) { + c.SDL_Delay(1); + continue; + } + if (fb_width > 0 and fb_height > 0 and (self.swapchain_rebuild or self.vulkan.window.?.Width != fb_width or @@ -369,12 +378,13 @@ pub const UI = struct { // Rendering while preview locks are held. c.ImGui_Render(); const draw_data = c.ImGui_GetDrawData(); - const is_minimized = (draw_data.*.DisplaySize.x <= 0.0 or draw_data.*.DisplaySize.y <= 0.0); + const imgui_zero_sized = (draw_data.*.DisplaySize.x <= 0.0 or draw_data.*.DisplaySize.y <= 0.0); + const should_present = run_ui_frame and !imgui_zero_sized; self.vulkan.window.?.ClearValue.color.float32[0] = clear_color.x * clear_color.w; self.vulkan.window.?.ClearValue.color.float32[1] = clear_color.y * clear_color.w; self.vulkan.window.?.ClearValue.color.float32[2] = clear_color.z * clear_color.w; self.vulkan.window.?.ClearValue.color.float32[3] = clear_color.w; - if (!is_minimized) { + if (should_present) { try self.frame_render(draw_data); } @@ -383,7 +393,7 @@ pub const UI = struct { c.ImGui_RenderPlatformWindowsDefault(); } - if (!is_minimized) { + if (should_present) { try self.frame_present(); } } @@ -481,6 +491,19 @@ pub const UI = struct { .p_swapchains = @ptrCast(&wd.Swapchain), .p_image_indices = @ptrCast(&wd.FrameIndex), }; + var present_completed = true; + if (self.wayland_present_gate) |*wayland_present_gate| { + present_completed = try wayland_present_gate.register_present_callback(); + if (!present_completed) { + log.debug("[frame_present] skipping present because wl_surface.frame is not ready", .{}); + return; + } + defer { + if (!present_completed) { + wayland_present_gate.cancel_callback(); + } + } + } self.vulkan.queue_present_khr(&info) catch |err| { switch (err) { error.OutOfDateKHR => { @@ -490,10 +513,38 @@ pub const UI = struct { else => return err, } }; + present_completed = true; wd.SemaphoreIndex = (wd.SemaphoreIndex + 1) % wd.SemaphoreCount; // Now we can use the next set of semaphores } + /// Check for various window states to see if UI frames + /// should be rendered/presented. SDL3 does not provide + /// what we need for Wayland so we have to implement some + /// custom stuff. + fn should_run_ui_frame( + self: *Self, + framebuffer_zero_sized: bool, + ) bool { + const window_flags = c.SDL_GetWindowFlags(self.window.?); + const window_hidden = (window_flags & c.SDL_WINDOW_HIDDEN) > 0; + const window_minimized = (window_flags & c.SDL_WINDOW_MINIMIZED) > 0; + const window_occluded = (window_flags & c.SDL_WINDOW_OCCLUDED) > 0; + const wayland_allows_present = blk: { + if (self.wayland_present_gate) |wayland_present_gate| { + break :blk wayland_present_gate.frame_ready(); + } + + break :blk true; + }; + + return !window_hidden and + !window_minimized and + !window_occluded and + !framebuffer_zero_sized and + wayland_allows_present; + } + fn setup_vulkan_window(self: *Self) !void { self.vulkan.window = .{ .Surface = self.surface.?, @@ -522,8 +573,10 @@ pub const UI = struct { const present_modes = [_]c.VkPresentModeKHR{ // c.VK_PRESENT_MODE_IMMEDIATE_KHR, // c.VK_PRESENT_MODE_MAILBOX_KHR, - // NOTE: FIFO seems to be the best for a desktop application. Had some deadlock issues - // in the past with this, but it should be resolved now. + // NOTE: FIFO seems to be the best for a desktop application and is + // more widely supported. When using this method, we MUST make sure + // it doesn't try to render/present when the window is not visible + // (or is throttled by the compositor). c.VK_PRESENT_MODE_FIFO_KHR, }; self.vulkan.window.?.PresentMode = c.cImGui_ImplVulkanH_SelectPresentMode( diff --git a/src/ui/wayland_present_gate.zig b/src/ui/wayland_present_gate.zig new file mode 100644 index 0000000..1ce943a --- /dev/null +++ b/src/ui/wayland_present_gate.zig @@ -0,0 +1,148 @@ +//! Wayland does not expose an API to check if windows +//! in are minimized, hidden, or generally waiting to present +//! (https://wiki.libsdl.org/SDL3/README-wayland#minimizerestored-window-events-are-not-sent-and-the-sdl_window_minimized-flag-is-not-set). +//! This module exposes Wayland specific methods to register a frame callback, +//! which enables the consumer to check if a frame is ready. The consumer +//! can skip render/present if a frame is not ready. + +const std = @import("std"); +const imguiz = @import("imguiz").imguiz; +const util = @import("../util.zig"); + +pub const WaylandPresentGate = if (util.is_linux()) LinuxWaylandPresentGate else StubWaylandPresentGate; + +const LinuxWaylandPresentGate = struct { + const Self = @This(); + const c = @cImport({ + @cInclude("wayland-client-core.h"); + @cInclude("wayland-client-protocol.h"); + }); + const log = std.log.scoped(.wayland_present_gate); + + const frame_listener = c.struct_wl_callback_listener{ + .done = frame_done, + }; + + display: ?*c.struct_wl_display = null, + surface: ?*c.struct_wl_surface = null, + frame_callback: ?*c.struct_wl_callback = null, + + /// Initialize display/surface pointers. Will return null on anything + /// other than Wayland. + pub fn init(window: ?*imguiz.struct_SDL_Window) ?Self { + const current_video_driver = imguiz.SDL_GetCurrentVideoDriver() orelse return null; + if (!std.mem.eql(u8, std.mem.span(current_video_driver), "wayland")) { + log.debug("[init] current video driver is not using wayland", .{}); + return null; + } + + const window_properties = imguiz.SDL_GetWindowProperties(window); + if (window_properties == 0) { + log.warn("[init] SDL_GetWindowProperties returned 0 for a Wayland window", .{}); + return null; + } + + const display_ptr = imguiz.SDL_GetPointerProperty(window_properties, imguiz.SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, null); + const surface_ptr = imguiz.SDL_GetPointerProperty(window_properties, imguiz.SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, null); + if (display_ptr == null or surface_ptr == null) { + log.warn( + "[init] missing Wayland window properties: display_present={} surface_present={}", + .{ display_ptr != null, surface_ptr != null }, + ); + return null; + } + + log.info("[init] enabled wl_surface.frame present gating", .{}); + return .{ + .display = @ptrCast(display_ptr.?), + .surface = @ptrCast(surface_ptr.?), + }; + } + + pub fn deinit(self: *Self) void { + self.cancel_callback(); + } + + /// The frame is ready when we don't have a callback. This _should_ be true + /// when the window is not minimized or hidden. + pub fn frame_ready(self: *const Self) bool { + return self.frame_callback == null; + } + + /// Dispatches already queued Wayland events so frame callbacks can flip the + /// gate back to ready when the compositor requests another redraw. + pub fn dispatch_pending(self: *Self) void { + if (self.display == null or self.surface == null) { + return; + } + + const result = c.wl_display_dispatch_pending(self.display); + if (result < 0) { + log.warn("[dispatch_pending] wl_display_dispatch_pending failed: {}", .{result}); + } + } + + /// Returns false if a frame is not ready. If a frame is ready, + /// register a frame done callback, then return true. + pub fn register_present_callback(self: *Self) !bool { + if (self.display == null or self.surface == null) { + return true; + } + if (!self.frame_ready()) { + return false; + } + + const callback = c.wl_surface_frame(self.surface) orelse { + return error.WaylandSurfaceFrameFailure; + }; + errdefer c.wl_callback_destroy(callback); + + if (c.wl_callback_add_listener(callback, &frame_listener, self) != 0) { + return error.WaylandCallbackListenerAddFailure; + } + + self.frame_callback = callback; + return true; + } + + /// Cancels any pending callbacks. + pub fn cancel_callback(self: *Self) void { + if (self.frame_callback) |callback| { + c.wl_callback_destroy(callback); + self.frame_callback = null; + } + } + + fn frame_done(data: ?*anyopaque, callback: ?*c.struct_wl_callback, callback_data: u32) callconv(.c) void { + _ = callback_data; + + if (callback) |_callback| { + c.wl_callback_destroy(_callback); + } + + const self: *Self = @ptrCast(@alignCast(data)); + self.frame_callback = null; + } +}; + +const StubWaylandPresentGate = struct { + const Self = @This(); + pub fn frame_ready(_: *const Self) bool { + return true; + } + + /// Non-Wayland targets never create a real gate. + pub fn init(_: ?*imguiz.struct_SDL_Window) ?Self { + return null; + } + + pub fn deinit(_: *Self) void {} + + pub fn dispatch_pending(_: *Self) void {} + + pub fn register_present_callback(_: *Self) !bool { + return true; + } + + pub fn cancel_callback(_: *Self) void {} +};