const std = @import("std"); const mem = std.mem; const posix = std.posix; const c = @cImport({ @cDefine("GL_GLEXT_PROTOTYPES", {}); @cInclude("GL/gl.h"); @cInclude("GL/glext.h"); }); const wayland = @import("wayland"); const wl = wayland.client.wl; const xdg = wayland.client.xdg; const egl = @import("egl.zig"); const vertex_shader_src: [*c]const u8 = \\#version 330 core \\layout (location = 0) in vec3 pos; \\ \\void main() \\{ \\ gl_Position = vec4(pos.x, pos.y, pos.z, 1.0); \\} ; const frag_shader_src: [*c]const u8 = \\#version 330 core \\out vec4 frag_color; \\ \\void main() \\{ \\ frag_color = vec4(1.0f, 0.2f, 0.2f, 1.0f); \\} ; /// Global Wayland registry context; contains pointers to the globals we expect /// from the compositor. const RegistryContext = struct { compositor: ?*wl.Compositor = null, wm_base: ?*xdg.WmBase = null, }; var registry_ctx = RegistryContext{}; var egl_display: *egl.Display = undefined; var egl_cfg: [1]*egl.Config = undefined; const GlGetIvFn = *const fn ( obj: c_uint, pname: c.GLenum, params: [*c]c_int, ) callconv(.C) void; const GlGetLogFn = *const fn ( obj: c_uint, max: c_int, len: [*c]c_int, buf: [*c]u8, ) callconv(.C) void; fn checkGlError( getIvFn: GlGetIvFn, getLogFn: GlGetLogFn, obj: c_uint, pname: c.GLenum, ) bool { var success: c_int = 0; var gl_log: [512:0]u8 = undefined; getIvFn(obj, pname, &success); const err = success == 0; if (err) { getLogFn(obj, 512, null, &gl_log); std.debug.print("Shader compilation error: {s}\n", .{gl_log}); } return err; } fn checkProgramLinkError(prog: c_uint) !void { if (checkGlError( c.glGetProgramiv, c.glGetProgramInfoLog, prog, c.GL_LINK_STATUS, )) { return error.GlProgramLinkError; } } fn checkShaderError(shader: c_uint) !void { if (checkGlError( c.glGetShaderiv, c.glGetShaderInfoLog, shader, c.GL_COMPILE_STATUS, )) { return error.GlShaderCompileError; } } /// A single Window (surface); data and methods we need to tell the compositor /// to render a surface for us. pub const Window = struct { /// The allocator that we were allocated on, needed to destroy ourselves allocator: mem.Allocator, /// Wayland surface wl_surface: *wl.Surface, /// XDG surface attached to the Wayland surface xdg_surface: *xdg.Surface = undefined, /// XDG toplevel data xdg_toplevel: *xdg.Toplevel = undefined, /// EGL Wayland display egl_win: *wl.EglWindow = undefined, /// EGL context egl_ctx: *egl.Context = undefined, /// EGL surface egl_surface: *egl.Surface = undefined, /// OpenGl triangle shader program shader_prog: c_uint = undefined, /// Triangle vertices vertices: [9]f32, /// OpenGL vertex buffer object vbo: c_uint = undefined, /// OpenGL vertex array object vao: c_uint = undefined, /// This window's title title: [*:0]const u8, /// Width in pixels width: i32 = 480, /// Height in pixels height: i32 = 360, /// Offset of our checkerboard animation offset: f32 = 0.0, /// Last frame's timestamp in ms last_frame: u32 = 0, /// Frames per second statistic current_fps: f32 = 0.0, /// Should we keep this window open? open: bool = true, /// Allocate a new window and all backing resources for it pub fn create(allocator: mem.Allocator, title: [*:0]const u8) !*Window { const compositor = registry_ctx.compositor orelse return error.NoWlCompositor; const wm_base = registry_ctx.wm_base orelse return error.NoXdgWmBase; var win = try allocator.create(Window); win.* = Window{ .allocator = allocator, .wl_surface = try compositor.createSurface(), .title = title, .vertices = .{ -0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0, }, }; win.xdg_surface = try wm_base.getXdgSurface(win.wl_surface); errdefer win.xdg_surface.destroy(); win.xdg_toplevel = try win.xdg_surface.getToplevel(); errdefer win.xdg_toplevel.destroy(); win.egl_ctx = try egl_display.createContext(egl_cfg[0], null, null); win.egl_win = try wl.EglWindow.create(win.wl_surface, win.width, win.height); win.egl_surface = try egl_display.createWindowSurface(egl_cfg[0], win.egl_win, null); try egl_display.makeCurrent(win.egl_surface, win.egl_surface, win.egl_ctx); const gl_vendor = c.glGetString(c.GL_VENDOR); const gl_version = c.glGetString(c.GL_VERSION); std.debug.print("{?s} OpenGL {?s}\n", .{ gl_vendor, gl_version }); const vertex_shader = c.glCreateShader(c.GL_VERTEX_SHADER); defer c.glDeleteShader(vertex_shader); c.glShaderSource(vertex_shader, 1, &vertex_shader_src, null); c.glCompileShader(vertex_shader); try checkShaderError(vertex_shader); const frag_shader = c.glCreateShader(c.GL_FRAGMENT_SHADER); defer c.glDeleteShader(frag_shader); c.glShaderSource(frag_shader, 1, &frag_shader_src, null); c.glCompileShader(frag_shader); try checkShaderError(frag_shader); win.shader_prog = c.glCreateProgram(); c.glAttachShader(win.shader_prog, vertex_shader); c.glAttachShader(win.shader_prog, frag_shader); c.glLinkProgram(win.shader_prog); try checkProgramLinkError(win.shader_prog); c.glGenVertexArrays(1, &win.vao); c.glGenBuffers(1, &win.vbo); c.glBindVertexArray(win.vao); c.glBindBuffer(c.GL_ARRAY_BUFFER, win.vbo); c.glBufferData( c.GL_ARRAY_BUFFER, @sizeOf(@TypeOf(win.vertices)), &win.vertices, c.GL_STATIC_DRAW, ); c.glVertexAttribPointer(0, 3, c.GL_FLOAT, c.GL_FALSE, 3 * @sizeOf(f32), null); c.glEnableVertexAttribArray(0); c.glBindBuffer(c.GL_ARRAY_BUFFER, 0); c.glBindVertexArray(0); c.glViewport(@divTrunc(win.width, 2) - 160, @divTrunc(win.height, 2) - 160, 320, 320); win.xdg_surface.setListener(*Window, xdgSurfaceListener, win); win.xdg_toplevel.setListener(*Window, xdgToplevelListener, win); win.xdg_toplevel.setTitle(title); win.wl_surface.commit(); const wl_cb = try win.wl_surface.frame(); wl_cb.setListener(*Window, frameListener, win); return win; } /// Destroy this window and all its associated resources pub fn destroy(win: *Window) void { c.glDeleteVertexArrays(1, &win.vao); c.glDeleteBuffers(1, &win.vbo); c.glDeleteProgram(win.shader_prog); win.egl_win.destroy(); win.xdg_toplevel.destroy(); win.xdg_surface.destroy(); win.wl_surface.destroy(); win.allocator.destroy(win); } /// Draw a new frame pub fn drawFrame(win: *Window) !void { try egl_display.swapInterval(0); c.glClearColor(0, 0.5, 1, 1); c.glClear(c.GL_COLOR_BUFFER_BIT); c.glUseProgram(win.shader_prog); c.glBindVertexArray(win.vao); c.glDrawArrays(c.GL_TRIANGLES, 0, 3); try egl_display.swapBuffers(win.egl_surface); } /// Resize this window fn resize(win: *Window, width: i32, height: i32) !void { const new_height = if (height != 0) height else win.height; const new_width = if (width != 0) width else win.width; if (win.width == new_width and win.height == new_height) return; win.width = new_width; win.height = new_height; win.egl_win.resize(win.width, win.height, 0, 0); if (win.width < win.height) { c.glViewport( 0, @divTrunc(win.height, 2) - @divTrunc(win.width, 2), win.width, win.width, ); } else { c.glViewport( @divTrunc(win.width, 2) - @divTrunc(win.height, 2), 0, win.height, win.height, ); } } /// XDG Surface callbacks fn xdgSurfaceListener(xdg_surface: *xdg.Surface, event: xdg.Surface.Event, win: *Window) void { switch (event) { .configure => |configure| { xdg_surface.ackConfigure(configure.serial); win.drawFrame() catch |err| { std.debug.print("error drawing frame {}\n", .{err}); }; win.wl_surface.commit(); }, } } /// XDG Toplevel callbacks fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, win: *Window) void { switch (event) { .configure => |configure| { win.resize(configure.width, configure.height) catch |err| { std.debug.print("error resizing window {}\n", .{err}); return; }; }, .close => win.open = false, } } /// Wayland frame callback fn frameListener(wl_cb: *wl.Callback, event: wl.Callback.Event, win: *Window) void { switch (event) { .done => |done| { wl_cb.destroy(); const new_cb = win.wl_surface.frame() catch |err| { std.debug.print("unable to get frame {}\n", .{err}); win.open = false; return; }; new_cb.setListener(*Window, frameListener, win); if (win.last_frame != 0) { const elapsed: f32 = @floatFromInt(done.callback_data -% win.last_frame); win.offset -= elapsed / 1000.0 * 32.0; win.current_fps = 1000.0 / elapsed; } win.last_frame = done.callback_data; win.drawFrame() catch |err| { std.debug.print("error drawing frame {}\n", .{err}); return; }; win.wl_surface.damageBuffer(0, 0, std.math.maxInt(i32), std.math.maxInt(i32)); win.wl_surface.commit(); }, } } }; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); const display = try wl.Display.connect(null); defer display.disconnect(); const registry = try display.getRegistry(); registry.setListener(*RegistryContext, registryListener, ®istry_ctx); if (display.roundtrip() != .SUCCESS) return error.RoundTripFailed; const wm_base = registry_ctx.wm_base orelse return error.NoXdgWmBase; wm_base.setListener(?*void, xdgWmBaseListener, null); try egl.bindApi(.opengl); var attrs = [_]egl.Attribute{ .{ .type = .red_size, .value = 8 }, .{ .type = .green_size, .value = 8 }, .{ .type = .blue_size, .value = 8 }, .{ .type = .none, .value = 0 }, }; egl_display = try egl.Display.get(display); var major: i32 = 0; var minor: i32 = 0; try egl_display.initialize(&major, &minor); defer egl_display.terminate(); std.debug.print("EGL version {}.{}\n", .{ major, minor }); _ = try egl_display.chooseConfig(&attrs, &egl_cfg); const win = try Window.create(allocator, "Hello, wayland!"); defer win.destroy(); while (win.open) { if (display.dispatch() != .SUCCESS) return error.DispatchFailed; } } /// Global registry callback fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, ctx: *RegistryContext) void { switch (event) { .global => |global| { std.debug.print("inteface: {s}, version {}, name {}\n", .{ global.interface, global.version, global.name, }); if (mem.orderZ(u8, global.interface, wl.Compositor.getInterface().name) == .eq) { ctx.compositor = registry.bind(global.name, wl.Compositor, 6) catch return; } else if (mem.orderZ(u8, global.interface, xdg.WmBase.getInterface().name) == .eq) { ctx.wm_base = registry.bind(global.name, xdg.WmBase, 2) catch return; } }, .global_remove => {}, } } /// XDG Wm Base callback fn xdgWmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: ?*void) void { switch (event) { .ping => |ping| { wm_base.pong(ping.serial); }, } }