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
1 change: 1 addition & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
77 changes: 65 additions & 12 deletions src/ui/ui.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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?
Expand All @@ -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
Expand Down Expand Up @@ -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);
}

Expand All @@ -383,7 +393,7 @@ pub const UI = struct {
c.ImGui_RenderPlatformWindowsDefault();
}

if (!is_minimized) {
if (should_present) {
try self.frame_present();
}
}
Expand Down Expand Up @@ -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 => {
Expand All @@ -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.?,
Expand Down Expand Up @@ -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(
Expand Down
148 changes: 148 additions & 0 deletions src/ui/wayland_present_gate.zig
Original file line number Diff line number Diff line change
@@ -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 {}
};
Loading