diff --git a/src/App.zig b/src/App.zig index 1154cf86d..683a70f47 100644 --- a/src/App.zig +++ b/src/App.zig @@ -128,6 +128,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void { .close => |surface| try self.closeSurface(rt_app, surface), .quit => try self.setQuit(), .surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message), + .redraw_surface => |surface| try self.redrawSurface(rt_app, surface), } } } @@ -137,6 +138,11 @@ fn closeSurface(self: *App, rt_app: *apprt.App, surface: *Surface) !void { rt_app.closeSurface(surface.rt_surface); } +fn redrawSurface(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) !void { + if (!self.hasSurface(&surface.core_surface)) return; + rt_app.redrawSurface(surface); +} + /// Create a new window fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void { if (!@hasDecl(apprt.App, "newWindow")) { @@ -230,6 +236,12 @@ pub const Message = union(enum) { message: apprt.surface.Message, }, + /// Redraw a surface. This only has an effect for runtimes that + /// use single-threaded draws. To redraw a surface for all runtimes, + /// wake up the renderer thread. The renderer thread will send this + /// message if it needs to. + redraw_surface: *apprt.Surface, + const NewWindow = struct { /// The parent surface parent: ?*Surface = null, diff --git a/src/Surface.zig b/src/Surface.zig index 65740340e..b7ce3ede7 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -316,6 +316,7 @@ pub fn init( rt_surface, &self.renderer, &self.renderer_state, + app_mailbox, ); errdefer render_thread.deinit(); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 6ba10656d..dbdb925d3 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -137,6 +137,13 @@ pub const App = struct { self.app.alloc.destroy(surface); } + pub fn redrawSurface(self: *App, surface: *Surface) void { + _ = self; + _ = surface; + + @panic("This should never be called for GLFW."); + } + fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void { std.log.warn("glfw error={} message={s}", .{ code, desc }); diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 62020e4eb..4c4cb9429 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -115,6 +115,11 @@ pub const App = struct { @panic("This should not be called with GTK."); } + pub fn redrawSurface(self: *App, surface: *Surface) void { + _ = self; + surface.invalidate(); + } + pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void { _ = parent_; @@ -169,42 +174,30 @@ pub const Surface = struct { /// The app we're part of app: *App, + /// Our GTK area + gl_area: *c.GtkGLArea, + /// The core surface backing this surface core_surface: CoreSurface, + /// Cached metrics about the surface from GTK callbacks. + size: apprt.SurfaceSize, + pub fn init(self: *Surface, app: *App, opts: Options) !void { // Build our result self.* = .{ .app = app, + .gl_area = opts.gl_area, .core_surface = undefined, + .size = .{ .width = 800, .height = 600 }, }; errdefer self.* = undefined; // Create the GL area that will contain our surface - _ = c.g_signal_connect_data( - opts.gl_area, - "realize", - c.G_CALLBACK(>kRealize), - self, - null, - c.G_CONNECT_DEFAULT, - ); - _ = c.g_signal_connect_data( - opts.gl_area, - "render", - c.G_CALLBACK(>kRender), - null, - null, - c.G_CONNECT_DEFAULT, - ); - _ = c.g_signal_connect_data( - opts.gl_area, - "destroy", - c.G_CALLBACK(>kDestroy), - self, - null, - c.G_CONNECT_DEFAULT, - ); + _ = c.g_signal_connect_data(opts.gl_area, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(opts.gl_area, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(opts.gl_area, "render", c.G_CALLBACK(>kRender), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(opts.gl_area, "resize", c.G_CALLBACK(>kResize), self, null, c.G_CONNECT_DEFAULT); } fn realize(self: *Surface) !void { @@ -220,16 +213,33 @@ pub const Surface = struct { self, ); errdefer self.core_surface.deinit(); + + // Note we're realized + self.realized = true; + } + + fn render(self: *Surface) !void { + try self.core_surface.renderer.draw(); + } + + /// Invalidate the surface so that it forces a redraw on the next tick. + fn invalidate(self: *Surface) void { + c.gtk_gl_area_queue_render(self.gl_area); } fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { - _ = area; - log.debug("gl surface realized", .{}); + // We need to make the context current so we can call GL functions. + c.gtk_gl_area_make_current(area); + if (c.gtk_gl_area_get_error(area)) |err| { + log.err("surface failed to realize: {s}", .{err.*.message}); + return; + } + // realize means that our OpenGL context is ready, so we can now // initialize the core surface which will setup the renderer. - const self = userdataSelf(ud orelse return); + const self = userdataSelf(ud.?); self.realize() catch |err| { // TODO: we need to destroy the GL area here. log.err("surface failed to realize: {}", .{err}); @@ -237,24 +247,45 @@ pub const Surface = struct { }; } + /// render singal fn gtkRender(area: *c.GtkGLArea, ctx: *c.GdkGLContext, ud: ?*anyopaque) callconv(.C) c.gboolean { _ = area; _ = ctx; - _ = ud; - log.debug("gl render", .{}); - const opengl = @import("../renderer/opengl/main.zig"); - opengl.clearColor(0, 0.5, 1, 1); - opengl.clear(opengl.c.GL_COLOR_BUFFER_BIT); + const self = userdataSelf(ud.?); + self.render() catch |err| { + log.err("surface failed to render: {}", .{err}); + return 0; + }; return 1; } + /// render singal + fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque) callconv(.C) void { + _ = area; + log.debug("gl resize {} {}", .{ width, height }); + + const self = userdataSelf(ud.?); + self.size = .{ + .width = @intCast(u32, width), + .height = @intCast(u32, height), + }; + + // Call the primary callback. + if (self.realized) { + self.core_surface.sizeCallback(self.size) catch |err| { + log.err("error in size callback err={}", .{err}); + return; + }; + } + } + /// "destroy" signal for surface fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { _ = v; - const self = userdataSelf(ud orelse return); + const self = userdataSelf(ud.?); const alloc = self.app.core_app.alloc; self.deinit(); alloc.destroy(self); @@ -283,8 +314,7 @@ pub const Surface = struct { } pub fn getSize(self: *const Surface) !apprt.SurfaceSize { - _ = self; - return .{ .width = 800, .height = 600 }; + return self.size; } pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 9a2a2fc47..b041f8d2f 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -34,7 +34,7 @@ const CellsLRU = lru.AutoHashMap(struct { /// The runtime can request a single-threaded draw by setting this boolean /// to true. In this case, the renderer.draw() call is expected to be called /// from the runtime. -const single_threaded_draw = if (@hasDecl(apprt.Surface, "opengl_single_threaded_draw")) +pub const single_threaded_draw = if (@hasDecl(apprt.Surface, "opengl_single_threaded_draw")) apprt.Surface.opengl_single_threaded_draw else false; diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 2ffd0a1df..12424272d 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -10,6 +10,7 @@ const apprt = @import("../apprt.zig"); const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue; const tracy = @import("tracy"); const trace = tracy.trace; +const App = @import("../App.zig"); const Allocator = std.mem.Allocator; const log = std.log.scoped(.renderer_thread); @@ -60,6 +61,9 @@ state: *renderer.State, /// this is a blocking queue so if it is full you will get errors (or block). mailbox: *Mailbox, +/// Mailbox to send messages to the app thread +app_mailbox: App.Mailbox, + /// Initialize the thread. This does not START the thread. This only sets /// up all the internal state necessary prior to starting the thread. It /// is up to the caller to start the thread with the threadMain entrypoint. @@ -68,6 +72,7 @@ pub fn init( surface: *apprt.Surface, renderer_impl: *renderer.Renderer, state: *renderer.State, + app_mailbox: App.Mailbox, ) !Thread { // Create our event loop. var loop = try xev.Loop.init(.{}); @@ -104,6 +109,7 @@ pub fn init( .renderer = renderer_impl, .state = state, .mailbox = mailbox, + .app_mailbox = app_mailbox, }; } @@ -307,6 +313,15 @@ fn renderCallback( t.renderer.render(t.surface, t.state) catch |err| log.warn("error rendering err={}", .{err}); + + // If we're doing single-threaded GPU calls then we also wake up the + // app thread to redraw at this point. + if (renderer.Renderer == renderer.OpenGL and + renderer.OpenGL.single_threaded_draw) + { + _ = t.app_mailbox.push(.{ .redraw_surface = t.surface }, .{ .instant = {} }); + } + return .disarm; } diff --git a/src/renderer/opengl/Program.zig b/src/renderer/opengl/Program.zig index 5140b7517..2e2978363 100644 --- a/src/renderer/opengl/Program.zig +++ b/src/renderer/opengl/Program.zig @@ -69,6 +69,7 @@ pub inline fn link(p: Program) !void { pub inline fn use(p: Program) !Binding { glad.context.UseProgram.?(p.id); + try errors.getError(); return Binding{}; } diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig index 446f0a9ad..f52d12859 100644 --- a/src/renderer/opengl/Texture.zig +++ b/src/renderer/opengl/Texture.zig @@ -9,6 +9,7 @@ id: c.GLuint, pub inline fn active(target: c.GLenum) !void { glad.context.ActiveTexture.?(target); + try errors.getError(); } /// Enun for possible texture binding targets. @@ -153,6 +154,7 @@ pub inline fn create() !Texture { /// glBindTexture pub inline fn bind(v: Texture, target: Target) !Binding { glad.context.BindTexture.?(@enumToInt(target), v.id); + try errors.getError(); return Binding{ .target = target }; } diff --git a/src/renderer/opengl/VertexArray.zig b/src/renderer/opengl/VertexArray.zig index 6222a2b68..b86794042 100644 --- a/src/renderer/opengl/VertexArray.zig +++ b/src/renderer/opengl/VertexArray.zig @@ -2,6 +2,7 @@ const VertexArray = @This(); const c = @import("c.zig"); const glad = @import("glad.zig"); +const errors = @import("errors.zig"); id: c.GLuint, @@ -20,6 +21,7 @@ pub inline fn unbind() !void { /// glBindVertexArray pub inline fn bind(v: VertexArray) !void { glad.context.BindVertexArray.?(v.id); + try errors.getError(); } pub inline fn destroy(v: VertexArray) void {