From 67dce5ce0e7dfbb9dca356bc3072ba4fea25757c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Nov 2023 12:28:36 -0800 Subject: [PATCH 01/55] update zig-objc --- build.zig.zon | 4 ++-- src/apprt/glfw.zig | 2 +- src/os/locale.zig | 2 +- src/os/macos_version.zig | 2 +- src/os/mouse.zig | 2 +- src/renderer/Metal.zig | 6 +++--- src/renderer/metal/image.zig | 2 +- src/renderer/metal/shaders.zig | 8 ++++---- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index ed9fec81b..1ba57e1b2 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,8 +13,8 @@ .hash = "12202da6b8e9024c653f5d67f55a8065b401c42b3c08b69333d95400fe85d6019a59", }, .zig_objc = .{ - .url = "https://github.com/mitchellh/zig-objc/archive/146a50bb018d8e1ac5b9a1454d9db9a5eba5361f.tar.gz", - .hash = "12209f62dae4fccae478f5bd5670725c55308d8d985506110ba122ee2fb5e73122e0", + .url = "https://github.com/mitchellh/zig-objc/archive/a38331cb6ee366b3f22d0068297810ef14c0c400.tar.gz", + .hash = "1220dcb34ec79a9b02c46372a41a446212f2366e7c69c8eba68e88f0f25b5ddf475d", }, .zig_js = .{ .url = "https://github.com/mitchellh/zig-js/archive/60ac42ab137461cdba2b38cc6c5e16376470aae6.tar.gz", diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 53cd31c1d..c75e5a3ee 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -291,7 +291,7 @@ pub const App = struct { tabbing_id: *macos.foundation.String, pub fn init() !Darwin { - const NSWindow = objc.Class.getClass("NSWindow").?; + const NSWindow = objc.getClass("NSWindow").?; NSWindow.msgSend(void, objc.sel("setAllowsAutomaticWindowTabbing:"), .{true}); // Our tabbing ID allows all of our windows to group together diff --git a/src/os/locale.zig b/src/os/locale.zig index 3c3f35fcf..361f4fe62 100644 --- a/src/os/locale.zig +++ b/src/os/locale.zig @@ -70,7 +70,7 @@ fn setLangFromCocoa() void { defer pool.deinit(); // The classes we're going to need. - const NSLocale = objc.Class.getClass("NSLocale") orelse { + const NSLocale = objc.getClass("NSLocale") orelse { log.err("NSLocale class not found. Locale may be incorrect.", .{}); return; }; diff --git a/src/os/macos_version.zig b/src/os/macos_version.zig index 575dd2b72..e0b21560e 100644 --- a/src/os/macos_version.zig +++ b/src/os/macos_version.zig @@ -7,7 +7,7 @@ const objc = @import("objc"); pub fn macosVersionAtLeast(major: i64, minor: i64, patch: i64) bool { assert(builtin.target.isDarwin()); - const NSProcessInfo = objc.Class.getClass("NSProcessInfo").?; + const NSProcessInfo = objc.getClass("NSProcessInfo").?; const info = NSProcessInfo.msgSend(objc.Object, objc.sel("processInfo"), .{}); return info.msgSend(bool, objc.sel("isOperatingSystemAtLeastVersion:"), .{ NSOperatingSystemVersion{ .major = major, .minor = minor, .patch = patch }, diff --git a/src/os/mouse.zig b/src/os/mouse.zig index e8b6f0f10..1774399c9 100644 --- a/src/os/mouse.zig +++ b/src/os/mouse.zig @@ -9,7 +9,7 @@ const log = std.log.scoped(.os); pub fn clickInterval() ?u32 { // On macOS, we can ask the system. if (comptime builtin.target.isDarwin()) { - const NSEvent = objc.Class.getClass("NSEvent") orelse { + const NSEvent = objc.getClass("NSEvent") orelse { log.err("NSEvent class not found. Can't get click interval.", .{}); return null; }; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index d3eb22a1f..370eb3494 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -203,7 +203,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); const swapchain = swapchain: { - const CAMetalLayer = objc.Class.getClass("CAMetalLayer").?; + const CAMetalLayer = objc.getClass("CAMetalLayer").?; const swapchain = CAMetalLayer.msgSend(objc.Object, objc.sel("layer"), .{}); swapchain.setProperty("device", device.value); swapchain.setProperty("opaque", options.config.background_opacity >= 1); @@ -587,7 +587,7 @@ pub fn render( { // MTLRenderPassDescriptor const desc = desc: { - const MTLRenderPassDescriptor = objc.Class.getClass("MTLRenderPassDescriptor").?; + const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?; const desc = MTLRenderPassDescriptor.msgSend( objc.Object, objc.sel("renderPassDescriptor"), @@ -1625,7 +1625,7 @@ fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object // Create our descriptor const desc = init: { - const Class = objc.Class.getClass("MTLTextureDescriptor").?; + const Class = objc.getClass("MTLTextureDescriptor").?; const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; diff --git a/src/renderer/metal/image.zig b/src/renderer/metal/image.zig index 88de3d27a..154174697 100644 --- a/src/renderer/metal/image.zig +++ b/src/renderer/metal/image.zig @@ -218,7 +218,7 @@ pub const Image = union(enum) { fn initTexture(p: Pending, device: objc.Object) !objc.Object { // Create our descriptor const desc = init: { - const Class = objc.Class.getClass("MTLTextureDescriptor").?; + const Class = objc.getClass("MTLTextureDescriptor").?; const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 9001ede8f..3338e48b4 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -137,7 +137,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { // Cell as input. const vertex_desc = vertex_desc: { const desc = init: { - const Class = objc.Class.getClass("MTLVertexDescriptor").?; + const Class = objc.getClass("MTLVertexDescriptor").?; const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; @@ -242,7 +242,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { // Create our descriptor const desc = init: { - const Class = objc.Class.getClass("MTLRenderPipelineDescriptor").?; + const Class = objc.getClass("MTLRenderPipelineDescriptor").?; const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; @@ -320,7 +320,7 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { // Image as input. const vertex_desc = vertex_desc: { const desc = init: { - const Class = objc.Class.getClass("MTLVertexDescriptor").?; + const Class = objc.getClass("MTLVertexDescriptor").?; const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; @@ -392,7 +392,7 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { // Create our descriptor const desc = init: { - const Class = objc.Class.getClass("MTLRenderPipelineDescriptor").?; + const Class = objc.getClass("MTLRenderPipelineDescriptor").?; const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; From 0230222c0dbb2b9a1a8dcc3a91a225703fde3b9f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Nov 2023 14:03:38 -0800 Subject: [PATCH 02/55] pkg/macos: add CoreVide for DisplayLink --- pkg/macos/build.zig | 1 + pkg/macos/main.zig | 1 + pkg/macos/video.zig | 6 +++ pkg/macos/video/c.zig | 3 ++ pkg/macos/video/display_link.zig | 71 ++++++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+) create mode 100644 pkg/macos/video.zig create mode 100644 pkg/macos/video/c.zig create mode 100644 pkg/macos/video/display_link.zig diff --git a/pkg/macos/build.zig b/pkg/macos/build.zig index 942100e45..cf8b4a541 100644 --- a/pkg/macos/build.zig +++ b/pkg/macos/build.zig @@ -28,6 +28,7 @@ pub fn build(b: *std.Build) !void { lib.linkFramework("CoreFoundation"); lib.linkFramework("CoreGraphics"); lib.linkFramework("CoreText"); + lib.linkFramework("CoreVideo"); if (!target.isNative()) try apple_sdk.addPaths(b, lib); b.installArtifact(lib); diff --git a/pkg/macos/main.zig b/pkg/macos/main.zig index 73bc5d3f6..46071d55b 100644 --- a/pkg/macos/main.zig +++ b/pkg/macos/main.zig @@ -2,6 +2,7 @@ pub const foundation = @import("foundation.zig"); pub const graphics = @import("graphics.zig"); pub const os = @import("os.zig"); pub const text = @import("text.zig"); +pub const video = @import("video.zig"); test { @import("std").testing.refAllDecls(@This()); diff --git a/pkg/macos/video.zig b/pkg/macos/video.zig new file mode 100644 index 000000000..c04f5cf27 --- /dev/null +++ b/pkg/macos/video.zig @@ -0,0 +1,6 @@ +pub const c = @import("video/c.zig"); +pub usingnamespace @import("video/display_link.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/pkg/macos/video/c.zig b/pkg/macos/video/c.zig new file mode 100644 index 000000000..46c5d9ef3 --- /dev/null +++ b/pkg/macos/video/c.zig @@ -0,0 +1,3 @@ +pub usingnamespace @cImport({ + @cInclude("CoreVideo/CoreVideo.h"); +}); diff --git a/pkg/macos/video/display_link.zig b/pkg/macos/video/display_link.zig new file mode 100644 index 000000000..e7f4844a0 --- /dev/null +++ b/pkg/macos/video/display_link.zig @@ -0,0 +1,71 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const c = @import("c.zig"); + +pub const DisplayLink = opaque { + pub const Error = error{ + InvalidOperation, + }; + + pub fn createWithActiveCGDisplays() Allocator.Error!*DisplayLink { + var result: ?*DisplayLink = null; + if (c.CVDisplayLinkCreateWithActiveCGDisplays( + @ptrCast(&result), + ) != c.kCVReturnSuccess) + return error.OutOfMemory; + + return result orelse error.OutOfMemory; + } + + pub fn release(self: *DisplayLink) void { + c.CVDisplayLinkRelease(@ptrCast(self)); + } + + pub fn start(self: *DisplayLink) Error!void { + if (c.CVDisplayLinkStart(@ptrCast(self)) != c.kCVReturnSuccess) + return error.InvalidOperation; + } + + pub fn stop(self: *DisplayLink) Error!void { + if (c.CVDisplayLinkStop(@ptrCast(self)) != c.kCVReturnSuccess) + return error.InvalidOperation; + } + + pub fn isRunning(self: *DisplayLink) bool { + return c.CVDisplayLinkIsRunning(@ptrCast(self)) != 0; + } + + // Note: this purposely throws away a ton of arguments I didn't need. + // It would be trivial to refactor this into Zig types and properly + // pass this through. + pub fn setOutputCallback( + self: *DisplayLink, + comptime callbackFn: *const fn (*DisplayLink, ?*anyopaque) void, + userinfo: ?*anyopaque, + ) Error!void { + if (c.CVDisplayLinkSetOutputCallback( + @ptrCast(self), + @ptrCast(&(struct { + fn callback( + displayLink: *DisplayLink, + inNow: *const c.CVTimeStamp, + inOutputTime: *const c.CVTimeStamp, + flagsIn: c.CVOptionFlags, + flagsOut: *c.CVOptionFlags, + inner_userinfo: ?*anyopaque, + ) callconv(.C) c.CVReturn { + _ = inNow; + _ = inOutputTime; + _ = flagsIn; + _ = flagsOut; + + callbackFn(displayLink, inner_userinfo); + return c.kCVReturnSuccess; + } + }).callback), + userinfo, + ) != c.kCVReturnSuccess) + return error.InvalidOperation; + } +}; From 0e92f68228c09a8775b112dcfede0f573d185bdb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Nov 2023 14:13:39 -0800 Subject: [PATCH 03/55] renderer: separate update frame data from draw --- src/renderer/Metal.zig | 53 +++++++++++++++++++++++++---------------- src/renderer/Thread.zig | 8 ++++++- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 370eb3494..275184de0 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -76,6 +76,10 @@ background_color: terminal.color.RGB, /// by a terminal application cursor_color: ?terminal.color.RGB, +/// The current frame background color. This is only updated during +/// the updateFrame method. +current_background_color: terminal.color.RGB, + /// The current set of cells to render. This is rebuilt on every frame /// but we keep this around so that we don't reallocate. Each set of /// cells goes into a separate shader. @@ -271,6 +275,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .foreground_color = options.config.foreground, .background_color = options.config.background, .cursor_color = options.config.cursor_color, + .current_background_color = options.config.background, // Render state .cells_bg = .{}, @@ -448,8 +453,8 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void { }, .{ .forever = {} }); } -/// The primary render callback that is completely thread-safe. -pub fn render( +/// Update the frame data. +pub fn updateFrame( self: *Metal, surface: *apprt.Surface, state: *renderer.State, @@ -535,10 +540,6 @@ pub fn render( }; defer critical.screen.deinit(); - // @autoreleasepool {} - const pool = objc.AutoreleasePool.init(); - defer pool.deinit(); - // Build our GPU cells try self.rebuildCells( critical.selection, @@ -547,18 +548,8 @@ pub fn render( critical.cursor_style, ); - // Get our drawable (CAMetalDrawable) - const drawable = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); - - // If our font atlas changed, sync the texture data - if (self.font_group.atlas_greyscale.modified) { - try syncAtlasTexture(self.device, &self.font_group.atlas_greyscale, &self.texture_greyscale); - self.font_group.atlas_greyscale.modified = false; - } - if (self.font_group.atlas_color.modified) { - try syncAtlasTexture(self.device, &self.font_group.atlas_color, &self.texture_color); - self.font_group.atlas_color.modified = false; - } + // Update our background color + self.current_background_color = critical.bg; // Go through our images and see if we need to setup any textures. { @@ -580,6 +571,26 @@ pub fn render( } } } +} + +/// Draw the frame to the screen. +pub fn drawFrame(self: *Metal) !void { + // @autoreleasepool {} + const pool = objc.AutoreleasePool.init(); + defer pool.deinit(); + + // Get our drawable (CAMetalDrawable) + const drawable = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); + + // If our font atlas changed, sync the texture data + if (self.font_group.atlas_greyscale.modified) { + try syncAtlasTexture(self.device, &self.font_group.atlas_greyscale, &self.texture_greyscale); + self.font_group.atlas_greyscale.modified = false; + } + if (self.font_group.atlas_color.modified) { + try syncAtlasTexture(self.device, &self.font_group.atlas_color, &self.texture_color); + self.font_group.atlas_color.modified = false; + } // Command buffer (MTLCommandBuffer) const buffer = self.queue.msgSend(objc.Object, objc.sel("commandBuffer"), .{}); @@ -612,9 +623,9 @@ pub fn render( attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store)); attachment.setProperty("texture", texture); attachment.setProperty("clearColor", mtl.MTLClearColor{ - .red = @as(f32, @floatFromInt(critical.bg.r)) / 255, - .green = @as(f32, @floatFromInt(critical.bg.g)) / 255, - .blue = @as(f32, @floatFromInt(critical.bg.b)) / 255, + .red = @as(f32, @floatFromInt(self.current_background_color.r)) / 255, + .green = @as(f32, @floatFromInt(self.current_background_color.g)) / 255, + .blue = @as(f32, @floatFromInt(self.current_background_color.b)) / 255, .alpha = self.config.background_opacity, }); } diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 17abd6325..3a41f593d 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -346,7 +346,8 @@ fn renderCallback( _ = t.app_mailbox.push(.{ .redraw_inspector = t.surface }, .{ .instant = {} }); } - t.renderer.render( + // Update our frame data + t.renderer.updateFrame( t.surface, t.state, t.flags.cursor_blink_visible, @@ -359,8 +360,13 @@ fn renderCallback( renderer.OpenGL.single_threaded_draw) { _ = t.app_mailbox.push(.{ .redraw_surface = t.surface }, .{ .instant = {} }); + return .disarm; } + // Draw + t.renderer.drawFrame() catch |err| + log.warn("error drawing err={}", .{err}); + return .disarm; } From 389712a698c6fc1188d7faa7c21afc4379e2ef32 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Nov 2023 14:17:30 -0800 Subject: [PATCH 04/55] renderer/opengl: switch to new update vs draw --- src/apprt/gtk/Surface.zig | 2 +- src/renderer/Metal.zig | 4 +++- src/renderer/OpenGL.zig | 26 +++++++++++--------------- src/renderer/Thread.zig | 2 +- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 5a7b2da0b..deb88f9ff 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -252,7 +252,7 @@ pub fn deinit(self: *Surface) void { } fn render(self: *Surface) !void { - try self.core_surface.renderer.draw(); + try self.core_surface.renderer.drawFrame(self); } /// Queue the inspector to render if we have one. diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 275184de0..bc97f3b93 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -574,7 +574,9 @@ pub fn updateFrame( } /// Draw the frame to the screen. -pub fn drawFrame(self: *Metal) !void { +pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { + _ = surface; + // @autoreleasepool {} const pool = objc.AutoreleasePool.init(); defer pool.deinit(); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 07e1d030c..32781a7eb 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -576,12 +576,14 @@ fn resetFontMetrics( } /// The primary render callback that is completely thread-safe. -pub fn render( +pub fn updateFrame( self: *OpenGL, surface: *apprt.Surface, state: *renderer.State, cursor_blink_visible: bool, ) !void { + _ = surface; + // Data we extract out of the critical area. const Critical = struct { gl_bg: terminal.color.RGB, @@ -669,19 +671,6 @@ pub fn render( critical.cursor_style, ); } - - // We're out of the critical path now. Let's render. We only render if - // we're not single threaded. If we're single threaded we expect the - // runtime to call draw. - if (single_threaded_draw) return; - - try self.draw(); - - // Swap our window buffers - switch (apprt.runtime) { - else => @compileError("unsupported runtime"), - apprt.glfw => surface.window.swapBuffers(), - } } /// rebuildCells rebuilds all the GPU cells from our CPU state. This is a @@ -1448,7 +1437,7 @@ fn flushAtlas(self: *OpenGL) !void { /// Render renders the current cell state. This will not modify any of /// the cells. -pub fn draw(self: *OpenGL) !void { +pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { const t = trace(@src()); defer t.end(); @@ -1507,6 +1496,13 @@ pub fn draw(self: *OpenGL) !void { try self.drawCells(binding, self.cells_bg); try self.drawCells(binding, self.cells); + + // Swap our window buffers + switch (apprt.runtime) { + apprt.glfw => surface.window.swapBuffers(), + apprt.gtk => {}, + else => @compileError("unsupported runtime"), + } } /// Loads some set of cell data into our buffer and issues a draw call. diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 3a41f593d..926bd8e42 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -364,7 +364,7 @@ fn renderCallback( } // Draw - t.renderer.drawFrame() catch |err| + t.renderer.drawFrame(t.surface) catch |err| log.warn("error drawing err={}", .{err}); return .disarm; From 3095dce6850743cf67259eb56ae12725bff55ef1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Nov 2023 15:55:42 -0800 Subject: [PATCH 05/55] renderer/metal: wip for loading custom shader pipelines --- src/renderer/Metal.zig | 6 +- src/renderer/metal/shaders.zig | 113 ++++++++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 5 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index bc97f3b93..5b0ecf07b 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -257,8 +257,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { errdefer buf_instance.deinit(); // Initialize our shaders - var shaders = try Shaders.init(device); - errdefer shaders.deinit(); + var shaders = try Shaders.init(alloc, device, &.{}); + errdefer shaders.deinit(alloc); // Font atlas textures const texture_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale); @@ -328,7 +328,7 @@ pub fn deinit(self: *Metal) void { deinitMTLResource(self.texture_color); self.queue.msgSend(void, objc.sel("release"), .{}); - self.shaders.deinit(); + self.shaders.deinit(self.alloc); self.* = undefined; } diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 3338e48b4..ba55eefb7 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -12,10 +12,31 @@ const log = std.log.scoped(.metal); /// This contains the state for the shaders used by the Metal renderer. pub const Shaders = struct { library: objc.Object, + + /// The cell shader is the shader used to render the terminal cells. + /// It is a single shader that is used for both the background and + /// foreground. cell_pipeline: objc.Object, + + /// The image shader is the shader used to render images for things + /// like the Kitty image protocol. image_pipeline: objc.Object, - pub fn init(device: objc.Object) !Shaders { + /// Custom shaders to run against the final drawable texture. This + /// can be used to apply a lot of effects. Each shader is run in sequence + /// against the output of the previous shader. + post_pipelines: []const objc.Object, + + /// Initialize our shader set. + /// + /// "post_shaders" is an optional list of postprocess shaders to run + /// against the final drawable texture. This is an array of shader source + /// code, not file paths. + pub fn init( + alloc: Allocator, + device: objc.Object, + post_shaders: []const [:0]const u8, + ) !Shaders { const library = try initLibrary(device); errdefer library.msgSend(void, objc.sel("release"), .{}); @@ -25,17 +46,43 @@ pub const Shaders = struct { const image_pipeline = try initImagePipeline(device, library); errdefer image_pipeline.msgSend(void, objc.sel("release"), .{}); + const post_pipelines: []const objc.Object = initPostPipelines( + alloc, + device, + post_shaders, + ) catch |err| err: { + // If an error happens while building postprocess shaders we + // want to just not use any postprocess shaders since we don't + // want to block Ghostty from working. + log.warn("error initializing postprocess shaders err={}", .{err}); + break :err &.{}; + }; + errdefer if (post_pipelines.len > 0) { + for (post_pipelines) |pipeline| pipeline.msgSend(void, objc.sel("release"), .{}); + alloc.free(post_pipelines); + }; + return .{ .library = library, .cell_pipeline = cell_pipeline, .image_pipeline = image_pipeline, + .post_pipelines = post_pipelines, }; } - pub fn deinit(self: *Shaders) void { + pub fn deinit(self: *Shaders, alloc: Allocator) void { + // Release our primary shaders self.cell_pipeline.msgSend(void, objc.sel("release"), .{}); self.image_pipeline.msgSend(void, objc.sel("release"), .{}); self.library.msgSend(void, objc.sel("release"), .{}); + + // Release our postprocess shaders + if (self.post_pipelines.len > 0) { + for (self.post_pipelines) |pipeline| { + pipeline.msgSend(void, objc.sel("release"), .{}); + } + alloc.free(self.post_pipelines); + } } }; @@ -105,6 +152,68 @@ fn initLibrary(device: objc.Object) !objc.Object { return library; } +/// Initialize our custom shader pipelines. The shaders argument is a +/// set of shader source code, not file paths. +fn initPostPipelines( + alloc: Allocator, + device: objc.Object, + shaders: []const [:0]const u8, +) ![]const objc.Object { + // If we have no shaders, do nothing. + if (shaders.len == 0) return &.{}; + + // Keeps track of how many shaders we successfully wrote. + var i: usize = 0; + + // Initialize our result set. If any error happens, we undo everything. + var pipelines = try alloc.alloc(objc.Object, shaders.len); + errdefer { + for (pipelines[0..i]) |pipeline| { + pipeline.msgSend(void, objc.sel("release"), .{}); + } + alloc.free(pipelines); + } + + // Build each shader. Note we don't use "0.." to build our index + // because we need to keep track of our length to clean up above. + for (shaders) |source| { + pipelines[i] = try initPostPipeline(device, source); + i += 1; + } + + return pipelines; +} + +/// Initialize a single custom shader pipeline from shader source. +fn initPostPipeline(device: objc.Object, data: [:0]const u8) !objc.Object { + // Create our library which has the shader source + const library = library: { + const source = try macos.foundation.String.createWithBytes( + data, + .utf8, + false, + ); + defer source.release(); + + var err: ?*anyopaque = null; + const library = device.msgSend( + objc.Object, + objc.sel("newLibraryWithSource:options:error:"), + .{ source, @as(?*anyopaque, null), &err }, + ); + try checkError(err); + errdefer library.msgSend(void, objc.sel("release"), .{}); + + break :library library; + }; + // TODO: need to do this once we set the pipeline + //defer library.msgSend(void, objc.sel("release"), .{}); + + // TODO: need to implement the actual pipeline + + return library; +} + /// Initialize the cell render pipeline for our shader library. fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { // Get our vertex and fragment functions From 28246a80b8180d14e393ea808ad06b5186f58f75 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Nov 2023 18:52:22 -0800 Subject: [PATCH 06/55] renderer/metal: release some of our shader initialization objects --- src/renderer/metal/shaders.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index ba55eefb7..05b9993b4 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -348,6 +348,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { break :vertex_desc desc; }; + defer vertex_desc.msgSend(void, objc.sel("release"), .{}); // Create our descriptor const desc = init: { @@ -356,6 +357,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; }; + defer desc.msgSend(void, objc.sel("release"), .{}); // Set our properties desc.setProperty("vertexFunction", func_vert); @@ -393,6 +395,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { .{ desc, &err }, ); try checkError(err); + errdefer pipeline_state.msgSend(void, objc.sel("release"), .{}); return pipeline_state; } From c347148fd7fd11debb2a3525e1186567aa26a5b9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Nov 2023 22:40:40 -0800 Subject: [PATCH 07/55] renderer/metal: CRT effect, ugly hacky code --- src/renderer/Metal.zig | 160 ++++++++++++++++++++++++++++++- src/renderer/metal/api.zig | 10 ++ src/renderer/metal/shaders.zig | 93 ++++++++++++++++-- src/renderer/shaders/cell.metal | 17 ++++ src/renderer/shaders/temp3.metal | 116 ++++++++++++++++++++++ 5 files changed, 383 insertions(+), 13 deletions(-) create mode 100644 src/renderer/shaders/temp3.metal diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 5b0ecf07b..1bc4bfd1d 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -257,7 +257,9 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { errdefer buf_instance.deinit(); // Initialize our shaders - var shaders = try Shaders.init(alloc, device, &.{}); + var shaders = try Shaders.init(alloc, device, &.{ + @embedFile("shaders/temp3.metal"), + }); errdefer shaders.deinit(alloc); // Font atlas textures @@ -584,6 +586,36 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // Get our drawable (CAMetalDrawable) const drawable = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); + // Make our intermediate texture + const target = target: { + const desc = init: { + const Class = objc.getClass("MTLTextureDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + + // Set our properties + desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm)); + desc.setProperty("width", @as(c_ulong, @intCast(self.screen_size.?.width))); + desc.setProperty("height", @as(c_ulong, @intCast(self.screen_size.?.height))); + desc.setProperty( + "usage", + @intFromEnum(mtl.MTLTextureUsage.render_target) | + @intFromEnum(mtl.MTLTextureUsage.shader_read) | + @intFromEnum(mtl.MTLTextureUsage.shader_write), + ); + + const id = self.device.msgSend( + ?*anyopaque, + objc.sel("newTextureWithDescriptor:"), + .{desc}, + ) orelse return error.MetalFailed; + + break :target objc.Object.fromId(id); + }; + defer target.msgSend(void, objc.sel("release"), .{}); + // If our font atlas changed, sync the texture data if (self.font_group.atlas_greyscale.modified) { try syncAtlasTexture(self.device, &self.font_group.atlas_greyscale, &self.texture_greyscale); @@ -620,10 +652,10 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // Ghostty in XCode in debug mode it returns a CaptureMTLDrawable // which ironically doesn't implement CAMetalDrawable as a // property so we just send a message. - const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); + //const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); attachment.setProperty("loadAction", @intFromEnum(mtl.MTLLoadAction.clear)); attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store)); - attachment.setProperty("texture", texture); + attachment.setProperty("texture", target.value); attachment.setProperty("clearColor", mtl.MTLClearColor{ .red = @as(f32, @floatFromInt(self.current_background_color.r)) / 255, .green = @as(f32, @floatFromInt(self.current_background_color.g)) / 255, @@ -659,10 +691,132 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); } + { + // MTLRenderPassDescriptor + const desc = desc: { + const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?; + const desc = MTLRenderPassDescriptor.msgSend( + objc.Object, + objc.sel("renderPassDescriptor"), + .{}, + ); + + // Set our color attachment to be our drawable surface. + const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); + { + const attachment = attachments.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + // Texture is a property of CAMetalDrawable but if you run + // Ghostty in XCode in debug mode it returns a CaptureMTLDrawable + // which ironically doesn't implement CAMetalDrawable as a + // property so we just send a message. + const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); + attachment.setProperty("loadAction", @intFromEnum(mtl.MTLLoadAction.clear)); + attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store)); + attachment.setProperty("texture", texture); + attachment.setProperty("clearColor", mtl.MTLClearColor{ + .red = 0, + .green = 0, + .blue = 0, + .alpha = 1, + }); + } + + break :desc desc; + }; + + // MTLRenderCommandEncoder + const encoder = buffer.msgSend( + objc.Object, + objc.sel("renderCommandEncoderWithDescriptor:"), + .{desc.value}, + ); + defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); + + try self.drawPostShader(encoder, target.value); + } + buffer.msgSend(void, objc.sel("presentDrawable:"), .{drawable.value}); buffer.msgSend(void, objc.sel("commit"), .{}); } +var post_time: f32 = 1; + +fn drawPostShader( + self: *Metal, + encoder: objc.Object, + texture: objc.c.id, +) !void { + // Use our image shader pipeline + encoder.msgSend( + void, + objc.sel("setRenderPipelineState:"), + .{self.shaders.post_pipelines[0].value}, + ); + + // Set our uniform, which is the only shared buffer + encoder.msgSend( + void, + objc.sel("setVertexBytes:length:atIndex:"), + .{ + @as(*const anyopaque, @ptrCast(&self.uniforms)), + @as(c_ulong, @sizeOf(@TypeOf(self.uniforms))), + @as(c_ulong, 1), + }, + ); + + const Buffer = mtl_buffer.Buffer(mtl_shaders.PostUniforms); + var buf = try Buffer.initFill(self.device, &.{.{ + .resolution = .{ + @floatFromInt(self.screen_size.?.width), + @floatFromInt(self.screen_size.?.height), + 1, + }, + .time = post_time, + .time_delta = 1, + .frame_rate = 1, + .frame = 1, + .channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + .channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + .mouse = .{ 0, 0, 0, 0 }, + .date = .{ 0, 0, 0, 0 }, + .sample_rate = 1, + }}); + defer buf.deinit(); + post_time += 1; + + // Set our buffer + encoder.msgSend( + void, + objc.sel("setFragmentBuffer:offset:atIndex:"), + .{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, + ); + + encoder.msgSend( + void, + objc.sel("setFragmentTexture:atIndex:"), + .{ + texture, + @as(c_ulong, 0), + }, + ); + + // Draw! + encoder.msgSend( + void, + objc.sel("drawPrimitives:vertexStart:vertexCount:"), + .{ + @intFromEnum(mtl.MTLPrimitiveType.triangle_strip), + @as(c_ulong, 0), + @as(c_ulong, 4), + }, + ); +} + fn drawImagePlacements( self: *Metal, encoder: objc.Object, diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index f3dc2f835..1c9f175ac 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -57,6 +57,7 @@ pub const MTLVertexStepFunction = enum(c_ulong) { /// https://developer.apple.com/documentation/metal/mtlpixelformat?language=objc pub const MTLPixelFormat = enum(c_ulong) { r8unorm = 10, + rgba8unorm = 70, rgba8uint = 73, bgra8unorm = 80, }; @@ -98,6 +99,15 @@ pub const MTLBlendOperation = enum(c_ulong) { max = 4, }; +/// https://developer.apple.com/documentation/metal/mtltextureusage?language=objc +pub const MTLTextureUsage = enum(c_ulong) { + unknown = 0, + shader_read = 1, + shader_write = 2, + render_target = 4, + pixel_format_view = 8, +}; + /// https://developer.apple.com/documentation/metal/mtlresourceoptions?language=objc /// (incomplete, we only use this mode so we just hardcode it) pub const MTLResourceStorageModeShared: c_ulong = @intFromEnum(MTLStorageMode.shared) << 4; diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 05b9993b4..4edb7eb90 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -49,6 +49,7 @@ pub const Shaders = struct { const post_pipelines: []const objc.Object = initPostPipelines( alloc, device, + library, post_shaders, ) catch |err| err: { // If an error happens while building postprocess shaders we @@ -126,6 +127,20 @@ pub const Uniforms = extern struct { strikethrough_thickness: f32, }; +/// The uniforms used for custom postprocess shaders. +pub const PostUniforms = extern struct { + resolution: [3]f32 align(16), + time: f32 align(4), + time_delta: f32 align(4), + frame_rate: f32 align(4), + frame: i32 align(4), + channel_time: [4][4]f32 align(16), + channel_resolution: [4][4]f32 align(16), + mouse: [4]f32 align(16), + date: [4]f32 align(16), + sample_rate: f32 align(4), +}; + /// Initialize the MTLLibrary. A MTLLibrary is a collection of shaders. fn initLibrary(device: objc.Object) !objc.Object { // Hardcoded since this file isn't meant to be reusable. @@ -157,6 +172,7 @@ fn initLibrary(device: objc.Object) !objc.Object { fn initPostPipelines( alloc: Allocator, device: objc.Object, + library: objc.Object, shaders: []const [:0]const u8, ) ![]const objc.Object { // If we have no shaders, do nothing. @@ -177,7 +193,7 @@ fn initPostPipelines( // Build each shader. Note we don't use "0.." to build our index // because we need to keep track of our length to clean up above. for (shaders) |source| { - pipelines[i] = try initPostPipeline(device, source); + pipelines[i] = try initPostPipeline(device, library, source); i += 1; } @@ -185,9 +201,13 @@ fn initPostPipelines( } /// Initialize a single custom shader pipeline from shader source. -fn initPostPipeline(device: objc.Object, data: [:0]const u8) !objc.Object { +fn initPostPipeline( + device: objc.Object, + library: objc.Object, + data: [:0]const u8, +) !objc.Object { // Create our library which has the shader source - const library = library: { + const post_library = library: { const source = try macos.foundation.String.createWithBytes( data, .utf8, @@ -196,22 +216,75 @@ fn initPostPipeline(device: objc.Object, data: [:0]const u8) !objc.Object { defer source.release(); var err: ?*anyopaque = null; - const library = device.msgSend( + const post_library = device.msgSend( objc.Object, objc.sel("newLibraryWithSource:options:error:"), .{ source, @as(?*anyopaque, null), &err }, ); try checkError(err); - errdefer library.msgSend(void, objc.sel("release"), .{}); + errdefer post_library.msgSend(void, objc.sel("release"), .{}); - break :library library; + break :library post_library; }; - // TODO: need to do this once we set the pipeline - //defer library.msgSend(void, objc.sel("release"), .{}); + defer post_library.msgSend(void, objc.sel("release"), .{}); - // TODO: need to implement the actual pipeline + // Get our vertex and fragment functions + const func_vert = func_vert: { + const str = try macos.foundation.String.createWithBytes( + "post_vertex", + .utf8, + false, + ); + defer str.release(); - return library; + const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + break :func_vert objc.Object.fromId(ptr.?); + }; + const func_frag = func_frag: { + const str = try macos.foundation.String.createWithBytes( + "main0", + .utf8, + false, + ); + defer str.release(); + + const ptr = post_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + break :func_frag objc.Object.fromId(ptr.?); + }; + + // Create our descriptor + const desc = init: { + const Class = objc.getClass("MTLRenderPipelineDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + desc.setProperty("vertexFunction", func_vert); + desc.setProperty("fragmentFunction", func_frag); + + // Set our color attachment + const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); + { + const attachment = attachments.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + // Value is MTLPixelFormatBGRA8Unorm + attachment.setProperty("pixelFormat", @as(c_ulong, 80)); + } + + // Make our state + var err: ?*anyopaque = null; + const pipeline_state = device.msgSend( + objc.Object, + objc.sel("newRenderPipelineStateWithDescriptor:error:"), + .{ desc, &err }, + ); + try checkError(err); + + return pipeline_state; } /// Initialize the cell render pipeline for our shader library. diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 8c1e0750a..e6ba3f7ac 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -253,3 +253,20 @@ fragment float4 image_fragment( uint4 rgba = image.sample(textureSampler, in.tex_coord); return float4(rgba) / 255.0f; } + +//------------------------------------------------------------------- +// Post Shader +//------------------------------------------------------------------- +#pragma mark - Post Shader + +struct PostVertexOut { + float4 position [[ position ]]; +}; + +constant float2 post_pos[4] = { {-1,-1}, {1,-1}, {-1,1}, {1,1 } }; + +vertex PostVertexOut post_vertex(uint id [[ vertex_id ]]) { + PostVertexOut out; + out.position = float4(post_pos[id], 0, 1); + return out; +} diff --git a/src/renderer/shaders/temp3.metal b/src/renderer/shaders/temp3.metal new file mode 100644 index 000000000..c8cedc0d4 --- /dev/null +++ b/src/renderer/shaders/temp3.metal @@ -0,0 +1,116 @@ +#pragma clang diagnostic ignored "-Wmissing-prototypes" + +#include +#include + +using namespace metal; + +// Implementation of the GLSL mod() function, which is slightly different than Metal fmod() +template +inline Tx mod(Tx x, Ty y) +{ + return x - y * floor(x / y); +} + +struct Globals +{ + float3 iResolution; + float iTime; + float iTimeDelta; + float iFrameRate; + int iFrame; + float4 iChannelTime[4]; + float3 iChannelResolution[4]; + float4 iMouse; + float4 iDate; + float iSampleRate; +}; + +struct main0_out +{ + float4 _fragColor [[color(0)]]; +}; + +static inline __attribute__((always_inline)) +float2 curve(thread float2& uv) +{ + uv = (uv - float2(0.5)) * 2.0; + uv *= 1.10000002384185791015625; + uv.x *= (1.0 + pow(abs(uv.y) / 5.0, 2.0)); + uv.y *= (1.0 + pow(abs(uv.x) / 4.0, 2.0)); + uv = (uv / float2(2.0)) + float2(0.5); + uv = (uv * 0.920000016689300537109375) + float2(0.039999999105930328369140625); + return uv; +} + +static inline __attribute__((always_inline)) +void mainImage(thread float4& fragColor, thread const float2& fragCoord, constant Globals& _89, texture2d iChannel0, sampler iChannel0Smplr) +{ + float2 q = fragCoord / float2(_89.iResolution[0], _89.iResolution[1]); + float2 uv = q; + float2 param = uv; + float2 _100 = curve(param); + uv = _100; + float3 oricol = iChannel0.sample(iChannel0Smplr, float2(q.x, q.y)).xyz; + float x = ((sin((0.300000011920928955078125 * _89.iTime) + (uv.y * 21.0)) * sin((0.699999988079071044921875 * _89.iTime) + (uv.y * 29.0))) * sin((0.300000011920928955078125 + (0.3300000131130218505859375 * _89.iTime)) + (uv.y * 31.0))) * 0.001700000022538006305694580078125; + float3 col; + col.x = iChannel0.sample(iChannel0Smplr, float2((x + uv.x) + 0.001000000047497451305389404296875, uv.y + 0.001000000047497451305389404296875)).x + 0.0500000007450580596923828125; + col.y = iChannel0.sample(iChannel0Smplr, float2((x + uv.x) + 0.0, uv.y - 0.00200000009499490261077880859375)).y + 0.0500000007450580596923828125; + col.z = iChannel0.sample(iChannel0Smplr, float2((x + uv.x) - 0.00200000009499490261077880859375, uv.y + 0.0)).z + 0.0500000007450580596923828125; + col.x += (0.07999999821186065673828125 * iChannel0.sample(iChannel0Smplr, ((float2(x + 0.02500000037252902984619140625, -0.02700000070035457611083984375) * 0.75) + float2(uv.x + 0.001000000047497451305389404296875, uv.y + 0.001000000047497451305389404296875))).x); + col.y += (0.0500000007450580596923828125 * iChannel0.sample(iChannel0Smplr, ((float2(x + (-0.02199999988079071044921875), -0.0199999995529651641845703125) * 0.75) + float2(uv.x + 0.0, uv.y - 0.00200000009499490261077880859375))).y); + col.z += (0.07999999821186065673828125 * iChannel0.sample(iChannel0Smplr, ((float2(x + (-0.0199999995529651641845703125), -0.017999999225139617919921875) * 0.75) + float2(uv.x - 0.00200000009499490261077880859375, uv.y + 0.0))).z); + col = fast::clamp((col * 0.60000002384185791015625) + (((col * 0.4000000059604644775390625) * col) * 1.0), float3(0.0), float3(1.0)); + float vig = 0.0 + ((((16.0 * uv.x) * uv.y) * (1.0 - uv.x)) * (1.0 - uv.y)); + col *= float3(pow(vig, 0.300000011920928955078125)); + col *= float3(0.949999988079071044921875, 1.0499999523162841796875, 0.949999988079071044921875); + col *= 2.7999999523162841796875; + float scans = fast::clamp(0.3499999940395355224609375 + (0.3499999940395355224609375 * sin((3.5 * _89.iTime) + ((uv.y * _89.iResolution[1u]) * 1.5))), 0.0, 1.0); + float s = pow(scans, 1.7000000476837158203125); + col *= float3(0.4000000059604644775390625 + (0.699999988079071044921875 * s)); + col *= (1.0 + (0.00999999977648258209228515625 * sin(110.0 * _89.iTime))); + bool _352 = uv.x < 0.0; + bool _359; + if (!_352) + { + _359 = uv.x > 1.0; + } + else + { + _359 = _352; + } + if (_359) + { + col *= 0.0; + } + bool _366 = uv.y < 0.0; + bool _373; + if (!_366) + { + _373 = uv.y > 1.0; + } + else + { + _373 = _366; + } + if (_373) + { + col *= 0.0; + } + col *= (float3(1.0) - (float3(fast::clamp((mod(fragCoord.x, 2.0) - 1.0) * 2.0, 0.0, 1.0)) * 0.64999997615814208984375)); + float comp = smoothstep(0.100000001490116119384765625, 0.89999997615814208984375, sin(_89.iTime)); + fragColor = float4(col, 1.0); +} + +fragment main0_out main0(constant Globals& _89 [[buffer(0)]], texture2d iChannel0 [[texture(0)]], float4 gl_FragCoord [[position]]) +{ + constexpr sampler iChannel0Smplr(address::clamp_to_edge, filter::linear); + + main0_out out = {}; + float2 param_1 = gl_FragCoord.xy; + float4 param; + mainImage(param, param_1, _89, iChannel0, iChannel0Smplr); + out._fragColor = param; + return out; +} + From 6ebbea84d55c28658a2fc1cab5842c464fc61deb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Nov 2023 21:20:47 -0800 Subject: [PATCH 08/55] pkg/glslang: can build --- pkg/glslang/build.zig | 120 ++++++++++++++++++++++ pkg/glslang/build.zig.zon | 11 ++ pkg/glslang/c.zig | 4 + pkg/glslang/main.zig | 22 ++++ pkg/glslang/override/glslang/build_info.h | 62 +++++++++++ 5 files changed, 219 insertions(+) create mode 100644 pkg/glslang/build.zig create mode 100644 pkg/glslang/build.zig.zon create mode 100644 pkg/glslang/c.zig create mode 100644 pkg/glslang/main.zig create mode 100644 pkg/glslang/override/glslang/build_info.h diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig new file mode 100644 index 000000000..a5589e87c --- /dev/null +++ b/pkg/glslang/build.zig @@ -0,0 +1,120 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + _ = b.addModule("glslang", .{ .source_file = .{ .path = "main.zig" } }); + + const upstream = b.dependency("glslang", .{}); + const lib = try buildGlslang(b, upstream, target, optimize); + b.installArtifact(lib); + + { + const test_exe = b.addTest(.{ + .name = "test", + .root_source_file = .{ .path = "main.zig" }, + .target = target, + .optimize = optimize, + }); + test_exe.linkLibrary(lib); + const tests_run = b.addRunArtifact(test_exe); + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&tests_run.step); + } +} + +fn buildGlslang( + b: *std.Build, + upstream: *std.Build.Dependency, + target: std.zig.CrossTarget, + optimize: std.builtin.OptimizeMode, +) !*std.Build.Step.Compile { + const lib = b.addStaticLibrary(.{ + .name = "glslang", + .target = target, + .optimize = optimize, + }); + lib.linkLibC(); + lib.linkLibCpp(); + lib.addIncludePath(upstream.path("")); + lib.addIncludePath(.{ .path = "override" }); + + var flags = std.ArrayList([]const u8).init(b.allocator); + defer flags.deinit(); + + lib.addCSourceFiles(.{ + .dependency = upstream, + .flags = flags.items, + .files = &.{ + // GenericCodeGen + "glslang/GenericCodeGen/CodeGen.cpp", + "glslang/GenericCodeGen/Link.cpp", + + // MachineIndependent + //"MachineIndependent/glslang.y", + "glslang/MachineIndependent/glslang_tab.cpp", + "glslang/MachineIndependent/attribute.cpp", + "glslang/MachineIndependent/Constant.cpp", + "glslang/MachineIndependent/iomapper.cpp", + "glslang/MachineIndependent/InfoSink.cpp", + "glslang/MachineIndependent/Initialize.cpp", + "glslang/MachineIndependent/IntermTraverse.cpp", + "glslang/MachineIndependent/Intermediate.cpp", + "glslang/MachineIndependent/ParseContextBase.cpp", + "glslang/MachineIndependent/ParseHelper.cpp", + "glslang/MachineIndependent/PoolAlloc.cpp", + "glslang/MachineIndependent/RemoveTree.cpp", + "glslang/MachineIndependent/Scan.cpp", + "glslang/MachineIndependent/ShaderLang.cpp", + "glslang/MachineIndependent/SpirvIntrinsics.cpp", + "glslang/MachineIndependent/SymbolTable.cpp", + "glslang/MachineIndependent/Versions.cpp", + "glslang/MachineIndependent/intermOut.cpp", + "glslang/MachineIndependent/limits.cpp", + "glslang/MachineIndependent/linkValidate.cpp", + "glslang/MachineIndependent/parseConst.cpp", + "glslang/MachineIndependent/reflection.cpp", + "glslang/MachineIndependent/preprocessor/Pp.cpp", + "glslang/MachineIndependent/preprocessor/PpAtom.cpp", + "glslang/MachineIndependent/preprocessor/PpContext.cpp", + "glslang/MachineIndependent/preprocessor/PpScanner.cpp", + "glslang/MachineIndependent/preprocessor/PpTokens.cpp", + "glslang/MachineIndependent/propagateNoContraction.cpp", + + // C Interface + "glslang/CInterface/glslang_c_interface.cpp", + + // ResourceLimits + "glslang/ResourceLimits/ResourceLimits.cpp", + "glslang/ResourceLimits/resource_limits_c.cpp", + }, + }); + + if (!target.isWindows()) { + lib.addCSourceFiles(.{ + .dependency = upstream, + .flags = flags.items, + .files = &.{ + "glslang/OSDependent/Unix/ossource.cpp", + }, + }); + } else { + lib.addCSourceFiles(.{ + .dependency = upstream, + .flags = flags.items, + .files = &.{ + "glslang/OSDependent/Windows/ossource.cpp", + }, + }); + } + + lib.installHeadersDirectoryOptions(.{ + .source_dir = upstream.path(""), + .install_dir = .header, + .install_subdir = "", + .include_extensions = &.{".h"}, + }); + + return lib; +} diff --git a/pkg/glslang/build.zig.zon b/pkg/glslang/build.zig.zon new file mode 100644 index 000000000..d1ffcfa5c --- /dev/null +++ b/pkg/glslang/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = "glslang", + .version = "13.1.1", + .paths = .{""}, + .dependencies = .{ + .glslang = .{ + .url = "https://github.com/KhronosGroup/glslang/archive/refs/tags/13.1.1.tar.gz", + .hash = "1220481fe19def1172cd0728743019c0f440181a6342b62d03e24d05c70141516799", + }, + }, +} diff --git a/pkg/glslang/c.zig b/pkg/glslang/c.zig new file mode 100644 index 000000000..97d9046a5 --- /dev/null +++ b/pkg/glslang/c.zig @@ -0,0 +1,4 @@ +pub usingnamespace @cImport({ + @cInclude("glslang/Include/glslang_c_interface.h"); + @cInclude("glslang/Public/resource_limits_c.h"); +}); diff --git a/pkg/glslang/main.zig b/pkg/glslang/main.zig new file mode 100644 index 000000000..253a1add1 --- /dev/null +++ b/pkg/glslang/main.zig @@ -0,0 +1,22 @@ +pub const c = @import("c.zig"); + +test { + const input: c.glslang_input_t = .{ + .language = c.GLSLANG_SOURCE_GLSL, + .stage = c.GLSLANG_STAGE_VERTEX, + .client = c.GLSLANG_CLIENT_VULKAN, + .client_version = c.GLSLANG_TARGET_VULKAN_1_2, + .target_language = c.GLSLANG_TARGET_SPV, + .target_language_version = c.GLSLANG_TARGET_SPV_1_5, + .code = "", + .default_version = 100, + .default_profile = c.GLSLANG_NO_PROFILE, + .force_default_version_and_profile = 0, + .forward_compatible = 0, + .messages = c.GLSLANG_MSG_DEFAULT_BIT, + .resource = c.glslang_default_resource(), + }; + + const shader = c.glslang_shader_create(&input); + _ = shader; +} diff --git a/pkg/glslang/override/glslang/build_info.h b/pkg/glslang/override/glslang/build_info.h new file mode 100644 index 000000000..c25117eef --- /dev/null +++ b/pkg/glslang/override/glslang/build_info.h @@ -0,0 +1,62 @@ +// Copyright (C) 2020 The Khronos Group Inc. +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// Neither the name of The Khronos Group Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#ifndef GLSLANG_BUILD_INFO +#define GLSLANG_BUILD_INFO + +#define GLSLANG_VERSION_MAJOR 13 +#define GLSLANG_VERSION_MINOR 1 +#define GLSLANG_VERSION_PATCH 1 +#define GLSLANG_VERSION_FLAVOR "" + +#define GLSLANG_VERSION_GREATER_THAN(major, minor, patch) \ + ((GLSLANG_VERSION_MAJOR) > (major) || ((major) == GLSLANG_VERSION_MAJOR && \ + ((GLSLANG_VERSION_MINOR) > (minor) || ((minor) == GLSLANG_VERSION_MINOR && \ + (GLSLANG_VERSION_PATCH) > (patch))))) + +#define GLSLANG_VERSION_GREATER_OR_EQUAL_TO(major, minor, patch) \ + ((GLSLANG_VERSION_MAJOR) > (major) || ((major) == GLSLANG_VERSION_MAJOR && \ + ((GLSLANG_VERSION_MINOR) > (minor) || ((minor) == GLSLANG_VERSION_MINOR && \ + (GLSLANG_VERSION_PATCH >= (patch)))))) + +#define GLSLANG_VERSION_LESS_THAN(major, minor, patch) \ + ((GLSLANG_VERSION_MAJOR) < (major) || ((major) == GLSLANG_VERSION_MAJOR && \ + ((GLSLANG_VERSION_MINOR) < (minor) || ((minor) == GLSLANG_VERSION_MINOR && \ + (GLSLANG_VERSION_PATCH) < (patch))))) + +#define GLSLANG_VERSION_LESS_OR_EQUAL_TO(major, minor, patch) \ + ((GLSLANG_VERSION_MAJOR) < (major) || ((major) == GLSLANG_VERSION_MAJOR && \ + ((GLSLANG_VERSION_MINOR) < (minor) || ((minor) == GLSLANG_VERSION_MINOR && \ + (GLSLANG_VERSION_PATCH <= (patch)))))) + +#endif // GLSLANG_BUILD_INFO From 4afaea19d621f6f552e6d51c0217863cf39aa93c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Nov 2023 21:46:07 -0800 Subject: [PATCH 09/55] pkg/glslang: shader api --- pkg/glslang/build.zig | 7 +++++ pkg/glslang/init.zig | 9 ++++++ pkg/glslang/main.zig | 21 ++----------- pkg/glslang/shader.zig | 58 ++++++++++++++++++++++++++++++++++++ pkg/glslang/test.zig | 10 +++++++ pkg/glslang/test/simple.frag | 56 ++++++++++++++++++++++++++++++++++ 6 files changed, 143 insertions(+), 18 deletions(-) create mode 100644 pkg/glslang/init.zig create mode 100644 pkg/glslang/shader.zig create mode 100644 pkg/glslang/test.zig create mode 100644 pkg/glslang/test/simple.frag diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig index a5589e87c..8b05b357e 100644 --- a/pkg/glslang/build.zig +++ b/pkg/glslang/build.zig @@ -21,6 +21,9 @@ pub fn build(b: *std.Build) !void { const tests_run = b.addRunArtifact(test_exe); const test_step = b.step("test", "Run tests"); test_step.dependOn(&tests_run.step); + + // Uncomment this if we're debugging tests + b.installArtifact(test_exe); } } @@ -42,6 +45,10 @@ fn buildGlslang( var flags = std.ArrayList([]const u8).init(b.allocator); defer flags.deinit(); + try flags.appendSlice(&.{ + "-fno-sanitize=undefined", + "-fno-sanitize-trap=undefined", + }); lib.addCSourceFiles(.{ .dependency = upstream, diff --git a/pkg/glslang/init.zig b/pkg/glslang/init.zig new file mode 100644 index 000000000..33ddd081d --- /dev/null +++ b/pkg/glslang/init.zig @@ -0,0 +1,9 @@ +const c = @import("c.zig"); + +pub fn init() !void { + if (c.glslang_initialize_process() == 0) return error.GlslangInitFailed; +} + +pub fn finalize() void { + c.glslang_finalize_process(); +} diff --git a/pkg/glslang/main.zig b/pkg/glslang/main.zig index 253a1add1..f7dc6c2da 100644 --- a/pkg/glslang/main.zig +++ b/pkg/glslang/main.zig @@ -1,22 +1,7 @@ pub const c = @import("c.zig"); +pub usingnamespace @import("init.zig"); +pub usingnamespace @import("shader.zig"); test { - const input: c.glslang_input_t = .{ - .language = c.GLSLANG_SOURCE_GLSL, - .stage = c.GLSLANG_STAGE_VERTEX, - .client = c.GLSLANG_CLIENT_VULKAN, - .client_version = c.GLSLANG_TARGET_VULKAN_1_2, - .target_language = c.GLSLANG_TARGET_SPV, - .target_language_version = c.GLSLANG_TARGET_SPV_1_5, - .code = "", - .default_version = 100, - .default_profile = c.GLSLANG_NO_PROFILE, - .force_default_version_and_profile = 0, - .forward_compatible = 0, - .messages = c.GLSLANG_MSG_DEFAULT_BIT, - .resource = c.glslang_default_resource(), - }; - - const shader = c.glslang_shader_create(&input); - _ = shader; + @import("std").testing.refAllDecls(@This()); } diff --git a/pkg/glslang/shader.zig b/pkg/glslang/shader.zig new file mode 100644 index 000000000..90e5e192f --- /dev/null +++ b/pkg/glslang/shader.zig @@ -0,0 +1,58 @@ +const std = @import("std"); +const c = @import("c.zig"); +const testlib = @import("test.zig"); + +pub const Shader = opaque { + pub fn create(input: *const c.glslang_input_t) !*Shader { + if (c.glslang_shader_create(input)) |ptr| return @ptrCast(ptr); + return error.OutOfMemory; + } + + pub fn delete(self: *Shader) void { + c.glslang_shader_delete(@ptrCast(self)); + } + + pub fn preprocess(self: *Shader, input: *const c.glslang_input_t) !void { + if (c.glslang_shader_preprocess(@ptrCast(self), input) == 0) + return error.GlslangFailed; + } + + pub fn parse(self: *Shader, input: *const c.glslang_input_t) !void { + if (c.glslang_shader_parse(@ptrCast(self), input) == 0) + return error.GlslangFailed; + } + + pub fn getInfoLog(self: *Shader) ![:0]const u8 { + const ptr = c.glslang_shader_get_info_log(@ptrCast(self)); + return std.mem.sliceTo(ptr, 0); + } + + pub fn getDebugInfoLog(self: *Shader) ![:0]const u8 { + const ptr = c.glslang_shader_get_info_debug_log(@ptrCast(self)); + return std.mem.sliceTo(ptr, 0); + } +}; + +test { + const input: c.glslang_input_t = .{ + .language = c.GLSLANG_SOURCE_GLSL, + .stage = c.GLSLANG_STAGE_FRAGMENT, + .client = c.GLSLANG_CLIENT_VULKAN, + .client_version = c.GLSLANG_TARGET_VULKAN_1_2, + .target_language = c.GLSLANG_TARGET_SPV, + .target_language_version = c.GLSLANG_TARGET_SPV_1_5, + .code = @embedFile("test/simple.frag"), + .default_version = 100, + .default_profile = c.GLSLANG_NO_PROFILE, + .force_default_version_and_profile = 0, + .forward_compatible = 0, + .messages = c.GLSLANG_MSG_DEFAULT_BIT, + .resource = c.glslang_default_resource(), + }; + + try testlib.ensureInit(); + const shader = try Shader.create(&input); + defer shader.delete(); + try shader.preprocess(&input); + try shader.parse(&input); +} diff --git a/pkg/glslang/test.zig b/pkg/glslang/test.zig new file mode 100644 index 000000000..8cdf98f75 --- /dev/null +++ b/pkg/glslang/test.zig @@ -0,0 +1,10 @@ +const glslang = @import("main.zig"); + +var initialized: bool = false; + +/// Call this function before any other tests in this package to ensure that +/// the glslang library is initialized. +pub fn ensureInit() !void { + if (initialized) return; + try glslang.init(); +} diff --git a/pkg/glslang/test/simple.frag b/pkg/glslang/test/simple.frag new file mode 100644 index 000000000..c1cd903ce --- /dev/null +++ b/pkg/glslang/test/simple.frag @@ -0,0 +1,56 @@ +#version 430 core + +layout(binding = 0) uniform Globals { + uniform vec3 iResolution; + uniform float iTime; + uniform float iTimeDelta; + uniform float iFrameRate; + uniform int iFrame; + uniform float iChannelTime[4]; + uniform vec3 iChannelResolution[4]; + uniform vec4 iMouse; + uniform vec4 iDate; + uniform float iSampleRate; +}; + +layout(binding = 0) uniform sampler2D iChannel0; +layout(binding = 1) uniform sampler2D iChannel1; +layout(binding = 2) uniform sampler2D iChannel2; +layout(binding = 3) uniform sampler2D iChannel3; + +layout(location = 0) in vec4 gl_FragCoord; +layout(location = 0) out vec4 _fragColor; + +#define texture2D texture + +void mainImage( out vec4 fragColor, in vec2 fragCoord ); +void main() { mainImage (_fragColor, gl_FragCoord.xy); } + +#define t iTime + +void mainImage( out vec4 fragColor, in vec2 fragCoord ) +{ + // Normalized pixel coordinates (from 0 to 1) + vec2 uv = ( fragCoord - .5*iResolution.xy) / iResolution.y; + vec3 col = vec3(0.); + float a = atan(uv.y,uv.x); + float r = 0.5*length(uv); + float counter = 100.; + a = 4.*a+20.*r+50.*cos(r)*cos(.1*t)+abs(a*r); + float f = 0.02*abs(cos(a))/(r*r); + + + vec2 v = vec2(0.); + for(float i=0.;i2.){ + counter = i; + break; + } + } + + col=vec3(min(0.9,1.2*exp(-pow(f,0.45)*counter))); + + fragColor = min(0.9,1.2*exp(-pow(f,0.45)*counter) ) + * ( 0.7 + 0.3* cos(10.*r - 2.*t -vec4(.7,1.4,2.1,0) ) ); +} From 54ee8c1e3deb115a9f4cb25522cc1ea585c60e56 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Nov 2023 22:05:45 -0800 Subject: [PATCH 10/55] pkg/glslang: complete the API --- pkg/glslang/build.zig | 2 +- pkg/glslang/main.zig | 1 + pkg/glslang/program.zig | 60 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 pkg/glslang/program.zig diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig index 8b05b357e..1f220f99e 100644 --- a/pkg/glslang/build.zig +++ b/pkg/glslang/build.zig @@ -23,7 +23,7 @@ pub fn build(b: *std.Build) !void { test_step.dependOn(&tests_run.step); // Uncomment this if we're debugging tests - b.installArtifact(test_exe); + // b.installArtifact(test_exe); } } diff --git a/pkg/glslang/main.zig b/pkg/glslang/main.zig index f7dc6c2da..9b13662c0 100644 --- a/pkg/glslang/main.zig +++ b/pkg/glslang/main.zig @@ -1,5 +1,6 @@ pub const c = @import("c.zig"); pub usingnamespace @import("init.zig"); +pub usingnamespace @import("program.zig"); pub usingnamespace @import("shader.zig"); test { diff --git a/pkg/glslang/program.zig b/pkg/glslang/program.zig new file mode 100644 index 000000000..8a925ef15 --- /dev/null +++ b/pkg/glslang/program.zig @@ -0,0 +1,60 @@ +const std = @import("std"); +const c = @import("c.zig"); +const testlib = @import("test.zig"); +const Shader = @import("shader.zig").Shader; + +pub const Program = opaque { + pub fn create() !*Program { + if (c.glslang_program_create()) |ptr| return @ptrCast(ptr); + return error.OutOfMemory; + } + + pub fn delete(self: *Program) void { + c.glslang_program_delete(@ptrCast(self)); + } + + pub fn addShader(self: *Program, shader: *Shader) void { + c.glslang_program_add_shader(@ptrCast(self), @ptrCast(shader)); + } + + pub fn link(self: *Program, messages: c_int) !void { + if (c.glslang_program_link(@ptrCast(self), messages) != 0) return; + return error.GlslangFailed; + } + + pub fn spirvGenerate(self: *Program, stage: c.glslang_stage_t) void { + c.glslang_program_spirv_generate(@ptrCast(self), stage); + } + + pub fn spirvGetSize(self: *Program) usize { + return @intCast(c.glslang_program_spirv_get_size(@ptrCast(self))); + } + + pub fn spirvGet(self: *Program, buf: []u8) void { + c.glslang_program_spirv_get(@ptrCast(self), buf.ptr); + } + + pub fn spirvGetPtr(self: *Program) ![*]u8 { + return c.glslang_program_SPIRV_get_ptr(@ptrCast(self)); + } + + pub fn sprivGetMessages(self: *Program) ![:0]const u8 { + const ptr = c.glslang_program_spirv_get_messages(@ptrCast(self)); + return std.mem.sliceTo(ptr, 0); + } + + pub fn getInfoLog(self: *Program) ![:0]const u8 { + const ptr = c.glslang_program_get_info_log(@ptrCast(self)); + return std.mem.sliceTo(ptr, 0); + } + + pub fn getDebugInfoLog(self: *Program) ![:0]const u8 { + const ptr = c.glslang_program_get_info_debug_log(@ptrCast(self)); + return std.mem.sliceTo(ptr, 0); + } +}; + +test { + var program = try Program.create(); + defer program.delete(); +} From 9715eef3883180f9fc730b21fecf946cd9a817c8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Nov 2023 22:20:26 -0800 Subject: [PATCH 11/55] pkg/glslang: build SPIRV lib --- pkg/glslang/build.zig | 11 +++++++++++ pkg/glslang/program.zig | 8 ++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig index 1f220f99e..46d0ba4a4 100644 --- a/pkg/glslang/build.zig +++ b/pkg/glslang/build.zig @@ -95,6 +95,17 @@ fn buildGlslang( // ResourceLimits "glslang/ResourceLimits/ResourceLimits.cpp", "glslang/ResourceLimits/resource_limits_c.cpp", + + // SPIRV + "SPIRV/GlslangToSpv.cpp", + "SPIRV/InReadableOrder.cpp", + "SPIRV/Logger.cpp", + "SPIRV/SpvBuilder.cpp", + "SPIRV/SpvPostProcess.cpp", + "SPIRV/doc.cpp", + "SPIRV/SpvTools.cpp", + "SPIRV/disassemble.cpp", + "SPIRV/CInterface/spirv_c_interface.cpp", }, }); diff --git a/pkg/glslang/program.zig b/pkg/glslang/program.zig index 8a925ef15..4cfbc8fa1 100644 --- a/pkg/glslang/program.zig +++ b/pkg/glslang/program.zig @@ -23,15 +23,15 @@ pub const Program = opaque { } pub fn spirvGenerate(self: *Program, stage: c.glslang_stage_t) void { - c.glslang_program_spirv_generate(@ptrCast(self), stage); + c.glslang_program_SPIRV_generate(@ptrCast(self), stage); } pub fn spirvGetSize(self: *Program) usize { - return @intCast(c.glslang_program_spirv_get_size(@ptrCast(self))); + return @intCast(c.glslang_program_SPIRV_get_size(@ptrCast(self))); } pub fn spirvGet(self: *Program, buf: []u8) void { - c.glslang_program_spirv_get(@ptrCast(self), buf.ptr); + c.glslang_program_SPIRV_get(@ptrCast(self), buf.ptr); } pub fn spirvGetPtr(self: *Program) ![*]u8 { @@ -39,7 +39,7 @@ pub const Program = opaque { } pub fn sprivGetMessages(self: *Program) ![:0]const u8 { - const ptr = c.glslang_program_spirv_get_messages(@ptrCast(self)); + const ptr = c.glslang_program_SPIRV_get_messages(@ptrCast(self)); return std.mem.sliceTo(ptr, 0); } From 1678c3a038807efd4e6e07866e45222894e7de05 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Nov 2023 22:23:24 -0800 Subject: [PATCH 12/55] build: add glslang --- build.zig | 9 ++++++ build.zig.zon | 1 + src/App.zig | 35 +++++++++++++++++++++ src/temp.frag | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 src/temp.frag diff --git a/build.zig b/build.zig index f6164675e..df093ac8a 100644 --- a/build.zig +++ b/build.zig @@ -643,6 +643,10 @@ fn addDeps( .optimize = step.optimize, .@"enable-libpng" = true, }); + const glslang_dep = b.dependency("glslang", .{ + .target = step.target, + .optimize = step.optimize, + }); const mach_glfw_dep = b.dependency("mach_glfw", .{ .target = step.target, .optimize = step.optimize, @@ -718,6 +722,7 @@ fn addDeps( fontconfig_dep.module("fontconfig"), ); step.addModule("freetype", freetype_dep.module("freetype")); + step.addModule("glslang", glslang_dep.module("glslang")); step.addModule("harfbuzz", harfbuzz_dep.module("harfbuzz")); step.addModule("xev", libxev_dep.module("xev")); step.addModule("pixman", pixman_dep.module("pixman")); @@ -743,6 +748,10 @@ fn addDeps( try static_libs.append(tracy_dep.artifact("tracy").getEmittedBin()); } + // Glslang + step.linkLibrary(glslang_dep.artifact("glslang")); + try static_libs.append(glslang_dep.artifact("glslang").getEmittedBin()); + // Dynamic link if (!static) { step.addIncludePath(freetype_dep.path("")); diff --git a/build.zig.zon b/build.zig.zon index 1ba57e1b2..198743c45 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -29,6 +29,7 @@ .cimgui = .{ .path = "./pkg/cimgui" }, .fontconfig = .{ .path = "./pkg/fontconfig" }, .freetype = .{ .path = "./pkg/freetype" }, + .glslang = .{ .path = "./pkg/glslang" }, .harfbuzz = .{ .path = "./pkg/harfbuzz" }, .libpng = .{ .path = "./pkg/libpng" }, .macos = .{ .path = "./pkg/macos" }, diff --git a/src/App.zig b/src/App.zig index a09860a86..4536a2f69 100644 --- a/src/App.zig +++ b/src/App.zig @@ -73,6 +73,41 @@ pub fn create( }; errdefer app.surfaces.deinit(alloc); + // TODO: remove this temporary crap + // const glslang = @import("glslang"); + // try glslang.init(); + // { + // const c = glslang.c; + // const glsl_input: c.glslang_input_t = .{ + // .language = c.GLSLANG_SOURCE_GLSL, + // .stage = c.GLSLANG_STAGE_FRAGMENT, + // .client = c.GLSLANG_CLIENT_VULKAN, + // .client_version = c.GLSLANG_TARGET_VULKAN_1_2, + // .target_language = c.GLSLANG_TARGET_SPV, + // .target_language_version = c.GLSLANG_TARGET_SPV_1_5, + // .code = @embedFile("temp.frag"), + // .default_version = 100, + // .default_profile = c.GLSLANG_NO_PROFILE, + // .force_default_version_and_profile = 0, + // .forward_compatible = 0, + // .messages = c.GLSLANG_MSG_DEFAULT_BIT, + // .resource = c.glslang_default_resource(), + // }; + // + // const shader = try glslang.Shader.create(&glsl_input); + // defer shader.delete(); + // try shader.preprocess(&glsl_input); + // try shader.parse(&glsl_input); + // + // const program = try glslang.Program.create(); + // defer program.delete(); + // program.addShader(shader); + // try program.link(c.GLSLANG_MSG_SPV_RULES_BIT | c.GLSLANG_MSG_VULKAN_RULES_BIT); + // program.spirvGenerate(c.GLSLANG_STAGE_FRAGMENT); + // const size = program.spirvGetSize(); + // log.warn("SPIRV PROGRAM size={d}", .{size}); + // } + return app; } diff --git a/src/temp.frag b/src/temp.frag new file mode 100644 index 000000000..4c331118c --- /dev/null +++ b/src/temp.frag @@ -0,0 +1,84 @@ +#version 430 core + +layout(binding = 0) uniform Globals { + uniform vec3 iResolution; + uniform float iTime; + uniform float iTimeDelta; + uniform float iFrameRate; + uniform int iFrame; + uniform float iChannelTime[4]; + uniform vec3 iChannelResolution[4]; + uniform vec4 iMouse; + uniform vec4 iDate; + uniform float iSampleRate; +}; + +layout(binding = 0) uniform sampler2D iChannel0; +layout(binding = 1) uniform sampler2D iChannel1; +layout(binding = 2) uniform sampler2D iChannel2; +layout(binding = 3) uniform sampler2D iChannel3; + +layout(location = 0) in vec4 gl_FragCoord; +layout(location = 0) out vec4 _fragColor; + +#define texture2D texture + +void mainImage( out vec4 fragColor, in vec2 fragCoord ); +void main() { mainImage (_fragColor, gl_FragCoord.xy); } + +// Loosely based on postprocessing shader by inigo quilez, License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. + +vec2 curve(vec2 uv) +{ + uv = (uv - 0.5) * 2.0; + uv *= 1.1; + uv.x *= 1.0 + pow((abs(uv.y) / 5.0), 2.0); + uv.y *= 1.0 + pow((abs(uv.x) / 4.0), 2.0); + uv = (uv / 2.0) + 0.5; + uv = uv *0.92 + 0.04; + return uv; +} +void mainImage( out vec4 fragColor, in vec2 fragCoord ) +{ + vec2 q = fragCoord.xy / iResolution.xy; + vec2 uv = q; + uv = curve( uv ); + vec3 oricol = texture( iChannel0, vec2(q.x,q.y) ).xyz; + vec3 col; + float x = sin(0.3*iTime+uv.y*21.0)*sin(0.7*iTime+uv.y*29.0)*sin(0.3+0.33*iTime+uv.y*31.0)*0.0017; + + col.r = texture(iChannel0,vec2(x+uv.x+0.001,uv.y+0.001)).x+0.05; + col.g = texture(iChannel0,vec2(x+uv.x+0.000,uv.y-0.002)).y+0.05; + col.b = texture(iChannel0,vec2(x+uv.x-0.002,uv.y+0.000)).z+0.05; + col.r += 0.08*texture(iChannel0,0.75*vec2(x+0.025, -0.027)+vec2(uv.x+0.001,uv.y+0.001)).x; + col.g += 0.05*texture(iChannel0,0.75*vec2(x+-0.022, -0.02)+vec2(uv.x+0.000,uv.y-0.002)).y; + col.b += 0.08*texture(iChannel0,0.75*vec2(x+-0.02, -0.018)+vec2(uv.x-0.002,uv.y+0.000)).z; + + col = clamp(col*0.6+0.4*col*col*1.0,0.0,1.0); + + float vig = (0.0 + 1.0*16.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y)); + col *= vec3(pow(vig,0.3)); + + col *= vec3(0.95,1.05,0.95); + col *= 2.8; + + float scans = clamp( 0.35+0.35*sin(3.5*iTime+uv.y*iResolution.y*1.5), 0.0, 1.0); + + float s = pow(scans,1.7); + col = col*vec3( 0.4+0.7*s) ; + + col *= 1.0+0.01*sin(110.0*iTime); + if (uv.x < 0.0 || uv.x > 1.0) + col *= 0.0; + if (uv.y < 0.0 || uv.y > 1.0) + col *= 0.0; + + col*=1.0-0.65*vec3(clamp((mod(fragCoord.x, 2.0)-1.0)*2.0,0.0,1.0)); + + float comp = smoothstep( 0.1, 0.9, sin(iTime) ); + + // Remove the next line to stop cross-fade between original and postprocess +// col = mix( col, oricol, comp ); + + fragColor = vec4(col,1.0); +} From 6634ccc09c9b3bd16351755a2ffc39d070cd61a2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Nov 2023 22:36:41 -0800 Subject: [PATCH 13/55] pkg/spirv-cross --- pkg/spirv-cross/build.zig | 85 +++++++++++++++++++++++++++++++++++ pkg/spirv-cross/build.zig.zon | 11 +++++ pkg/spirv-cross/c.zig | 3 ++ pkg/spirv-cross/main.zig | 1 + 4 files changed, 100 insertions(+) create mode 100644 pkg/spirv-cross/build.zig create mode 100644 pkg/spirv-cross/build.zig.zon create mode 100644 pkg/spirv-cross/c.zig create mode 100644 pkg/spirv-cross/main.zig diff --git a/pkg/spirv-cross/build.zig b/pkg/spirv-cross/build.zig new file mode 100644 index 000000000..753aa4931 --- /dev/null +++ b/pkg/spirv-cross/build.zig @@ -0,0 +1,85 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + _ = b.addModule("spirv-cross", .{ .source_file = .{ .path = "main.zig" } }); + + const upstream = b.dependency("spirv_cross", .{}); + const lib = try buildSpirvCross(b, upstream, target, optimize); + b.installArtifact(lib); + + { + const test_exe = b.addTest(.{ + .name = "test", + .root_source_file = .{ .path = "main.zig" }, + .target = target, + .optimize = optimize, + }); + test_exe.linkLibrary(lib); + const tests_run = b.addRunArtifact(test_exe); + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&tests_run.step); + + // Uncomment this if we're debugging tests + // b.installArtifact(test_exe); + } +} + +fn buildSpirvCross( + b: *std.Build, + upstream: *std.Build.Dependency, + target: std.zig.CrossTarget, + optimize: std.builtin.OptimizeMode, +) !*std.Build.Step.Compile { + const lib = b.addStaticLibrary(.{ + .name = "spirv-cross", + .target = target, + .optimize = optimize, + }); + lib.linkLibC(); + lib.linkLibCpp(); + //lib.addIncludePath(upstream.path("")); + //lib.addIncludePath(.{ .path = "override" }); + + var flags = std.ArrayList([]const u8).init(b.allocator); + defer flags.deinit(); + try flags.appendSlice(&.{ + "-DSPIRV_CROSS_C_API_GLSL=1", + "-DSPIRV_CROSS_C_API_MSL=1", + + "-fno-sanitize=undefined", + "-fno-sanitize-trap=undefined", + }); + + lib.addCSourceFiles(.{ + .dependency = upstream, + .flags = flags.items, + .files = &.{ + // Core + "spirv_cross.cpp", + "spirv_parser.cpp", + "spirv_cross_parsed_ir.cpp", + "spirv_cfg.cpp", + + // C + "spirv_cross_c.cpp", + + // GLSL + "spirv_glsl.cpp", + + // MSL + "spirv_msl.cpp", + }, + }); + + lib.installHeadersDirectoryOptions(.{ + .source_dir = upstream.path("include"), + .install_dir = .header, + .install_subdir = "", + .include_extensions = &.{".h"}, + }); + + return lib; +} diff --git a/pkg/spirv-cross/build.zig.zon b/pkg/spirv-cross/build.zig.zon new file mode 100644 index 000000000..8338b7a61 --- /dev/null +++ b/pkg/spirv-cross/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = "spirv-cross", + .version = "13.1.1", + .paths = .{""}, + .dependencies = .{ + .spirv_cross = .{ + .url = "https://github.com/KhronosGroup/SPIRV-Cross/archive/4818f7e7ef7b7078a3a7a5a52c4a338e0dda22f4.tar.gz", + .hash = "1220b2d8a6cff1926ef28a29e312a0a503b555ebc2f082230b882410f49e672ac9c6", + }, + }, +} diff --git a/pkg/spirv-cross/c.zig b/pkg/spirv-cross/c.zig new file mode 100644 index 000000000..6ca10e1f2 --- /dev/null +++ b/pkg/spirv-cross/c.zig @@ -0,0 +1,3 @@ +pub usingnamespace @cImport({ + @cInclude("spriv_cross/external_interface.h"); +}); diff --git a/pkg/spirv-cross/main.zig b/pkg/spirv-cross/main.zig new file mode 100644 index 000000000..e66cd7094 --- /dev/null +++ b/pkg/spirv-cross/main.zig @@ -0,0 +1 @@ +pub const c = @import("c.zig"); From 7821e6aa3a0e8f0d8c3cdac2702921571778e7b7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Nov 2023 22:41:05 -0800 Subject: [PATCH 14/55] pkg/spirv-cross --- pkg/glslang/program.zig | 2 +- pkg/spirv-cross/build.zig | 4 ++-- pkg/spirv-cross/c.zig | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/glslang/program.zig b/pkg/glslang/program.zig index 4cfbc8fa1..03b4a2aeb 100644 --- a/pkg/glslang/program.zig +++ b/pkg/glslang/program.zig @@ -38,7 +38,7 @@ pub const Program = opaque { return c.glslang_program_SPIRV_get_ptr(@ptrCast(self)); } - pub fn sprivGetMessages(self: *Program) ![:0]const u8 { + pub fn spirvGetMessages(self: *Program) ![:0]const u8 { const ptr = c.glslang_program_SPIRV_get_messages(@ptrCast(self)); return std.mem.sliceTo(ptr, 0); } diff --git a/pkg/spirv-cross/build.zig b/pkg/spirv-cross/build.zig index 753aa4931..c603bf6ca 100644 --- a/pkg/spirv-cross/build.zig +++ b/pkg/spirv-cross/build.zig @@ -4,7 +4,7 @@ pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - _ = b.addModule("spirv-cross", .{ .source_file = .{ .path = "main.zig" } }); + _ = b.addModule("spirv_cross", .{ .source_file = .{ .path = "main.zig" } }); const upstream = b.dependency("spirv_cross", .{}); const lib = try buildSpirvCross(b, upstream, target, optimize); @@ -34,7 +34,7 @@ fn buildSpirvCross( optimize: std.builtin.OptimizeMode, ) !*std.Build.Step.Compile { const lib = b.addStaticLibrary(.{ - .name = "spirv-cross", + .name = "spirv_cross", .target = target, .optimize = optimize, }); diff --git a/pkg/spirv-cross/c.zig b/pkg/spirv-cross/c.zig index 6ca10e1f2..c3140ded3 100644 --- a/pkg/spirv-cross/c.zig +++ b/pkg/spirv-cross/c.zig @@ -1,3 +1,3 @@ pub usingnamespace @cImport({ - @cInclude("spriv_cross/external_interface.h"); + @cInclude("spirv_cross/external_interface.h"); }); From 291ca16c20d7aa527498b00b951c196c1f07dbf0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Nov 2023 22:41:12 -0800 Subject: [PATCH 15/55] build: add spirv-cross --- build.zig | 9 +++++++++ build.zig.zon | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/build.zig b/build.zig index df093ac8a..6146f93f5 100644 --- a/build.zig +++ b/build.zig @@ -647,6 +647,10 @@ fn addDeps( .target = step.target, .optimize = step.optimize, }); + const spirv_cross_dep = b.dependency("spirv_cross", .{ + .target = step.target, + .optimize = step.optimize, + }); const mach_glfw_dep = b.dependency("mach_glfw", .{ .target = step.target, .optimize = step.optimize, @@ -723,6 +727,7 @@ fn addDeps( ); step.addModule("freetype", freetype_dep.module("freetype")); step.addModule("glslang", glslang_dep.module("glslang")); + step.addModule("spirv_cross", spirv_cross_dep.module("spirv_cross")); step.addModule("harfbuzz", harfbuzz_dep.module("harfbuzz")); step.addModule("xev", libxev_dep.module("xev")); step.addModule("pixman", pixman_dep.module("pixman")); @@ -752,6 +757,10 @@ fn addDeps( step.linkLibrary(glslang_dep.artifact("glslang")); try static_libs.append(glslang_dep.artifact("glslang").getEmittedBin()); + // Spirv-Cross + step.linkLibrary(spirv_cross_dep.artifact("spirv_cross")); + try static_libs.append(spirv_cross_dep.artifact("spirv_cross").getEmittedBin()); + // Dynamic link if (!static) { step.addIncludePath(freetype_dep.path("")); diff --git a/build.zig.zon b/build.zig.zon index 198743c45..580c8593e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -29,7 +29,6 @@ .cimgui = .{ .path = "./pkg/cimgui" }, .fontconfig = .{ .path = "./pkg/fontconfig" }, .freetype = .{ .path = "./pkg/freetype" }, - .glslang = .{ .path = "./pkg/glslang" }, .harfbuzz = .{ .path = "./pkg/harfbuzz" }, .libpng = .{ .path = "./pkg/libpng" }, .macos = .{ .path = "./pkg/macos" }, @@ -37,6 +36,10 @@ .tracy = .{ .path = "./pkg/tracy" }, .zlib = .{ .path = "./pkg/zlib" }, + // Shader translation + .glslang = .{ .path = "./pkg/glslang" }, + .spirv_cross = .{ .path = "./pkg/spirv-cross" }, + // System headers .apple_sdk = .{ .path = "./pkg/apple-sdk" }, }, From 3a4aef2dcdb2466f1027afef2a7d531f5c35c20d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Nov 2023 22:43:07 -0800 Subject: [PATCH 16/55] pkg/spirv-cross: correct c header --- pkg/spirv-cross/build.zig | 2 +- pkg/spirv-cross/c.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/spirv-cross/build.zig b/pkg/spirv-cross/build.zig index c603bf6ca..13ff7d827 100644 --- a/pkg/spirv-cross/build.zig +++ b/pkg/spirv-cross/build.zig @@ -75,7 +75,7 @@ fn buildSpirvCross( }); lib.installHeadersDirectoryOptions(.{ - .source_dir = upstream.path("include"), + .source_dir = upstream.path(""), .install_dir = .header, .install_subdir = "", .include_extensions = &.{".h"}, diff --git a/pkg/spirv-cross/c.zig b/pkg/spirv-cross/c.zig index c3140ded3..42ad77dab 100644 --- a/pkg/spirv-cross/c.zig +++ b/pkg/spirv-cross/c.zig @@ -1,3 +1,3 @@ pub usingnamespace @cImport({ - @cInclude("spirv_cross/external_interface.h"); + @cInclude("spirv_cross_c.h"); }); From 1bd92619b19da249a3d2c6399dc442536d74abc6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 09:22:36 -0800 Subject: [PATCH 17/55] renderer: shadertoy functions --- pkg/glslang/main.zig | 1 + pkg/glslang/program.zig | 2 +- src/renderer.zig | 1 + src/renderer/shaders/shadertoy_prefix.glsl | 27 ++++ src/renderer/shaders/test_shadertoy_crt.glsl | 59 ++++++++ .../shaders/test_shadertoy_invalid.glsl | 12 ++ src/renderer/shadertoy.zig | 142 ++++++++++++++++++ 7 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 src/renderer/shaders/shadertoy_prefix.glsl create mode 100644 src/renderer/shaders/test_shadertoy_crt.glsl create mode 100644 src/renderer/shaders/test_shadertoy_invalid.glsl create mode 100644 src/renderer/shadertoy.zig diff --git a/pkg/glslang/main.zig b/pkg/glslang/main.zig index 9b13662c0..1a93e52be 100644 --- a/pkg/glslang/main.zig +++ b/pkg/glslang/main.zig @@ -1,4 +1,5 @@ pub const c = @import("c.zig"); +pub const testing = @import("test.zig"); pub usingnamespace @import("init.zig"); pub usingnamespace @import("program.zig"); pub usingnamespace @import("shader.zig"); diff --git a/pkg/glslang/program.zig b/pkg/glslang/program.zig index 03b4a2aeb..28fc8ab9b 100644 --- a/pkg/glslang/program.zig +++ b/pkg/glslang/program.zig @@ -35,7 +35,7 @@ pub const Program = opaque { } pub fn spirvGetPtr(self: *Program) ![*]u8 { - return c.glslang_program_SPIRV_get_ptr(@ptrCast(self)); + return @ptrCast(c.glslang_program_SPIRV_get_ptr(@ptrCast(self))); } pub fn spirvGetMessages(self: *Program) ![:0]const u8 { diff --git a/src/renderer.zig b/src/renderer.zig index b2a75ae36..65ad458b6 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -15,6 +15,7 @@ const WasmTarget = @import("os/wasm/target.zig").Target; pub usingnamespace @import("renderer/cursor.zig"); pub usingnamespace @import("renderer/message.zig"); pub usingnamespace @import("renderer/size.zig"); +pub const shadertoy = @import("renderer/shadertoy.zig"); pub const Metal = @import("renderer/Metal.zig"); pub const OpenGL = @import("renderer/OpenGL.zig"); pub const WebGL = @import("renderer/WebGL.zig"); diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl new file mode 100644 index 000000000..ff3749ff2 --- /dev/null +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -0,0 +1,27 @@ +#version 430 core + +layout(binding = 0) uniform Globals { + uniform vec3 iResolution; + uniform float iTime; + uniform float iTimeDelta; + uniform float iFrameRate; + uniform int iFrame; + uniform float iChannelTime[4]; + uniform vec3 iChannelResolution[4]; + uniform vec4 iMouse; + uniform vec4 iDate; + uniform float iSampleRate; +}; + +layout(binding = 0) uniform sampler2D iChannel0; +layout(binding = 1) uniform sampler2D iChannel1; +layout(binding = 2) uniform sampler2D iChannel2; +layout(binding = 3) uniform sampler2D iChannel3; + +layout(location = 0) in vec4 gl_FragCoord; +layout(location = 0) out vec4 _fragColor; + +#define texture2D texture + +void mainImage( out vec4 fragColor, in vec2 fragCoord ); +void main() { mainImage (_fragColor, gl_FragCoord.xy); } diff --git a/src/renderer/shaders/test_shadertoy_crt.glsl b/src/renderer/shaders/test_shadertoy_crt.glsl new file mode 100644 index 000000000..b8d6dbb49 --- /dev/null +++ b/src/renderer/shaders/test_shadertoy_crt.glsl @@ -0,0 +1,59 @@ +// This shader is NOT BUILD INTO Ghostty. This is only here for unit tests. + +// Loosely based on postprocessing shader by inigo quilez, License Creative +// Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. + +vec2 curve(vec2 uv) +{ + uv = (uv - 0.5) * 2.0; + uv *= 1.1; + uv.x *= 1.0 + pow((abs(uv.y) / 5.0), 2.0); + uv.y *= 1.0 + pow((abs(uv.x) / 4.0), 2.0); + uv = (uv / 2.0) + 0.5; + uv = uv *0.92 + 0.04; + return uv; +} +void mainImage( out vec4 fragColor, in vec2 fragCoord ) +{ + vec2 q = fragCoord.xy / iResolution.xy; + vec2 uv = q; + uv = curve( uv ); + vec3 oricol = texture( iChannel0, vec2(q.x,q.y) ).xyz; + vec3 col; + float x = sin(0.3*iTime+uv.y*21.0)*sin(0.7*iTime+uv.y*29.0)*sin(0.3+0.33*iTime+uv.y*31.0)*0.0017; + + col.r = texture(iChannel0,vec2(x+uv.x+0.001,uv.y+0.001)).x+0.05; + col.g = texture(iChannel0,vec2(x+uv.x+0.000,uv.y-0.002)).y+0.05; + col.b = texture(iChannel0,vec2(x+uv.x-0.002,uv.y+0.000)).z+0.05; + col.r += 0.08*texture(iChannel0,0.75*vec2(x+0.025, -0.027)+vec2(uv.x+0.001,uv.y+0.001)).x; + col.g += 0.05*texture(iChannel0,0.75*vec2(x+-0.022, -0.02)+vec2(uv.x+0.000,uv.y-0.002)).y; + col.b += 0.08*texture(iChannel0,0.75*vec2(x+-0.02, -0.018)+vec2(uv.x-0.002,uv.y+0.000)).z; + + col = clamp(col*0.6+0.4*col*col*1.0,0.0,1.0); + + float vig = (0.0 + 1.0*16.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y)); + col *= vec3(pow(vig,0.3)); + + col *= vec3(0.95,1.05,0.95); + col *= 2.8; + + float scans = clamp( 0.35+0.35*sin(3.5*iTime+uv.y*iResolution.y*1.5), 0.0, 1.0); + + float s = pow(scans,1.7); + col = col*vec3( 0.4+0.7*s) ; + + col *= 1.0+0.01*sin(110.0*iTime); + if (uv.x < 0.0 || uv.x > 1.0) + col *= 0.0; + if (uv.y < 0.0 || uv.y > 1.0) + col *= 0.0; + + col*=1.0-0.65*vec3(clamp((mod(fragCoord.x, 2.0)-1.0)*2.0,0.0,1.0)); + + float comp = smoothstep( 0.1, 0.9, sin(iTime) ); + + // Remove the next line to stop cross-fade between original and postprocess +// col = mix( col, oricol, comp ); + + fragColor = vec4(col,1.0); +} diff --git a/src/renderer/shaders/test_shadertoy_invalid.glsl b/src/renderer/shaders/test_shadertoy_invalid.glsl new file mode 100644 index 000000000..1cff1c7cf --- /dev/null +++ b/src/renderer/shaders/test_shadertoy_invalid.glsl @@ -0,0 +1,12 @@ +vec2 curve(vec2 uv) +{ + uv = (uv - 0.5) * 2.0; + uv *= 1.1; + uv.x *= 1.0 + pow((abs(uv.y) / 5.0), 2.0); + uv.y *= 1.0 + pow((abs(uv.x) / 4.0), 2.0); + uv = (uv / 2.0) + 0.5; + uv = uv *0.92 + 0.04; + return uv; +} + +// Missing mainImage! diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig new file mode 100644 index 000000000..00a2443e5 --- /dev/null +++ b/src/renderer/shadertoy.zig @@ -0,0 +1,142 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; +const glslang = @import("glslang"); + +/// Convert a ShaderToy shader into valid GLSL. +/// +/// ShaderToy shaders aren't full shaders, they're just implementing a +/// mainImage function and don't define any of the uniforms. This function +/// will convert the ShaderToy shader into a valid GLSL shader that can be +/// compiled and linked. +pub fn glslFromShader(writer: anytype, src: []const u8) !void { + const prefix = @embedFile("shaders/shadertoy_prefix.glsl"); + try writer.writeAll(prefix); + try writer.writeAll("\n\n"); + try writer.writeAll(src); +} + +/// Convert a GLSL shader into SPIR-V assembly. +pub fn spirvFromGlsl( + writer: anytype, + errlog: ?*SpirvLog, + src: [:0]const u8, +) !void { + // So we can run unit tests without fear. + if (builtin.is_test) try glslang.testing.ensureInit(); + + const c = glslang.c; + const input: c.glslang_input_t = .{ + .language = c.GLSLANG_SOURCE_GLSL, + .stage = c.GLSLANG_STAGE_FRAGMENT, + .client = c.GLSLANG_CLIENT_VULKAN, + .client_version = c.GLSLANG_TARGET_VULKAN_1_2, + .target_language = c.GLSLANG_TARGET_SPV, + .target_language_version = c.GLSLANG_TARGET_SPV_1_5, + .code = src.ptr, + .default_version = 100, + .default_profile = c.GLSLANG_NO_PROFILE, + .force_default_version_and_profile = 0, + .forward_compatible = 0, + .messages = c.GLSLANG_MSG_DEFAULT_BIT, + .resource = c.glslang_default_resource(), + }; + + const shader = try glslang.Shader.create(&input); + defer shader.delete(); + + shader.preprocess(&input) catch |err| { + if (errlog) |ptr| ptr.fromShader(shader) catch {}; + return err; + }; + shader.parse(&input) catch |err| { + if (errlog) |ptr| ptr.fromShader(shader) catch {}; + return err; + }; + + const program = try glslang.Program.create(); + defer program.delete(); + program.addShader(shader); + program.link( + c.GLSLANG_MSG_SPV_RULES_BIT | + c.GLSLANG_MSG_VULKAN_RULES_BIT, + ) catch |err| { + if (errlog) |ptr| ptr.fromProgram(program) catch {}; + return err; + }; + program.spirvGenerate(c.GLSLANG_STAGE_FRAGMENT); + const size = program.spirvGetSize(); + const ptr = try program.spirvGetPtr(); + try writer.writeAll(ptr[0..size]); +} + +/// Retrieve errors from spirv compilation. +pub const SpirvLog = struct { + alloc: Allocator, + info: [:0]const u8 = "", + debug: [:0]const u8 = "", + + pub fn deinit(self: *const SpirvLog) void { + if (self.info.len > 0) self.alloc.free(self.info); + if (self.debug.len > 0) self.alloc.free(self.debug); + } + + fn fromShader(self: *SpirvLog, shader: *glslang.Shader) !void { + const info = try shader.getInfoLog(); + const debug = try shader.getDebugInfoLog(); + self.info = ""; + self.debug = ""; + if (info.len > 0) self.info = try self.alloc.dupeZ(u8, info); + if (debug.len > 0) self.debug = try self.alloc.dupeZ(u8, debug); + } + + fn fromProgram(self: *SpirvLog, program: *glslang.Program) !void { + const info = try program.getInfoLog(); + const debug = try program.getDebugInfoLog(); + self.info = ""; + self.debug = ""; + if (info.len > 0) self.info = try self.alloc.dupeZ(u8, info); + if (debug.len > 0) self.debug = try self.alloc.dupeZ(u8, debug); + } +}; + +/// Convert ShaderToy shader to null-terminated glsl for testing. +fn testGlslZ(alloc: Allocator, src: []const u8) ![:0]const u8 { + var list = std.ArrayList(u8).init(alloc); + defer list.deinit(); + try glslFromShader(list.writer(), src); + return try list.toOwnedSliceSentinel(0); +} + +test "spirv" { + const testing = std.testing; + const alloc = testing.allocator; + + const src = try testGlslZ(alloc, test_crt); + defer alloc.free(src); + + var buf: [4096]u8 = undefined; + var buf_stream = std.io.fixedBufferStream(&buf); + const writer = buf_stream.writer(); + try spirvFromGlsl(writer, null, src); +} + +test "spirv invalid" { + const testing = std.testing; + const alloc = testing.allocator; + + const src = try testGlslZ(alloc, test_invalid); + defer alloc.free(src); + + var buf: [4096]u8 = undefined; + var buf_stream = std.io.fixedBufferStream(&buf); + const writer = buf_stream.writer(); + + var errlog: SpirvLog = .{ .alloc = alloc }; + defer errlog.deinit(); + try testing.expectError(error.GlslangFailed, spirvFromGlsl(writer, &errlog, src)); + try testing.expect(errlog.info.len > 0); +} + +const test_crt = @embedFile("shaders/test_shadertoy_crt.glsl"); +const test_invalid = @embedFile("shaders/test_shadertoy_invalid.glsl"); From dba78b20ca3ccb11d793866d1359928aa1ae44be Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 10:06:24 -0800 Subject: [PATCH 18/55] renderer: shadertoy convert to MSL --- pkg/glslang/program.zig | 4 +- src/renderer/shadertoy.zig | 79 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/pkg/glslang/program.zig b/pkg/glslang/program.zig index 28fc8ab9b..70d3c88cd 100644 --- a/pkg/glslang/program.zig +++ b/pkg/glslang/program.zig @@ -30,11 +30,11 @@ pub const Program = opaque { return @intCast(c.glslang_program_SPIRV_get_size(@ptrCast(self))); } - pub fn spirvGet(self: *Program, buf: []u8) void { + pub fn spirvGet(self: *Program, buf: []u32) void { c.glslang_program_SPIRV_get(@ptrCast(self), buf.ptr); } - pub fn spirvGetPtr(self: *Program) ![*]u8 { + pub fn spirvGetPtr(self: *Program) ![*]u32 { return @ptrCast(c.glslang_program_SPIRV_get_ptr(@ptrCast(self))); } diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 00a2443e5..81430932e 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -1,7 +1,9 @@ const std = @import("std"); const builtin = @import("builtin"); +const assert = std.debug.assert; const Allocator = std.mem.Allocator; const glslang = @import("glslang"); +const spvcross = @import("spirv_cross"); /// Convert a ShaderToy shader into valid GLSL. /// @@ -67,7 +69,9 @@ pub fn spirvFromGlsl( program.spirvGenerate(c.GLSLANG_STAGE_FRAGMENT); const size = program.spirvGetSize(); const ptr = try program.spirvGetPtr(); - try writer.writeAll(ptr[0..size]); + const ptr_u8: [*]u8 = @ptrCast(ptr); + const slice_u8: []u8 = ptr_u8[0 .. size * 4]; + try writer.writeAll(slice_u8); } /// Retrieve errors from spirv compilation. @@ -100,6 +104,59 @@ pub const SpirvLog = struct { } }; +/// Convert SPIR-V binary to MSL. +pub fn mslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 { + // Spir-V is always a multiple of 4 because it is written as a series of words + if (@mod(spv.len, 4) != 0) return error.SpirvInvalid; + + // Compiler context + const c = spvcross.c; + var ctx: c.spvc_context = undefined; + if (c.spvc_context_create(&ctx) != c.SPVC_SUCCESS) return error.SpvcFailed; + defer c.spvc_context_destroy(ctx); + + // It would be better to get this out into an output parameter to + // show users but for now we can just log it. + c.spvc_context_set_error_callback(ctx, @ptrCast(&(struct { + fn callback(_: ?*anyopaque, msg_ptr: [*c]const u8) callconv(.C) void { + const msg = std.mem.sliceTo(msg_ptr, 0); + std.log.warn("spirv-cross error message={s}", .{msg}); + } + }).callback), null); + + // Parse the Spir-V binary to an IR + var ir: c.spvc_parsed_ir = undefined; + if (c.spvc_context_parse_spirv( + ctx, + @ptrCast(@alignCast(spv.ptr)), + spv.len / 4, + &ir, + ) != c.SPVC_SUCCESS) { + return error.SpvcFailed; + } + + // Build our compiler to MSL + var compiler: c.spvc_compiler = undefined; + if (c.spvc_context_create_compiler( + ctx, + c.SPVC_BACKEND_MSL, + ir, + c.SPVC_CAPTURE_MODE_TAKE_OWNERSHIP, + &compiler, + ) != c.SPVC_SUCCESS) { + return error.SpvcFailed; + } + + // Compile the resulting string. This string pointer is owned by the + // context so we don't need to free it. + var result: [*:0]const u8 = undefined; + if (c.spvc_compiler_compile(compiler, @ptrCast(&result)) != c.SPVC_SUCCESS) { + return error.SpvcFailed; + } + + return try alloc.dupeZ(u8, std.mem.sliceTo(result, 0)); +} + /// Convert ShaderToy shader to null-terminated glsl for testing. fn testGlslZ(alloc: Allocator, src: []const u8) ![:0]const u8 { var list = std.ArrayList(u8).init(alloc); @@ -115,7 +172,7 @@ test "spirv" { const src = try testGlslZ(alloc, test_crt); defer alloc.free(src); - var buf: [4096]u8 = undefined; + var buf: [4096 * 4]u8 = undefined; var buf_stream = std.io.fixedBufferStream(&buf); const writer = buf_stream.writer(); try spirvFromGlsl(writer, null, src); @@ -128,7 +185,7 @@ test "spirv invalid" { const src = try testGlslZ(alloc, test_invalid); defer alloc.free(src); - var buf: [4096]u8 = undefined; + var buf: [4096 * 4]u8 = undefined; var buf_stream = std.io.fixedBufferStream(&buf); const writer = buf_stream.writer(); @@ -138,5 +195,21 @@ test "spirv invalid" { try testing.expect(errlog.info.len > 0); } +test "shadertoy to msl" { + const testing = std.testing; + const alloc = testing.allocator; + + const src = try testGlslZ(alloc, test_crt); + defer alloc.free(src); + + var spvlist = std.ArrayList(u8).init(alloc); + defer spvlist.deinit(); + try spirvFromGlsl(spvlist.writer(), null, src); + while (@mod(spvlist.items.len, 4) != 0) try spvlist.append(0); + + const msl = try mslFromSpv(alloc, spvlist.items); + defer alloc.free(msl); +} + const test_crt = @embedFile("shaders/test_shadertoy_crt.glsl"); const test_invalid = @embedFile("shaders/test_shadertoy_invalid.glsl"); From 9fbee7e6d1acbdb7d55436403630edc6a2a7ae70 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 13:44:58 -0800 Subject: [PATCH 19/55] renderer: turn assertion into error --- src/renderer/shadertoy.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 81430932e..3cef46e2d 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -205,7 +205,6 @@ test "shadertoy to msl" { var spvlist = std.ArrayList(u8).init(alloc); defer spvlist.deinit(); try spirvFromGlsl(spvlist.writer(), null, src); - while (@mod(spvlist.items.len, 4) != 0) try spvlist.append(0); const msl = try mslFromSpv(alloc, spvlist.items); defer alloc.free(msl); From 2520bb3d0712c14e2f01916d44520307a329b88b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 13:58:53 -0800 Subject: [PATCH 20/55] config: introduce RepeatablePath to automatically expand --- src/config/Config.zig | 112 ++++++++++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 32 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 11890d5b5..4f19f0bb6 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -528,7 +528,7 @@ keybind: Keybinds = .{}, /// /// Cycles are not allowed. If a cycle is detected, an error will be logged /// and the configuration file will be ignored. -@"config-file": RepeatableString = .{}, +@"config-file": RepeatablePath = .{}, /// Confirms that a surface should be closed before closing it. This defaults /// to true. If set to false, surfaces will close without any confirmation. @@ -1139,7 +1139,7 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { var buf_reader = std.io.bufferedReader(file.reader()); var iter = cli.args.lineIterator(buf_reader.reader()); try cli.args.parse(Config, alloc, self, &iter); - try self.expandConfigFiles(std.fs.path.dirname(config_path).?); + try self.expandPaths(std.fs.path.dirname(config_path).?); } else |err| switch (err) { error.FileNotFound => std.log.info( "homedir config not found, not loading path={s}", @@ -1168,15 +1168,15 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { try cli.args.parse(Config, alloc_gpa, self, &iter); // Config files loaded from the CLI args are relative to pwd - if (self.@"config-file".list.items.len > 0) { + if (self.@"config-file".value.list.items.len > 0) { var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - try self.expandConfigFiles(try std.fs.cwd().realpath(".", &buf)); + try self.expandPaths(try std.fs.cwd().realpath(".", &buf)); } } /// Load and parse the config files that were added in the "config-file" key. pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { - if (self.@"config-file".list.items.len == 0) return; + if (self.@"config-file".value.list.items.len == 0) return; const arena_alloc = self._arena.?.allocator(); // Keeps track of loaded files to prevent cycles. @@ -1185,8 +1185,8 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { const cwd = std.fs.cwd(); var i: usize = 0; - while (i < self.@"config-file".list.items.len) : (i += 1) { - const path = self.@"config-file".list.items[i]; + while (i < self.@"config-file".value.list.items.len) : (i += 1) { + const path = self.@"config-file".value.list.items[i]; // Error paths if (path.len == 0) continue; @@ -1223,37 +1223,22 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { var buf_reader = std.io.bufferedReader(file.reader()); var iter = cli.args.lineIterator(buf_reader.reader()); try cli.args.parse(Config, alloc_gpa, self, &iter); - try self.expandConfigFiles(std.fs.path.dirname(path).?); + try self.expandPaths(std.fs.path.dirname(path).?); } } /// Expand the relative paths in config-files to be absolute paths /// relative to the base directory. -fn expandConfigFiles(self: *Config, base: []const u8) !void { - assert(std.fs.path.isAbsolute(base)); - var dir = try std.fs.cwd().openDir(base, .{}); - defer dir.close(); - +fn expandPaths(self: *Config, base: []const u8) !void { const arena_alloc = self._arena.?.allocator(); - for (self.@"config-file".list.items, 0..) |path, i| { - // If it is already absolute we can ignore it. - if (path.len == 0 or std.fs.path.isAbsolute(path)) continue; - - // If it isn't absolute, we need to make it absolute relative to the base. - const abs = dir.realpathAlloc(arena_alloc, path) catch |err| { - try self._errors.add(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( - arena_alloc, - "error resolving config-file {s}: {}", - .{ path, err }, - ), - }); - self.@"config-file".list.items[i] = ""; - continue; - }; - - log.debug("expanding config-file path relative={s} abs={s}", .{ path, abs }); - self.@"config-file".list.items[i] = abs; + inline for (@typeInfo(Config).Struct.fields) |field| { + if (field.type == RepeatablePath) { + try @field(self, field.name).expand( + arena_alloc, + base, + &self._errors, + ); + } } } @@ -1885,6 +1870,69 @@ pub const RepeatableString = struct { } }; +/// RepeatablePath is like repeatable string but represents a path value. +/// The difference is that when loading the configuration any values for +/// this will be automatically expanded relative to the path of the config +/// file. +pub const RepeatablePath = struct { + const Self = @This(); + + value: RepeatableString = .{}, + + pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { + return self.value.parseCLI(alloc, input); + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const Self, alloc: Allocator) !Self { + return .{ + .value = try self.value.clone(alloc), + }; + } + + /// Compare if two of our value are requal. Required by Config. + pub fn equal(self: Self, other: Self) bool { + return self.value.equal(other.value); + } + + /// Expand all the paths relative to the base directory. + pub fn expand( + self: *Self, + alloc: Allocator, + base: []const u8, + errors: *ErrorList, + ) !void { + assert(std.fs.path.isAbsolute(base)); + var dir = try std.fs.cwd().openDir(base, .{}); + defer dir.close(); + + for (self.value.list.items, 0..) |path, i| { + // If it is already absolute we can ignore it. + if (path.len == 0 or std.fs.path.isAbsolute(path)) continue; + + // If it isn't absolute, we need to make it absolute relative + // to the base. + const abs = dir.realpathAlloc(alloc, path) catch |err| { + try errors.add(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + "error resolving config-file {s}: {}", + .{ path, err }, + ), + }); + self.value.list.items[i] = ""; + continue; + }; + + log.debug( + "expanding config-file path relative={s} abs={s}", + .{ path, abs }, + ); + self.value.list.items[i] = abs; + } + } +}; + /// FontVariation is a repeatable configuration value that sets a single /// font variation value. Font variations are configurations for what /// are often called "variable fonts." The font files usually end in From 1e572fb10b4c29e6b060269c8e272105173824a1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 15:19:54 -0800 Subject: [PATCH 21/55] renderer/metal: load custom shaders --- src/config/Config.zig | 23 ++++++++++++ src/main.zig | 4 +++ src/renderer/Metal.zig | 42 ++++++++++++++++++---- src/renderer/shadertoy.zig | 72 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 134 insertions(+), 7 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 4f19f0bb6..563b9627f 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -599,6 +599,29 @@ keybind: Keybinds = .{}, /// need KAM, you don't need it. @"vt-kam-allowed": bool = false, +/// Custom shaders to run after the default shaders. This is a file path +/// to a GLSL-syntax shader for all platforms. +/// +/// WARNING: Invalid shaders can cause Ghostty to become unusable such as by +/// causing the window to be completely black. If this happens, you can +/// unset this configuration to disable the shader. +/// +/// The shader API is identical to the ShaderToy API: you specify a `mainImage` +/// function and the available uniforms match ShaderToy. The iChannel0 uniform +/// is a texture containing the rendered terminal screen. +/// +/// If the shader fails to compile, the shader will be ignored. Any errors +/// related to shader compilation will not show up as configuration errors +/// and only show up in the log, since shader compilation happens after +/// configuration loading on the dedicated render thread. If your shader is +/// not working, another way to debug is to run the `ghostty +/// +custom-shader-compile` command which will compile the shader and show any +/// errors. For interactive development, use ShaderToy.com. +/// +/// This can be repeated multiple times to load multiple shaders. The shaders +/// will be run in the order they are specified. +@"custom-shader": RepeatablePath = .{}, + /// If anything other than false, fullscreen mode on macOS will not use the /// native fullscreen, but make the window fullscreen without animations and /// using a new space. It's faster than the native fullscreen mode since it diff --git a/src/main.zig b/src/main.zig index 16ad70ef3..3ef42d136 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator; const build_config = @import("build_config.zig"); const options = @import("build_options"); const glfw = @import("glfw"); +const glslang = @import("glslang"); const macos = @import("macos"); const tracy = @import("tracy"); const cli = @import("cli.zig"); @@ -267,6 +268,9 @@ pub const GlobalState = struct { // We need to make sure the process locale is set properly. Locale // affects a lot of behaviors in a shell. try internal_os.ensureLocale(self.alloc); + + // Initialize glslang for shader compilation + try glslang.init(); } /// Cleans up the global state. This doesn't _need_ to be called but diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 1bc4bfd1d..0b9d3ac70 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -10,6 +10,7 @@ const glfw = @import("glfw"); const objc = @import("objc"); const macos = @import("macos"); const imgui = @import("imgui"); +const glslang = @import("glslang"); const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); const font = @import("../font/main.zig"); @@ -17,8 +18,10 @@ const terminal = @import("../terminal/main.zig"); const renderer = @import("../renderer.zig"); const math = @import("../math.zig"); const Surface = @import("../Surface.zig"); +const shadertoy = @import("shadertoy.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; const Terminal = terminal.Terminal; const mtl = @import("metal/api.zig"); @@ -116,8 +119,10 @@ texture_color: objc.Object, // MTLTexture /// configuration. This must be exported so that we don't need to /// pass around Config pointers which makes memory management a pain. pub const DerivedConfig = struct { + arena: ArenaAllocator, + font_thicken: bool, - font_features: std.ArrayList([]const u8), + font_features: std.ArrayListUnmanaged([]const u8), font_styles: font.Group.StyleStatus, cursor_color: ?terminal.color.RGB, cursor_opacity: f64, @@ -128,17 +133,21 @@ pub const DerivedConfig = struct { selection_background: ?terminal.color.RGB, selection_foreground: ?terminal.color.RGB, invert_selection_fg_bg: bool, + custom_shaders: std.ArrayListUnmanaged([]const u8), pub fn init( alloc_gpa: Allocator, config: *const configpkg.Config, ) !DerivedConfig { + var arena = ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Copy our shaders + const custom_shaders = try config.@"custom-shader".value.list.clone(alloc); + // Copy our font features - var font_features = features: { - var clone = try config.@"font-feature".list.clone(alloc_gpa); - break :features clone.toManaged(alloc_gpa); - }; - errdefer font_features.deinit(); + const font_features = try config.@"font-feature".list.clone(alloc); // Get our font styles var font_styles = font.Group.StyleStatus.initFill(true); @@ -177,11 +186,15 @@ pub const DerivedConfig = struct { bg.toTerminalRGB() else null, + + .custom_shaders = custom_shaders, + + .arena = arena, }; } pub fn deinit(self: *DerivedConfig) void { - self.font_features.deinit(); + self.arena.deinit(); } }; @@ -203,6 +216,10 @@ pub fn surfaceInit(surface: *apprt.Surface) !void { } pub fn init(alloc: Allocator, options: renderer.Options) !Metal { + var arena = ArenaAllocator.init(alloc); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + // Initialize our metal stuff const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); @@ -256,6 +273,17 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }); errdefer buf_instance.deinit(); + // Load our custom shaders + const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( + arena_alloc, + options.config.custom_shaders.items, + .msl, + ) catch |err| err: { + log.warn("error loading custom shaders err={}", .{err}); + break :err &.{}; + }; + _ = custom_shaders; + // Initialize our shaders var shaders = try Shaders.init(alloc, device, &.{ @embedFile("shaders/temp3.metal"), diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 3cef46e2d..1d5610c7a 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -2,9 +2,81 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; const glslang = @import("glslang"); const spvcross = @import("spirv_cross"); +const log = std.log.scoped(.shadertoy); + +/// The target to load shaders for. +pub const Target = enum { msl }; + +/// Load a set of shaders from files and convert them to the target +/// format. The shader order is preserved. +pub fn loadFromFiles( + alloc_gpa: Allocator, + paths: []const []const u8, + target: Target, +) ![]const [:0]const u8 { + var list = std.ArrayList([:0]const u8).init(alloc_gpa); + defer list.deinit(); + errdefer for (list.items) |shader| alloc_gpa.free(shader); + + for (paths) |path| { + const shader = try loadFromFile(alloc_gpa, path, target); + log.info("loaded custom shader path={s}", .{path}); + try list.append(shader); + } + + return try list.toOwnedSlice(); +} + +/// Load a single shader from a file and convert it to the target language +/// ready to be used with renderers. +pub fn loadFromFile( + alloc_gpa: Allocator, + path: []const u8, + target: Target, +) ![:0]const u8 { + var arena = ArenaAllocator.init(alloc_gpa); + defer arena.deinit(); + const alloc = arena.allocator(); + + // Load the shader fiel + const cwd = std.fs.cwd(); + const file = try cwd.openFile(path, .{}); + defer file.close(); + + // Read it all into memory -- we don't expect shaders to be large. + var buf_reader = std.io.bufferedReader(file.reader()); + const src = try buf_reader.reader().readAllAlloc( + alloc, + 4 * 1024 * 1024, // 4MB + ); + + // Convert to full GLSL + const glsl: [:0]const u8 = glsl: { + var list = std.ArrayList(u8).init(alloc); + try glslFromShader(list.writer(), src); + try list.append(0); + break :glsl list.items[0 .. list.items.len - 1 :0]; + }; + + // Convert to SPIR-V + const spirv: []const u8 = spirv: { + var list = std.ArrayList(u8).init(alloc); + try spirvFromGlsl(list.writer(), null, glsl); + break :spirv list.items; + }; + + // Convert to MSL + return switch (target) { + // Important: using the alloc_gpa here on purpose because this + // is the final result that will be returned to the caller. + .msl => try mslFromSpv(alloc_gpa, spirv), + }; +} + /// Convert a ShaderToy shader into valid GLSL. /// /// ShaderToy shaders aren't full shaders, they're just implementing a From 01a73994cba440a4972277dff576dbd7a7587454 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 15:37:50 -0800 Subject: [PATCH 22/55] renderer/metal: setup sampler state --- pkg/glslang/build.zig | 1 - src/renderer/Metal.zig | 41 ++++++++++++++++---------------- src/renderer/metal/sampler.zig | 31 ++++++++++++++++++++++++ src/renderer/shaders/temp3.metal | 2 +- 4 files changed, 52 insertions(+), 23 deletions(-) create mode 100644 src/renderer/metal/sampler.zig diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig index 46d0ba4a4..201c0743e 100644 --- a/pkg/glslang/build.zig +++ b/pkg/glslang/build.zig @@ -103,7 +103,6 @@ fn buildGlslang( "SPIRV/SpvBuilder.cpp", "SPIRV/SpvPostProcess.cpp", "SPIRV/doc.cpp", - "SPIRV/SpvTools.cpp", "SPIRV/disassemble.cpp", "SPIRV/CInterface/spirv_c_interface.cpp", }, diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 0b9d3ac70..c2e9ebc30 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -27,6 +27,7 @@ const Terminal = terminal.Terminal; const mtl = @import("metal/api.zig"); const mtl_buffer = @import("metal/buffer.zig"); const mtl_image = @import("metal/image.zig"); +const mtl_sampler = @import("metal/sampler.zig"); const mtl_shaders = @import("metal/shaders.zig"); const Image = mtl_image.Image; const ImageMap = mtl_image.ImageMap; @@ -282,12 +283,10 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { log.warn("error loading custom shaders err={}", .{err}); break :err &.{}; }; - _ = custom_shaders; // Initialize our shaders - var shaders = try Shaders.init(alloc, device, &.{ - @embedFile("shaders/temp3.metal"), - }); + var shaders = try Shaders.init(alloc, device, custom_shaders); + //var shaders = try Shaders.init(alloc, device, &.{@embedFile("shaders/temp3.metal")}); errdefer shaders.deinit(alloc); // Font atlas textures @@ -779,23 +778,9 @@ fn drawPostShader( encoder: objc.Object, texture: objc.c.id, ) !void { - // Use our image shader pipeline - encoder.msgSend( - void, - objc.sel("setRenderPipelineState:"), - .{self.shaders.post_pipelines[0].value}, - ); - - // Set our uniform, which is the only shared buffer - encoder.msgSend( - void, - objc.sel("setVertexBytes:length:atIndex:"), - .{ - @as(*const anyopaque, @ptrCast(&self.uniforms)), - @as(c_ulong, @sizeOf(@TypeOf(self.uniforms))), - @as(c_ulong, 1), - }, - ); + // Build our sampler for our texture + var sampler = try mtl_sampler.Sampler.init(self.device); + defer sampler.deinit(); const Buffer = mtl_buffer.Buffer(mtl_shaders.PostUniforms); var buf = try Buffer.initFill(self.device, &.{.{ @@ -817,6 +802,20 @@ fn drawPostShader( defer buf.deinit(); post_time += 1; + // Use our custom shader pipeline + encoder.msgSend( + void, + objc.sel("setRenderPipelineState:"), + .{self.shaders.post_pipelines[0].value}, + ); + + // Set our sampler + encoder.msgSend( + void, + objc.sel("setFragmentSamplerState:atIndex:"), + .{ sampler.sampler.value, @as(c_ulong, 0) }, + ); + // Set our buffer encoder.msgSend( void, diff --git a/src/renderer/metal/sampler.zig b/src/renderer/metal/sampler.zig new file mode 100644 index 000000000..5f4d51cfc --- /dev/null +++ b/src/renderer/metal/sampler.zig @@ -0,0 +1,31 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const objc = @import("objc"); + +pub const Sampler = struct { + sampler: objc.Object, + + pub fn init(device: objc.Object) !Sampler { + const desc = init: { + const Class = objc.getClass("MTLSamplerDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + defer desc.msgSend(void, objc.sel("release"), .{}); + + const sampler = device.msgSend( + objc.Object, + objc.sel("newSamplerStateWithDescriptor:"), + .{desc}, + ); + errdefer sampler.msgSend(void, objc.sel("release"), .{}); + + return .{ .sampler = sampler }; + } + + pub fn deinit(self: *Sampler) void { + self.sampler.msgSend(void, objc.sel("release"), .{}); + } +}; diff --git a/src/renderer/shaders/temp3.metal b/src/renderer/shaders/temp3.metal index c8cedc0d4..b7a648608 100644 --- a/src/renderer/shaders/temp3.metal +++ b/src/renderer/shaders/temp3.metal @@ -102,7 +102,7 @@ void mainImage(thread float4& fragColor, thread const float2& fragCoord, constan fragColor = float4(col, 1.0); } -fragment main0_out main0(constant Globals& _89 [[buffer(0)]], texture2d iChannel0 [[texture(0)]], float4 gl_FragCoord [[position]]) +fragment main0_out main0(constant Globals& _89 [[buffer(0)]], texture2d iChannel0 [[texture(0)]], sampler iChannel0Smplr [[sampler(0)]], float4 gl_FragCoord [[position]]) { constexpr sampler iChannel0Smplr(address::clamp_to_edge, filter::linear); From bc70d192572b84fcf4ee028231c8c33a1e3955e9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 15:43:03 -0800 Subject: [PATCH 23/55] renderer/metal: set valid sampler properties --- src/renderer/metal/api.zig | 16 ++++++++++++++++ src/renderer/metal/sampler.zig | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index 1c9f175ac..f92489374 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -67,6 +67,22 @@ pub const MTLPurgeableState = enum(c_ulong) { empty = 4, }; +/// https://developer.apple.com/documentation/metal/mtlsamplerminmagfilter?language=objc +pub const MTLSamplerMinMagFilter = enum(c_ulong) { + nearest = 0, + linear = 1, +}; + +/// https://developer.apple.com/documentation/metal/mtlsampleraddressmode?language=objc +pub const MTLSamplerAddressMode = enum(c_ulong) { + clamp_to_edge = 0, + mirror_clamp_to_edge = 1, + repeat = 2, + mirror_repeat = 3, + clamp_to_zero = 4, + clamp_to_border_color = 5, +}; + /// https://developer.apple.com/documentation/metal/mtlblendfactor?language=objc pub const MTLBlendFactor = enum(c_ulong) { zero = 0, diff --git a/src/renderer/metal/sampler.zig b/src/renderer/metal/sampler.zig index 5f4d51cfc..c7a04df3a 100644 --- a/src/renderer/metal/sampler.zig +++ b/src/renderer/metal/sampler.zig @@ -3,6 +3,8 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const objc = @import("objc"); +const mtl = @import("api.zig"); + pub const Sampler = struct { sampler: objc.Object, @@ -14,6 +16,11 @@ pub const Sampler = struct { break :init id_init; }; defer desc.msgSend(void, objc.sel("release"), .{}); + desc.setProperty("rAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge)); + desc.setProperty("sAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge)); + desc.setProperty("tAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge)); + desc.setProperty("minFilter", @intFromEnum(mtl.MTLSamplerMinMagFilter.linear)); + desc.setProperty("magFilter", @intFromEnum(mtl.MTLSamplerMinMagFilter.linear)); const sampler = device.msgSend( objc.Object, From d3bc1ab6daabe29d01e7c7aacd0b0c50859bf355 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 15:54:09 -0800 Subject: [PATCH 24/55] renderer/metal: reuse an intermediate texture for custom shaders --- src/renderer/Metal.zig | 91 ++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index c2e9ebc30..d146cdbfa 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -116,6 +116,10 @@ swapchain: objc.Object, // CAMetalLayer texture_greyscale: objc.Object, // MTLTexture texture_color: objc.Object, // MTLTexture +/// The screen texture. This is only set if we have custom shaders. If +/// we don't have custom shaders, we render directly to the drawable. +texture_screen: ?objc.Object, // MTLTexture + /// The configuration for this renderer that is derived from the main /// configuration. This must be exported so that we don't need to /// pass around Config pointers which makes memory management a pain. @@ -332,6 +336,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .swapchain = swapchain, .texture_greyscale = texture_greyscale, .texture_color = texture_color, + .texture_screen = null, }; } @@ -355,6 +360,7 @@ pub fn deinit(self: *Metal) void { self.buf_instance.deinit(); deinitMTLResource(self.texture_greyscale); deinitMTLResource(self.texture_color); + if (self.texture_screen) |tex| deinitMTLResource(tex); self.queue.msgSend(void, objc.sel("release"), .{}); self.shaders.deinit(self.alloc); @@ -613,35 +619,12 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // Get our drawable (CAMetalDrawable) const drawable = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); - // Make our intermediate texture - const target = target: { - const desc = init: { - const Class = objc.getClass("MTLTextureDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; - }; - - // Set our properties - desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm)); - desc.setProperty("width", @as(c_ulong, @intCast(self.screen_size.?.width))); - desc.setProperty("height", @as(c_ulong, @intCast(self.screen_size.?.height))); - desc.setProperty( - "usage", - @intFromEnum(mtl.MTLTextureUsage.render_target) | - @intFromEnum(mtl.MTLTextureUsage.shader_read) | - @intFromEnum(mtl.MTLTextureUsage.shader_write), - ); - - const id = self.device.msgSend( - ?*anyopaque, - objc.sel("newTextureWithDescriptor:"), - .{desc}, - ) orelse return error.MetalFailed; - - break :target objc.Object.fromId(id); + // Get our screen texture. If we don't have a dedicated screen texture + // then we just use the drawable texture. + const screen_texture = self.texture_screen orelse tex: { + const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); + break :tex objc.Object.fromId(texture); }; - defer target.msgSend(void, objc.sel("release"), .{}); // If our font atlas changed, sync the texture data if (self.font_group.atlas_greyscale.modified) { @@ -682,7 +665,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { //const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); attachment.setProperty("loadAction", @intFromEnum(mtl.MTLLoadAction.clear)); attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store)); - attachment.setProperty("texture", target.value); + attachment.setProperty("texture", screen_texture.value); attachment.setProperty("clearColor", mtl.MTLClearColor{ .red = @as(f32, @floatFromInt(self.current_background_color.r)) / 255, .green = @as(f32, @floatFromInt(self.current_background_color.g)) / 255, @@ -718,6 +701,10 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); } + // If we have custom shaders AND we have a screen texture, then we + // render the custom shaders. + if (self.config.custom_shaders.items.len > 0 and + self.texture_screen != null) { // MTLRenderPassDescriptor const desc = desc: { @@ -764,15 +751,13 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { ); defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); - try self.drawPostShader(encoder, target.value); + try self.drawPostShader(encoder, screen_texture.value); } buffer.msgSend(void, objc.sel("presentDrawable:"), .{drawable.value}); buffer.msgSend(void, objc.sel("commit"), .{}); } -var post_time: f32 = 1; - fn drawPostShader( self: *Metal, encoder: objc.Object, @@ -789,7 +774,7 @@ fn drawPostShader( @floatFromInt(self.screen_size.?.height), 1, }, - .time = post_time, + .time = 1, .time_delta = 1, .frame_rate = 1, .frame = 1, @@ -800,7 +785,6 @@ fn drawPostShader( .sample_rate = 1, }}); defer buf.deinit(); - post_time += 1; // Use our custom shader pipeline encoder.msgSend( @@ -1264,6 +1248,45 @@ pub fn setScreenSize( self.cells.clearAndFree(self.alloc); self.cells_bg.clearAndFree(self.alloc); + // Setup our screen texture + const screen_texture: ?objc.Object = screen_texture: { + // If we have no custom shaders then we don't need a screen texture. + if (self.config.custom_shaders.items.len == 0) break :screen_texture null; + + // This texture is the size of our drawable but supports being a + // render target AND reading so that the custom shaders can read from it. + const desc = init: { + const Class = objc.getClass("MTLTextureDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm)); + desc.setProperty("width", @as(c_ulong, @intCast(dim.width))); + desc.setProperty("height", @as(c_ulong, @intCast(dim.height))); + desc.setProperty( + "usage", + @intFromEnum(mtl.MTLTextureUsage.render_target) | + @intFromEnum(mtl.MTLTextureUsage.shader_read) | + @intFromEnum(mtl.MTLTextureUsage.shader_write), + ); + + // If we fail to create the texture, then we just don't have a screen + // texture and our custom shaders won't run. + const id = self.device.msgSend( + ?*anyopaque, + objc.sel("newTextureWithDescriptor:"), + .{desc}, + ) orelse break :screen_texture null; + + break :screen_texture objc.Object.fromId(id); + }; + errdefer if (screen_texture) |tex| tex.msgSend(void, objc.sel("release"), .{}); + + // Set our new screen texture + if (self.texture_screen) |tex| tex.msgSend(void, objc.sel("release"), .{}); + self.texture_screen = screen_texture; + log.debug("screen size screen={} grid={}, cell={}", .{ dim, grid_size, self.cell_size }); } From 244e7266a1088daf16bc9aef961f57ff30e57de7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 17:26:25 -0800 Subject: [PATCH 25/55] renderer/metal: don't recreate custom shader resources per frame --- src/renderer/Metal.zig | 180 ++++++++++++++++++++++++----------------- 1 file changed, 107 insertions(+), 73 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index d146cdbfa..49ab67fc1 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -116,9 +116,21 @@ swapchain: objc.Object, // CAMetalLayer texture_greyscale: objc.Object, // MTLTexture texture_color: objc.Object, // MTLTexture -/// The screen texture. This is only set if we have custom shaders. If -/// we don't have custom shaders, we render directly to the drawable. -texture_screen: ?objc.Object, // MTLTexture +/// Custom shader state. This is only set if we have custom shaders. +custom_shader_state: ?CustomShaderState = null, + +pub const CustomShaderState = struct { + /// The screen texture that we render the terminal to. If we don't have + /// custom shaders, we render directly to the drawable. + screen_texture: objc.Object, // MTLTexture + sampler: mtl_sampler.Sampler, + uniforms: mtl_shaders.PostUniforms, + + pub fn deinit(self: *CustomShaderState) void { + deinitMTLResource(self.screen_texture); + self.sampler.deinit(); + } +}; /// The configuration for this renderer that is derived from the main /// configuration. This must be exported so that we don't need to @@ -288,9 +300,37 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { break :err &.{}; }; + // If we have custom shaders then setup our state + var custom_shader_state: ?CustomShaderState = state: { + if (custom_shaders.len == 0) break :state null; + + // Build our sampler for our texture + var sampler = try mtl_sampler.Sampler.init(device); + errdefer sampler.deinit(); + + break :state .{ + // Resolution and screen texture will be fixed up by first + // call to setScreenSize. This happens before any draw call. + .screen_texture = undefined, + .sampler = sampler, + .uniforms = .{ + .resolution = .{ 0, 0, 1 }, + .time = 1, + .time_delta = 1, + .frame_rate = 1, + .frame = 1, + .channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + .channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + .mouse = .{ 0, 0, 0, 0 }, + .date = .{ 0, 0, 0, 0 }, + .sample_rate = 1, + }, + }; + }; + errdefer if (custom_shader_state) |*state| state.deinit(); + // Initialize our shaders var shaders = try Shaders.init(alloc, device, custom_shaders); - //var shaders = try Shaders.init(alloc, device, &.{@embedFile("shaders/temp3.metal")}); errdefer shaders.deinit(alloc); // Font atlas textures @@ -336,7 +376,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .swapchain = swapchain, .texture_greyscale = texture_greyscale, .texture_color = texture_color, - .texture_screen = null, + .custom_shader_state = custom_shader_state, }; } @@ -360,9 +400,10 @@ pub fn deinit(self: *Metal) void { self.buf_instance.deinit(); deinitMTLResource(self.texture_greyscale); deinitMTLResource(self.texture_color); - if (self.texture_screen) |tex| deinitMTLResource(tex); self.queue.msgSend(void, objc.sel("release"), .{}); + if (self.custom_shader_state) |*state| state.deinit(); + self.shaders.deinit(self.alloc); self.* = undefined; @@ -621,7 +662,9 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // Get our screen texture. If we don't have a dedicated screen texture // then we just use the drawable texture. - const screen_texture = self.texture_screen orelse tex: { + const screen_texture = if (self.custom_shader_state) |state| + state.screen_texture + else tex: { const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); break :tex objc.Object.fromId(texture); }; @@ -703,9 +746,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // If we have custom shaders AND we have a screen texture, then we // render the custom shaders. - if (self.config.custom_shaders.items.len > 0 and - self.texture_screen != null) - { + if (self.custom_shader_state) |state| { // MTLRenderPassDescriptor const desc = desc: { const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?; @@ -751,7 +792,9 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { ); defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); - try self.drawPostShader(encoder, screen_texture.value); + for (self.shaders.post_pipelines) |pipeline| { + try self.drawPostShader(encoder, pipeline, &state); + } } buffer.msgSend(void, objc.sel("presentDrawable:"), .{drawable.value}); @@ -761,57 +804,42 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { fn drawPostShader( self: *Metal, encoder: objc.Object, - texture: objc.c.id, + pipeline: objc.Object, + state: *const CustomShaderState, ) !void { - // Build our sampler for our texture - var sampler = try mtl_sampler.Sampler.init(self.device); - defer sampler.deinit(); - - const Buffer = mtl_buffer.Buffer(mtl_shaders.PostUniforms); - var buf = try Buffer.initFill(self.device, &.{.{ - .resolution = .{ - @floatFromInt(self.screen_size.?.width), - @floatFromInt(self.screen_size.?.height), - 1, - }, - .time = 1, - .time_delta = 1, - .frame_rate = 1, - .frame = 1, - .channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, - .channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, - .mouse = .{ 0, 0, 0, 0 }, - .date = .{ 0, 0, 0, 0 }, - .sample_rate = 1, - }}); - defer buf.deinit(); + _ = self; // Use our custom shader pipeline encoder.msgSend( void, objc.sel("setRenderPipelineState:"), - .{self.shaders.post_pipelines[0].value}, + .{pipeline.value}, ); // Set our sampler encoder.msgSend( void, objc.sel("setFragmentSamplerState:atIndex:"), - .{ sampler.sampler.value, @as(c_ulong, 0) }, + .{ state.sampler.sampler.value, @as(c_ulong, 0) }, ); - // Set our buffer + // Set our uniforms encoder.msgSend( void, - objc.sel("setFragmentBuffer:offset:atIndex:"), - .{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, + objc.sel("setFragmentBytes:length:atIndex:"), + .{ + @as(*const anyopaque, @ptrCast(&state.uniforms)), + @as(c_ulong, @sizeOf(@TypeOf(state.uniforms))), + @as(c_ulong, 0), + }, ); + // Screen texture encoder.msgSend( void, objc.sel("setFragmentTexture:atIndex:"), .{ - texture, + state.screen_texture.value, @as(c_ulong, 0), }, ); @@ -1248,44 +1276,50 @@ pub fn setScreenSize( self.cells.clearAndFree(self.alloc); self.cells_bg.clearAndFree(self.alloc); - // Setup our screen texture - const screen_texture: ?objc.Object = screen_texture: { - // If we have no custom shaders then we don't need a screen texture. - if (self.config.custom_shaders.items.len == 0) break :screen_texture null; + // If we have custom shaders then we update the state + if (self.custom_shader_state) |*state| { + // Only free our previous texture if this isn't our first + // time setting the custom shader state. + if (state.uniforms.resolution[0] > 0) { + deinitMTLResource(state.screen_texture); + } - // This texture is the size of our drawable but supports being a - // render target AND reading so that the custom shaders can read from it. - const desc = init: { - const Class = objc.getClass("MTLTextureDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; + state.uniforms.resolution = .{ + @floatFromInt(dim.width), + @floatFromInt(dim.height), + 1, }; - desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm)); - desc.setProperty("width", @as(c_ulong, @intCast(dim.width))); - desc.setProperty("height", @as(c_ulong, @intCast(dim.height))); - desc.setProperty( - "usage", - @intFromEnum(mtl.MTLTextureUsage.render_target) | - @intFromEnum(mtl.MTLTextureUsage.shader_read) | - @intFromEnum(mtl.MTLTextureUsage.shader_write), - ); - // If we fail to create the texture, then we just don't have a screen - // texture and our custom shaders won't run. - const id = self.device.msgSend( - ?*anyopaque, - objc.sel("newTextureWithDescriptor:"), - .{desc}, - ) orelse break :screen_texture null; + state.screen_texture = screen_texture: { + // This texture is the size of our drawable but supports being a + // render target AND reading so that the custom shaders can read from it. + const desc = init: { + const Class = objc.getClass("MTLTextureDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm)); + desc.setProperty("width", @as(c_ulong, @intCast(dim.width))); + desc.setProperty("height", @as(c_ulong, @intCast(dim.height))); + desc.setProperty( + "usage", + @intFromEnum(mtl.MTLTextureUsage.render_target) | + @intFromEnum(mtl.MTLTextureUsage.shader_read) | + @intFromEnum(mtl.MTLTextureUsage.shader_write), + ); - break :screen_texture objc.Object.fromId(id); - }; - errdefer if (screen_texture) |tex| tex.msgSend(void, objc.sel("release"), .{}); + // If we fail to create the texture, then we just don't have a screen + // texture and our custom shaders won't run. + const id = self.device.msgSend( + ?*anyopaque, + objc.sel("newTextureWithDescriptor:"), + .{desc}, + ) orelse return error.MetalFailed; - // Set our new screen texture - if (self.texture_screen) |tex| tex.msgSend(void, objc.sel("release"), .{}); - self.texture_screen = screen_texture; + break :screen_texture objc.Object.fromId(id); + }; + } log.debug("screen size screen={} grid={}, cell={}", .{ dim, grid_size, self.cell_size }); } From 3a17ac48cf9b9a00ddf7e46b9b9452f68e584dc8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 17:29:29 -0800 Subject: [PATCH 26/55] prettier: ignore shaders --- .prettierignore | 3 + src/renderer/shaders/temp3.metal | 116 ------------------------------- src/temp.frag | 84 ---------------------- 3 files changed, 3 insertions(+), 200 deletions(-) delete mode 100644 src/renderer/shaders/temp3.metal delete mode 100644 src/temp.frag diff --git a/.prettierignore b/.prettierignore index 02fdc5b88..a0b692219 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,3 +10,6 @@ macos/ # website dev run website/.next + +# shaders +*.frag diff --git a/src/renderer/shaders/temp3.metal b/src/renderer/shaders/temp3.metal deleted file mode 100644 index b7a648608..000000000 --- a/src/renderer/shaders/temp3.metal +++ /dev/null @@ -1,116 +0,0 @@ -#pragma clang diagnostic ignored "-Wmissing-prototypes" - -#include -#include - -using namespace metal; - -// Implementation of the GLSL mod() function, which is slightly different than Metal fmod() -template -inline Tx mod(Tx x, Ty y) -{ - return x - y * floor(x / y); -} - -struct Globals -{ - float3 iResolution; - float iTime; - float iTimeDelta; - float iFrameRate; - int iFrame; - float4 iChannelTime[4]; - float3 iChannelResolution[4]; - float4 iMouse; - float4 iDate; - float iSampleRate; -}; - -struct main0_out -{ - float4 _fragColor [[color(0)]]; -}; - -static inline __attribute__((always_inline)) -float2 curve(thread float2& uv) -{ - uv = (uv - float2(0.5)) * 2.0; - uv *= 1.10000002384185791015625; - uv.x *= (1.0 + pow(abs(uv.y) / 5.0, 2.0)); - uv.y *= (1.0 + pow(abs(uv.x) / 4.0, 2.0)); - uv = (uv / float2(2.0)) + float2(0.5); - uv = (uv * 0.920000016689300537109375) + float2(0.039999999105930328369140625); - return uv; -} - -static inline __attribute__((always_inline)) -void mainImage(thread float4& fragColor, thread const float2& fragCoord, constant Globals& _89, texture2d iChannel0, sampler iChannel0Smplr) -{ - float2 q = fragCoord / float2(_89.iResolution[0], _89.iResolution[1]); - float2 uv = q; - float2 param = uv; - float2 _100 = curve(param); - uv = _100; - float3 oricol = iChannel0.sample(iChannel0Smplr, float2(q.x, q.y)).xyz; - float x = ((sin((0.300000011920928955078125 * _89.iTime) + (uv.y * 21.0)) * sin((0.699999988079071044921875 * _89.iTime) + (uv.y * 29.0))) * sin((0.300000011920928955078125 + (0.3300000131130218505859375 * _89.iTime)) + (uv.y * 31.0))) * 0.001700000022538006305694580078125; - float3 col; - col.x = iChannel0.sample(iChannel0Smplr, float2((x + uv.x) + 0.001000000047497451305389404296875, uv.y + 0.001000000047497451305389404296875)).x + 0.0500000007450580596923828125; - col.y = iChannel0.sample(iChannel0Smplr, float2((x + uv.x) + 0.0, uv.y - 0.00200000009499490261077880859375)).y + 0.0500000007450580596923828125; - col.z = iChannel0.sample(iChannel0Smplr, float2((x + uv.x) - 0.00200000009499490261077880859375, uv.y + 0.0)).z + 0.0500000007450580596923828125; - col.x += (0.07999999821186065673828125 * iChannel0.sample(iChannel0Smplr, ((float2(x + 0.02500000037252902984619140625, -0.02700000070035457611083984375) * 0.75) + float2(uv.x + 0.001000000047497451305389404296875, uv.y + 0.001000000047497451305389404296875))).x); - col.y += (0.0500000007450580596923828125 * iChannel0.sample(iChannel0Smplr, ((float2(x + (-0.02199999988079071044921875), -0.0199999995529651641845703125) * 0.75) + float2(uv.x + 0.0, uv.y - 0.00200000009499490261077880859375))).y); - col.z += (0.07999999821186065673828125 * iChannel0.sample(iChannel0Smplr, ((float2(x + (-0.0199999995529651641845703125), -0.017999999225139617919921875) * 0.75) + float2(uv.x - 0.00200000009499490261077880859375, uv.y + 0.0))).z); - col = fast::clamp((col * 0.60000002384185791015625) + (((col * 0.4000000059604644775390625) * col) * 1.0), float3(0.0), float3(1.0)); - float vig = 0.0 + ((((16.0 * uv.x) * uv.y) * (1.0 - uv.x)) * (1.0 - uv.y)); - col *= float3(pow(vig, 0.300000011920928955078125)); - col *= float3(0.949999988079071044921875, 1.0499999523162841796875, 0.949999988079071044921875); - col *= 2.7999999523162841796875; - float scans = fast::clamp(0.3499999940395355224609375 + (0.3499999940395355224609375 * sin((3.5 * _89.iTime) + ((uv.y * _89.iResolution[1u]) * 1.5))), 0.0, 1.0); - float s = pow(scans, 1.7000000476837158203125); - col *= float3(0.4000000059604644775390625 + (0.699999988079071044921875 * s)); - col *= (1.0 + (0.00999999977648258209228515625 * sin(110.0 * _89.iTime))); - bool _352 = uv.x < 0.0; - bool _359; - if (!_352) - { - _359 = uv.x > 1.0; - } - else - { - _359 = _352; - } - if (_359) - { - col *= 0.0; - } - bool _366 = uv.y < 0.0; - bool _373; - if (!_366) - { - _373 = uv.y > 1.0; - } - else - { - _373 = _366; - } - if (_373) - { - col *= 0.0; - } - col *= (float3(1.0) - (float3(fast::clamp((mod(fragCoord.x, 2.0) - 1.0) * 2.0, 0.0, 1.0)) * 0.64999997615814208984375)); - float comp = smoothstep(0.100000001490116119384765625, 0.89999997615814208984375, sin(_89.iTime)); - fragColor = float4(col, 1.0); -} - -fragment main0_out main0(constant Globals& _89 [[buffer(0)]], texture2d iChannel0 [[texture(0)]], sampler iChannel0Smplr [[sampler(0)]], float4 gl_FragCoord [[position]]) -{ - constexpr sampler iChannel0Smplr(address::clamp_to_edge, filter::linear); - - main0_out out = {}; - float2 param_1 = gl_FragCoord.xy; - float4 param; - mainImage(param, param_1, _89, iChannel0, iChannel0Smplr); - out._fragColor = param; - return out; -} - diff --git a/src/temp.frag b/src/temp.frag deleted file mode 100644 index 4c331118c..000000000 --- a/src/temp.frag +++ /dev/null @@ -1,84 +0,0 @@ -#version 430 core - -layout(binding = 0) uniform Globals { - uniform vec3 iResolution; - uniform float iTime; - uniform float iTimeDelta; - uniform float iFrameRate; - uniform int iFrame; - uniform float iChannelTime[4]; - uniform vec3 iChannelResolution[4]; - uniform vec4 iMouse; - uniform vec4 iDate; - uniform float iSampleRate; -}; - -layout(binding = 0) uniform sampler2D iChannel0; -layout(binding = 1) uniform sampler2D iChannel1; -layout(binding = 2) uniform sampler2D iChannel2; -layout(binding = 3) uniform sampler2D iChannel3; - -layout(location = 0) in vec4 gl_FragCoord; -layout(location = 0) out vec4 _fragColor; - -#define texture2D texture - -void mainImage( out vec4 fragColor, in vec2 fragCoord ); -void main() { mainImage (_fragColor, gl_FragCoord.xy); } - -// Loosely based on postprocessing shader by inigo quilez, License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. - -vec2 curve(vec2 uv) -{ - uv = (uv - 0.5) * 2.0; - uv *= 1.1; - uv.x *= 1.0 + pow((abs(uv.y) / 5.0), 2.0); - uv.y *= 1.0 + pow((abs(uv.x) / 4.0), 2.0); - uv = (uv / 2.0) + 0.5; - uv = uv *0.92 + 0.04; - return uv; -} -void mainImage( out vec4 fragColor, in vec2 fragCoord ) -{ - vec2 q = fragCoord.xy / iResolution.xy; - vec2 uv = q; - uv = curve( uv ); - vec3 oricol = texture( iChannel0, vec2(q.x,q.y) ).xyz; - vec3 col; - float x = sin(0.3*iTime+uv.y*21.0)*sin(0.7*iTime+uv.y*29.0)*sin(0.3+0.33*iTime+uv.y*31.0)*0.0017; - - col.r = texture(iChannel0,vec2(x+uv.x+0.001,uv.y+0.001)).x+0.05; - col.g = texture(iChannel0,vec2(x+uv.x+0.000,uv.y-0.002)).y+0.05; - col.b = texture(iChannel0,vec2(x+uv.x-0.002,uv.y+0.000)).z+0.05; - col.r += 0.08*texture(iChannel0,0.75*vec2(x+0.025, -0.027)+vec2(uv.x+0.001,uv.y+0.001)).x; - col.g += 0.05*texture(iChannel0,0.75*vec2(x+-0.022, -0.02)+vec2(uv.x+0.000,uv.y-0.002)).y; - col.b += 0.08*texture(iChannel0,0.75*vec2(x+-0.02, -0.018)+vec2(uv.x-0.002,uv.y+0.000)).z; - - col = clamp(col*0.6+0.4*col*col*1.0,0.0,1.0); - - float vig = (0.0 + 1.0*16.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y)); - col *= vec3(pow(vig,0.3)); - - col *= vec3(0.95,1.05,0.95); - col *= 2.8; - - float scans = clamp( 0.35+0.35*sin(3.5*iTime+uv.y*iResolution.y*1.5), 0.0, 1.0); - - float s = pow(scans,1.7); - col = col*vec3( 0.4+0.7*s) ; - - col *= 1.0+0.01*sin(110.0*iTime); - if (uv.x < 0.0 || uv.x > 1.0) - col *= 0.0; - if (uv.y < 0.0 || uv.y > 1.0) - col *= 0.0; - - col*=1.0-0.65*vec3(clamp((mod(fragCoord.x, 2.0)-1.0)*2.0,0.0,1.0)); - - float comp = smoothstep( 0.1, 0.9, sin(iTime) ); - - // Remove the next line to stop cross-fade between original and postprocess -// col = mix( col, oricol, comp ); - - fragColor = vec4(col,1.0); -} From 17de73f802c0a81d716195b5bc28b52637ad2b1b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 17:45:08 -0800 Subject: [PATCH 27/55] renderer: log shadertoy compile errors --- src/renderer/shadertoy.zig | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 1d5610c7a..de6c0b419 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -65,7 +65,19 @@ pub fn loadFromFile( // Convert to SPIR-V const spirv: []const u8 = spirv: { var list = std.ArrayList(u8).init(alloc); - try spirvFromGlsl(list.writer(), null, glsl); + var errlog: SpirvLog = .{ .alloc = alloc }; + defer errlog.deinit(); + spirvFromGlsl(list.writer(), &errlog, glsl) catch |err| { + if (errlog.info.len > 0 or errlog.debug.len > 0) { + log.warn("spirv error path={s} info={s} debug={s}", .{ + path, + errlog.info, + errlog.debug, + }); + } + + return err; + }; break :spirv list.items; }; From 4742cd308d0c2895048f34f0dda13fd5dd90aeab Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 19:22:48 -0800 Subject: [PATCH 28/55] renderer: animation timer if we have custom shaders --- src/renderer/Metal.zig | 17 ++++++ src/renderer/Thread.zig | 106 ++++++++++++++++++++++++++------- src/renderer/metal/shaders.zig | 3 + 3 files changed, 105 insertions(+), 21 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 49ab67fc1..e26019a9a 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -125,6 +125,7 @@ pub const CustomShaderState = struct { screen_texture: objc.Object, // MTLTexture sampler: mtl_sampler.Sampler, uniforms: mtl_shaders.PostUniforms, + last_frame_time: std.time.Instant, pub fn deinit(self: *CustomShaderState) void { deinitMTLResource(self.screen_texture); @@ -325,6 +326,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .date = .{ 0, 0, 0, 0 }, .sample_rate = 1, }, + + .last_frame_time = try std.time.Instant.now(), }; }; errdefer if (custom_shader_state) |*state| state.deinit(); @@ -465,6 +468,12 @@ pub fn threadExit(self: *const Metal) void { // Metal requires no per-thread state. } +/// True if our renderer has animations so that a higher frequency +/// timer is used. +pub fn hasAnimations(self: *const Metal) bool { + return self.custom_shader_state != null; +} + /// Returns the grid size for a given screen size. This is safe to call /// on any thread. fn gridSize(self: *Metal) ?renderer.GridSize { @@ -653,6 +662,14 @@ pub fn updateFrame( pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { _ = surface; + // If we have custom shaders, update the animation time. + if (self.custom_shader_state) |*state| { + const now = std.time.Instant.now() catch state.last_frame_time; + const since_ns: f32 = @floatFromInt(now.since(state.last_frame_time)); + state.uniforms.time = since_ns / std.time.ns_per_s; + state.uniforms.time_delta = since_ns / std.time.ns_per_s; + } + // @autoreleasepool {} const pool = objc.AutoreleasePool.init(); defer pool.deinit(); diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 926bd8e42..e6d0809a5 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -15,6 +15,7 @@ const App = @import("../App.zig"); const Allocator = std.mem.Allocator; const log = std.log.scoped(.renderer_thread); +const DRAW_INTERVAL = 33; // 30 FPS const CURSOR_BLINK_INTERVAL = 600; /// The type used for sending messages to the IO thread. For now this is @@ -43,6 +44,13 @@ stop_c: xev.Completion = .{}, render_h: xev.Timer, render_c: xev.Completion = .{}, +/// The timer used for draw calls. Draw calls don't update from the +/// terminal state so they're much cheaper. They're used for animation +/// and are paused when the terminal is not focused. +draw_h: xev.Timer, +draw_c: xev.Completion = .{}, +draw_active: bool = false, + /// The timer used for cursor blinking cursor_h: xev.Timer, cursor_c: xev.Completion = .{}, @@ -100,6 +108,10 @@ pub fn init( var render_h = try xev.Timer.init(); errdefer render_h.deinit(); + // Draw timer, see comments. + var draw_h = try xev.Timer.init(); + errdefer draw_h.deinit(); + // Setup a timer for blinking the cursor var cursor_timer = try xev.Timer.init(); errdefer cursor_timer.deinit(); @@ -114,6 +126,7 @@ pub fn init( .wakeup = wakeup_h, .stop = stop_h, .render_h = render_h, + .draw_h = draw_h, .cursor_h = cursor_timer, .surface = surface, .renderer = renderer_impl, @@ -129,6 +142,7 @@ pub fn deinit(self: *Thread) void { self.stop.deinit(); self.wakeup.deinit(); self.render_h.deinit(); + self.draw_h.deinit(); self.cursor_h.deinit(); self.loop.deinit(); @@ -172,27 +186,8 @@ fn threadMain_(self: *Thread) !void { cursorTimerCallback, ); - // If we are using tracy, then we setup a prepare handle so that - // we can mark the frame. - // TODO - // var frame_h: libuv.Prepare = if (!tracy.enabled) undefined else frame_h: { - // const alloc_ptr = self.loop.getData(Allocator).?; - // const alloc = alloc_ptr.*; - // const h = try libuv.Prepare.init(alloc, self.loop); - // h.setData(self); - // try h.start(prepFrameCallback); - // - // break :frame_h h; - // }; - // defer if (tracy.enabled) { - // frame_h.close((struct { - // fn callback(h: *libuv.Prepare) void { - // const alloc_h = h.loop().getData(Allocator).?.*; - // h.deinit(alloc_h); - // } - // }).callback); - // _ = self.loop.run(.nowait) catch {}; - // }; + // Start the draw timer + self.startDrawTimer(); // Run log.debug("starting renderer thread", .{}); @@ -200,6 +195,34 @@ fn threadMain_(self: *Thread) !void { _ = try self.loop.run(.until_done); } +fn startDrawTimer(self: *Thread) void { + // If our renderer doesn't suppoort animations then we never run this. + if (!@hasDecl(renderer.Renderer, "hasAnimations")) return; + if (!self.renderer.hasAnimations()) return; + + // Set our active state so it knows we're running. We set this before + // even checking the active state in case we have a pending shutdown. + self.draw_active = true; + + // If our draw timer is already active, then we don't have to do anything. + if (self.draw_c.state() == .active) return; + + // Start the timer which loops + self.draw_h.run( + &self.loop, + &self.draw_c, + DRAW_INTERVAL, + Thread, + self, + drawCallback, + ); +} + +fn stopDrawTimer(self: *Thread) void { + // This will stop the draw on the next iteration. + self.draw_active = false; +} + /// Drain the mailbox. fn drainMailbox(self: *Thread) !void { const zone = trace(@src()); @@ -213,6 +236,9 @@ fn drainMailbox(self: *Thread) !void { try self.renderer.setFocus(v); if (!v) { + // Stop the draw timer + self.stopDrawTimer(); + // If we're not focused, then we stop the cursor blink if (self.cursor_c.state() == .active and self.cursor_c_cancel.state() == .dead) @@ -227,6 +253,9 @@ fn drainMailbox(self: *Thread) !void { ); } } else { + // Start the draw timer + self.startDrawTimer(); + // If we're focused, we immediately show the cursor again // and then restart the timer. if (self.cursor_c.state() != .active) { @@ -325,6 +354,41 @@ fn wakeupCallback( return .rearm; } +fn drawCallback( + self_: ?*Thread, + _: *xev.Loop, + _: *xev.Completion, + r: xev.Timer.RunError!void, +) xev.CallbackAction { + _ = r catch unreachable; + const t = self_ orelse { + // This shouldn't happen so we log it. + log.warn("render callback fired without data set", .{}); + return .disarm; + }; + + // If we're doing single-threaded GPU calls then we just 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 = {} }, + ); + } else { + t.renderer.drawFrame(t.surface) catch |err| + log.warn("error drawing err={}", .{err}); + } + + // Only continue if we're still active + if (t.draw_active) { + t.draw_h.run(&t.loop, &t.draw_c, DRAW_INTERVAL, Thread, t, drawCallback); + } + + return .disarm; +} + fn renderCallback( self_: ?*Thread, _: *xev.Loop, diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 4edb7eb90..32f775a50 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -129,6 +129,9 @@ pub const Uniforms = extern struct { /// The uniforms used for custom postprocess shaders. pub const PostUniforms = extern struct { + // Note: all of the explicit aligmnments are copied from the + // MSL developer reference just so that we can be sure that we got + // it all exactly right. resolution: [3]f32 align(16), time: f32 align(4), time_delta: f32 align(4), From 76a88e3fbe218459dcc4f194e29d95a0b09753a3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 21:03:32 -0800 Subject: [PATCH 29/55] renderer: spirv binary must be aligned to u32 --- src/renderer/shadertoy.zig | 49 ++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index de6c0b419..45c225d85 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -64,7 +64,9 @@ pub fn loadFromFile( // Convert to SPIR-V const spirv: []const u8 = spirv: { - var list = std.ArrayList(u8).init(alloc); + // SpirV pointer must be aligned to 4 bytes since we expect + // a slice of words. + var list = std.ArrayListAligned(u8, @alignOf(u32)).init(alloc); var errlog: SpirvLog = .{ .alloc = alloc }; defer errlog.deinit(); spirvFromGlsl(list.writer(), &errlog, glsl) catch |err| { @@ -190,6 +192,31 @@ pub const SpirvLog = struct { /// Convert SPIR-V binary to MSL. pub fn mslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 { + return try spvCross(alloc, spvcross.c.SPVC_BACKEND_MSL, spv, null); +} + +/// Convert SPIR-V binary to GLSL.. +pub fn glslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 { + const c = spvcross.c; + return try spvCross(alloc, c.SPVC_BACKEND_GLSL, spv, (struct { + fn setOptions(options: c.spvc_compiler_options) error{SpvcFailed}!void { + if (c.spvc_compiler_options_set_uint( + options, + c.SPVC_COMPILER_OPTION_GLSL_VERSION, + 330, + ) != c.SPVC_SUCCESS) { + return error.SpvcFailed; + } + } + }).setOptions); +} + +fn spvCross( + alloc: Allocator, + backend: spvcross.c.spvc_backend, + spv: []const u8, + comptime optionsFn_: ?*const fn (c: spvcross.c.spvc_compiler_options) error{SpvcFailed}!void, +) ![:0]const u8 { // Spir-V is always a multiple of 4 because it is written as a series of words if (@mod(spv.len, 4) != 0) return error.SpirvInvalid; @@ -219,11 +246,11 @@ pub fn mslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 { return error.SpvcFailed; } - // Build our compiler to MSL + // Build our compiler to GLSL var compiler: c.spvc_compiler = undefined; if (c.spvc_context_create_compiler( ctx, - c.SPVC_BACKEND_MSL, + backend, ir, c.SPVC_CAPTURE_MODE_TAKE_OWNERSHIP, &compiler, @@ -231,6 +258,20 @@ pub fn mslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 { return error.SpvcFailed; } + // Setup our options if we have any + if (optionsFn_) |optionsFn| { + var options: c.spvc_compiler_options = undefined; + if (c.spvc_compiler_create_compiler_options(compiler, &options) != c.SPVC_SUCCESS) { + return error.SpvcFailed; + } + + try optionsFn(options); + + if (c.spvc_compiler_install_compiler_options(compiler, options) != c.SPVC_SUCCESS) { + return error.SpvcFailed; + } + } + // Compile the resulting string. This string pointer is owned by the // context so we don't need to free it. var result: [*:0]const u8 = undefined; @@ -286,7 +327,7 @@ test "shadertoy to msl" { const src = try testGlslZ(alloc, test_crt); defer alloc.free(src); - var spvlist = std.ArrayList(u8).init(alloc); + var spvlist = std.ArrayListAligned(u8, @alignOf(u32)).init(alloc); defer spvlist.deinit(); try spirvFromGlsl(spvlist.writer(), null, src); From 8576acb89ec926ef2a61e609ec6325befbd1bda7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 21:13:55 -0800 Subject: [PATCH 30/55] renderer/opengl: move opengl API to pkg/opengl --- build.zig | 2 + build.zig.zon | 1 + pkg/opengl/Buffer.zig | 218 +++++++++++++++++++++++++++++++++++++ pkg/opengl/Program.zig | 128 ++++++++++++++++++++++ pkg/opengl/Shader.zig | 56 ++++++++++ pkg/opengl/Texture.zig | 163 +++++++++++++++++++++++++++ pkg/opengl/VertexArray.zig | 29 +++++ pkg/opengl/build.zig | 5 + pkg/opengl/c.zig | 3 + pkg/opengl/draw.zig | 59 ++++++++++ pkg/opengl/errors.zig | 33 ++++++ pkg/opengl/extensions.zig | 32 ++++++ pkg/opengl/glad.zig | 45 ++++++++ pkg/opengl/main.zig | 23 ++++ src/renderer/OpenGL.zig | 27 +++-- src/renderer/shadertoy.zig | 17 +++ 16 files changed, 833 insertions(+), 8 deletions(-) create mode 100644 pkg/opengl/Buffer.zig create mode 100644 pkg/opengl/Program.zig create mode 100644 pkg/opengl/Shader.zig create mode 100644 pkg/opengl/Texture.zig create mode 100644 pkg/opengl/VertexArray.zig create mode 100644 pkg/opengl/build.zig create mode 100644 pkg/opengl/c.zig create mode 100644 pkg/opengl/draw.zig create mode 100644 pkg/opengl/errors.zig create mode 100644 pkg/opengl/extensions.zig create mode 100644 pkg/opengl/glad.zig create mode 100644 pkg/opengl/main.zig diff --git a/build.zig b/build.zig index 6146f93f5..cb60aabb7 100644 --- a/build.zig +++ b/build.zig @@ -663,6 +663,7 @@ fn addDeps( .target = step.target, .optimize = step.optimize, }); + const opengl_dep = b.dependency("opengl", .{}); const pixman_dep = b.dependency("pixman", .{ .target = step.target, .optimize = step.optimize, @@ -730,6 +731,7 @@ fn addDeps( step.addModule("spirv_cross", spirv_cross_dep.module("spirv_cross")); step.addModule("harfbuzz", harfbuzz_dep.module("harfbuzz")); step.addModule("xev", libxev_dep.module("xev")); + step.addModule("opengl", opengl_dep.module("opengl")); step.addModule("pixman", pixman_dep.module("pixman")); step.addModule("ziglyph", ziglyph_dep.module("ziglyph")); diff --git a/build.zig.zon b/build.zig.zon index 580c8593e..309df74d1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -32,6 +32,7 @@ .harfbuzz = .{ .path = "./pkg/harfbuzz" }, .libpng = .{ .path = "./pkg/libpng" }, .macos = .{ .path = "./pkg/macos" }, + .opengl = .{ .path = "./pkg/opengl" }, .pixman = .{ .path = "./pkg/pixman" }, .tracy = .{ .path = "./pkg/tracy" }, .zlib = .{ .path = "./pkg/zlib" }, diff --git a/pkg/opengl/Buffer.zig b/pkg/opengl/Buffer.zig new file mode 100644 index 000000000..b794ca4f0 --- /dev/null +++ b/pkg/opengl/Buffer.zig @@ -0,0 +1,218 @@ +const Buffer = @This(); + +const std = @import("std"); +const c = @import("c.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); + +id: c.GLuint, + +/// Enum for possible binding targets. +pub const Target = enum(c_uint) { + ArrayBuffer = c.GL_ARRAY_BUFFER, + ElementArrayBuffer = c.GL_ELEMENT_ARRAY_BUFFER, + _, +}; + +/// Enum for possible buffer usages. +pub const Usage = enum(c_uint) { + StreamDraw = c.GL_STREAM_DRAW, + StreamRead = c.GL_STREAM_READ, + StreamCopy = c.GL_STREAM_COPY, + StaticDraw = c.GL_STATIC_DRAW, + StaticRead = c.GL_STATIC_READ, + StaticCopy = c.GL_STATIC_COPY, + DynamicDraw = c.GL_DYNAMIC_DRAW, + DynamicRead = c.GL_DYNAMIC_READ, + DynamicCopy = c.GL_DYNAMIC_COPY, + _, +}; + +/// Binding is a bound buffer. By using this for functions that operate +/// on bound buffers, you can easily defer unbinding and in safety-enabled +/// modes verify that unbound buffers are never accessed. +pub const Binding = struct { + target: Target, + + /// Sets the data of this bound buffer. The data can be any array-like + /// type. The size of the data is automatically determined based on the type. + pub inline fn setData( + b: Binding, + data: anytype, + usage: Usage, + ) !void { + const info = dataInfo(&data); + glad.context.BufferData.?(@intFromEnum(b.target), info.size, info.ptr, @intFromEnum(usage)); + try errors.getError(); + } + + /// Sets the data of this bound buffer. The data can be any array-like + /// type. The size of the data is automatically determined based on the type. + pub inline fn setSubData( + b: Binding, + offset: usize, + data: anytype, + ) !void { + const info = dataInfo(data); + glad.context.BufferSubData.?(@intFromEnum(b.target), @intCast(offset), info.size, info.ptr); + try errors.getError(); + } + + /// Sets the buffer data with a null buffer that is expected to be + /// filled in the future using subData. This requires the type just so + /// we can setup the data size. + pub inline fn setDataNull( + b: Binding, + comptime T: type, + usage: Usage, + ) !void { + glad.context.BufferData.?(@intFromEnum(b.target), @sizeOf(T), null, @intFromEnum(usage)); + try errors.getError(); + } + + /// Same as setDataNull but lets you manually specify the buffer size. + pub inline fn setDataNullManual( + b: Binding, + size: usize, + usage: Usage, + ) !void { + glad.context.BufferData.?(@intFromEnum(b.target), @intCast(size), null, @intFromEnum(usage)); + try errors.getError(); + } + + fn dataInfo(data: anytype) struct { + size: isize, + ptr: *const anyopaque, + } { + return switch (@typeInfo(@TypeOf(data))) { + .Pointer => |ptr| switch (ptr.size) { + .One => .{ + .size = @sizeOf(ptr.child) * data.len, + .ptr = data, + }, + .Slice => .{ + .size = @intCast(@sizeOf(ptr.child) * data.len), + .ptr = data.ptr, + }, + else => { + std.log.err("invalid buffer data pointer size: {}", .{ptr.size}); + unreachable; + }, + }, + else => { + std.log.err("invalid buffer data type: {s}", .{@tagName(@typeInfo(@TypeOf(data)))}); + unreachable; + }, + }; + } + + pub inline fn enableAttribArray(_: Binding, idx: c.GLuint) !void { + glad.context.EnableVertexAttribArray.?(idx); + } + + /// Shorthand for vertexAttribPointer that is specialized towards the + /// common use case of specifying an array of homogeneous types that + /// don't need normalization. This also enables the attribute at idx. + pub fn attribute( + b: Binding, + idx: c.GLuint, + size: c.GLint, + comptime T: type, + offset: usize, + ) !void { + const info: struct { + // Type of the each component in the array. + typ: c.GLenum, + + // The byte offset between each full set of attributes. + stride: c.GLsizei, + + // The size of each component used in calculating the offset. + offset: usize, + } = switch (@typeInfo(T)) { + .Array => |ary| .{ + .typ = switch (ary.child) { + f32 => c.GL_FLOAT, + else => @compileError("unsupported array child type"), + }, + .offset = @sizeOf(ary.child), + .stride = @sizeOf(T), + }, + else => @compileError("unsupported type"), + }; + + try b.attributeAdvanced( + idx, + size, + info.typ, + false, + info.stride, + offset * info.offset, + ); + try b.enableAttribArray(idx); + } + + /// VertexAttribDivisor + pub fn attributeDivisor(_: Binding, idx: c.GLuint, divisor: c.GLuint) !void { + glad.context.VertexAttribDivisor.?(idx, divisor); + try errors.getError(); + } + + pub inline fn attributeAdvanced( + _: Binding, + idx: c.GLuint, + size: c.GLint, + typ: c.GLenum, + normalized: bool, + stride: c.GLsizei, + offset: usize, + ) !void { + const normalized_c: c.GLboolean = if (normalized) c.GL_TRUE else c.GL_FALSE; + const offsetPtr = if (offset > 0) + @as(*const anyopaque, @ptrFromInt(offset)) + else + null; + + glad.context.VertexAttribPointer.?(idx, size, typ, normalized_c, stride, offsetPtr); + try errors.getError(); + } + + pub inline fn attributeIAdvanced( + _: Binding, + idx: c.GLuint, + size: c.GLint, + typ: c.GLenum, + stride: c.GLsizei, + offset: usize, + ) !void { + const offsetPtr = if (offset > 0) + @as(*const anyopaque, @ptrFromInt(offset)) + else + null; + + glad.context.VertexAttribIPointer.?(idx, size, typ, stride, offsetPtr); + try errors.getError(); + } + + pub inline fn unbind(b: *Binding) void { + glad.context.BindBuffer.?(@intFromEnum(b.target), 0); + b.* = undefined; + } +}; + +/// Create a single buffer. +pub inline fn create() !Buffer { + var vbo: c.GLuint = undefined; + glad.context.GenBuffers.?(1, &vbo); + return Buffer{ .id = vbo }; +} + +/// glBindBuffer +pub inline fn bind(v: Buffer, target: Target) !Binding { + glad.context.BindBuffer.?(@intFromEnum(target), v.id); + return Binding{ .target = target }; +} + +pub inline fn destroy(v: Buffer) void { + glad.context.DeleteBuffers.?(1, &v.id); +} diff --git a/pkg/opengl/Program.zig b/pkg/opengl/Program.zig new file mode 100644 index 000000000..d266bd226 --- /dev/null +++ b/pkg/opengl/Program.zig @@ -0,0 +1,128 @@ +const Program = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const log = std.log.scoped(.opengl); + +const c = @import("c.zig"); +const Shader = @import("Shader.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); + +id: c.GLuint, + +const Binding = struct { + pub inline fn unbind(_: Binding) void { + glad.context.UseProgram.?(0); + } +}; + +pub inline fn create() !Program { + const id = glad.context.CreateProgram.?(); + if (id == 0) try errors.mustError(); + + log.debug("program created id={}", .{id}); + return Program{ .id = id }; +} + +/// Create a program from a vertex and fragment shader source. This will +/// compile and link the vertex and fragment shader. +pub inline fn createVF(vsrc: [:0]const u8, fsrc: [:0]const u8) !Program { + const vs = try Shader.create(c.GL_VERTEX_SHADER); + try vs.setSourceAndCompile(vsrc); + defer vs.destroy(); + + const fs = try Shader.create(c.GL_FRAGMENT_SHADER); + try fs.setSourceAndCompile(fsrc); + defer fs.destroy(); + + const p = try create(); + try p.attachShader(vs); + try p.attachShader(fs); + try p.link(); + + return p; +} + +pub inline fn attachShader(p: Program, s: Shader) !void { + glad.context.AttachShader.?(p.id, s.id); + try errors.getError(); +} + +pub inline fn link(p: Program) !void { + glad.context.LinkProgram.?(p.id); + + // Check if linking succeeded + var success: c_int = undefined; + glad.context.GetProgramiv.?(p.id, c.GL_LINK_STATUS, &success); + if (success == c.GL_TRUE) { + log.debug("program linked id={}", .{p.id}); + return; + } + + log.err("program link failure id={} message={s}", .{ + p.id, + std.mem.sliceTo(&p.getInfoLog(), 0), + }); + return error.CompileFailed; +} + +pub inline fn use(p: Program) !Binding { + glad.context.UseProgram.?(p.id); + try errors.getError(); + return Binding{}; +} + +/// Requires the program is currently in use. +pub inline fn setUniform( + p: Program, + n: [:0]const u8, + value: anytype, +) !void { + const loc = glad.context.GetUniformLocation.?( + p.id, + @ptrCast(n.ptr), + ); + if (loc < 0) { + return error.UniformNameInvalid; + } + try errors.getError(); + + // Perform the correct call depending on the type of the value. + switch (@TypeOf(value)) { + comptime_int => glad.context.Uniform1i.?(loc, value), + f32 => glad.context.Uniform1f.?(loc, value), + @Vector(2, f32) => glad.context.Uniform2f.?(loc, value[0], value[1]), + @Vector(3, f32) => glad.context.Uniform3f.?(loc, value[0], value[1], value[2]), + @Vector(4, f32) => glad.context.Uniform4f.?(loc, value[0], value[1], value[2], value[3]), + [4]@Vector(4, f32) => glad.context.UniformMatrix4fv.?( + loc, + 1, + c.GL_FALSE, + @ptrCast(&value), + ), + else => { + log.warn("unsupported uniform type {}", .{@TypeOf(value)}); + unreachable; + }, + } + try errors.getError(); +} + +/// getInfoLog returns the info log for this program. This attempts to +/// keep the log fully stack allocated and is therefore limited to a max +/// amount of elements. +// +// NOTE(mitchellh): we can add a dynamic version that uses an allocator +// if we ever need it. +pub inline fn getInfoLog(s: Program) [512]u8 { + var msg: [512]u8 = undefined; + glad.context.GetProgramInfoLog.?(s.id, msg.len, null, &msg); + return msg; +} + +pub inline fn destroy(p: Program) void { + assert(p.id != 0); + glad.context.DeleteProgram.?(p.id); + log.debug("program destroyed id={}", .{p.id}); +} diff --git a/pkg/opengl/Shader.zig b/pkg/opengl/Shader.zig new file mode 100644 index 000000000..beaae9e94 --- /dev/null +++ b/pkg/opengl/Shader.zig @@ -0,0 +1,56 @@ +const Shader = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const log = std.log.scoped(.opengl); + +const c = @import("c.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); + +id: c.GLuint, + +pub inline fn create(typ: c.GLenum) errors.Error!Shader { + const id = glad.context.CreateShader.?(typ); + if (id == 0) { + try errors.mustError(); + unreachable; + } + + log.debug("shader created id={}", .{id}); + return Shader{ .id = id }; +} + +/// Set the source and compile a shader. +pub inline fn setSourceAndCompile(s: Shader, source: [:0]const u8) !void { + glad.context.ShaderSource.?(s.id, 1, &@as([*c]const u8, @ptrCast(source)), null); + glad.context.CompileShader.?(s.id); + + // Check if compilation succeeded + var success: c_int = undefined; + glad.context.GetShaderiv.?(s.id, c.GL_COMPILE_STATUS, &success); + if (success == c.GL_TRUE) return; + log.err("shader compilation failure id={} message={s}", .{ + s.id, + std.mem.sliceTo(&s.getInfoLog(), 0), + }); + return error.CompileFailed; +} + +/// getInfoLog returns the info log for this shader. This attempts to +/// keep the log fully stack allocated and is therefore limited to a max +/// amount of elements. +// +// NOTE(mitchellh): we can add a dynamic version that uses an allocator +// if we ever need it. +pub inline fn getInfoLog(s: Shader) [512]u8 { + var msg: [512]u8 = undefined; + glad.context.GetShaderInfoLog.?(s.id, msg.len, null, &msg); + return msg; +} + +pub inline fn destroy(s: Shader) void { + assert(s.id != 0); + glad.context.DeleteShader.?(s.id); + log.debug("shader destroyed id={}", .{s.id}); +} diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig new file mode 100644 index 000000000..91a65b565 --- /dev/null +++ b/pkg/opengl/Texture.zig @@ -0,0 +1,163 @@ +const Texture = @This(); + +const std = @import("std"); +const c = @import("c.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); + +id: c.GLuint, + +pub inline fn active(target: c.GLenum) !void { + glad.context.ActiveTexture.?(target); + try errors.getError(); +} + +/// Enun for possible texture binding targets. +pub const Target = enum(c_uint) { + @"1D" = c.GL_TEXTURE_1D, + @"2D" = c.GL_TEXTURE_2D, + @"3D" = c.GL_TEXTURE_3D, + @"1DArray" = c.GL_TEXTURE_1D_ARRAY, + @"2DArray" = c.GL_TEXTURE_2D_ARRAY, + Rectangle = c.GL_TEXTURE_RECTANGLE, + CubeMap = c.GL_TEXTURE_CUBE_MAP, + Buffer = c.GL_TEXTURE_BUFFER, + @"2DMultisample" = c.GL_TEXTURE_2D_MULTISAMPLE, + @"2DMultisampleArray" = c.GL_TEXTURE_2D_MULTISAMPLE_ARRAY, +}; + +/// Enum for possible texture parameters. +pub const Parameter = enum(c_uint) { + BaseLevel = c.GL_TEXTURE_BASE_LEVEL, + CompareFunc = c.GL_TEXTURE_COMPARE_FUNC, + CompareMode = c.GL_TEXTURE_COMPARE_MODE, + LodBias = c.GL_TEXTURE_LOD_BIAS, + MinFilter = c.GL_TEXTURE_MIN_FILTER, + MagFilter = c.GL_TEXTURE_MAG_FILTER, + MinLod = c.GL_TEXTURE_MIN_LOD, + MaxLod = c.GL_TEXTURE_MAX_LOD, + MaxLevel = c.GL_TEXTURE_MAX_LEVEL, + SwizzleR = c.GL_TEXTURE_SWIZZLE_R, + SwizzleG = c.GL_TEXTURE_SWIZZLE_G, + SwizzleB = c.GL_TEXTURE_SWIZZLE_B, + SwizzleA = c.GL_TEXTURE_SWIZZLE_A, + WrapS = c.GL_TEXTURE_WRAP_S, + WrapT = c.GL_TEXTURE_WRAP_T, + WrapR = c.GL_TEXTURE_WRAP_R, +}; + +/// Internal format enum for texture images. +pub const InternalFormat = enum(c_int) { + Red = c.GL_RED, + RGBA = c.GL_RGBA, + + // There are so many more that I haven't filled in. + _, +}; + +/// Format for texture images +pub const Format = enum(c_uint) { + Red = c.GL_RED, + BGRA = c.GL_BGRA, + + // There are so many more that I haven't filled in. + _, +}; + +/// Data type for texture images. +pub const DataType = enum(c_uint) { + UnsignedByte = c.GL_UNSIGNED_BYTE, + + // There are so many more that I haven't filled in. + _, +}; + +pub const Binding = struct { + target: Target, + + pub inline fn unbind(b: *Binding) void { + glad.context.BindTexture.?(@intFromEnum(b.target), 0); + b.* = undefined; + } + + pub fn generateMipmap(b: Binding) void { + glad.context.GenerateMipmap.?(@intFromEnum(b.target)); + } + + pub fn parameter(b: Binding, name: Parameter, value: anytype) !void { + switch (@TypeOf(value)) { + c.GLint => glad.context.TexParameteri.?( + @intFromEnum(b.target), + @intFromEnum(name), + value, + ), + else => unreachable, + } + } + + pub fn image2D( + b: Binding, + level: c.GLint, + internal_format: InternalFormat, + width: c.GLsizei, + height: c.GLsizei, + border: c.GLint, + format: Format, + typ: DataType, + data: ?*const anyopaque, + ) !void { + glad.context.TexImage2D.?( + @intFromEnum(b.target), + level, + @intFromEnum(internal_format), + width, + height, + border, + @intFromEnum(format), + @intFromEnum(typ), + data, + ); + } + + pub fn subImage2D( + b: Binding, + level: c.GLint, + xoffset: c.GLint, + yoffset: c.GLint, + width: c.GLsizei, + height: c.GLsizei, + format: Format, + typ: DataType, + data: ?*const anyopaque, + ) !void { + glad.context.TexSubImage2D.?( + @intFromEnum(b.target), + level, + xoffset, + yoffset, + width, + height, + @intFromEnum(format), + @intFromEnum(typ), + data, + ); + } +}; + +/// Create a single texture. +pub inline fn create() !Texture { + var id: c.GLuint = undefined; + glad.context.GenTextures.?(1, &id); + return Texture{ .id = id }; +} + +/// glBindTexture +pub inline fn bind(v: Texture, target: Target) !Binding { + glad.context.BindTexture.?(@intFromEnum(target), v.id); + try errors.getError(); + return Binding{ .target = target }; +} + +pub inline fn destroy(v: Texture) void { + glad.context.DeleteTextures.?(1, &v.id); +} diff --git a/pkg/opengl/VertexArray.zig b/pkg/opengl/VertexArray.zig new file mode 100644 index 000000000..b86794042 --- /dev/null +++ b/pkg/opengl/VertexArray.zig @@ -0,0 +1,29 @@ +const VertexArray = @This(); + +const c = @import("c.zig"); +const glad = @import("glad.zig"); +const errors = @import("errors.zig"); + +id: c.GLuint, + +/// Create a single vertex array object. +pub inline fn create() !VertexArray { + var vao: c.GLuint = undefined; + glad.context.GenVertexArrays.?(1, &vao); + return VertexArray{ .id = vao }; +} + +// Unbind any active vertex array. +pub inline fn unbind() !void { + glad.context.BindVertexArray.?(0); +} + +/// glBindVertexArray +pub inline fn bind(v: VertexArray) !void { + glad.context.BindVertexArray.?(v.id); + try errors.getError(); +} + +pub inline fn destroy(v: VertexArray) void { + glad.context.DeleteVertexArrays.?(1, &v.id); +} diff --git a/pkg/opengl/build.zig b/pkg/opengl/build.zig new file mode 100644 index 000000000..34e5a8ab1 --- /dev/null +++ b/pkg/opengl/build.zig @@ -0,0 +1,5 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + _ = b.addModule("opengl", .{ .source_file = .{ .path = "main.zig" } }); +} diff --git a/pkg/opengl/c.zig b/pkg/opengl/c.zig new file mode 100644 index 000000000..8f4a0f22f --- /dev/null +++ b/pkg/opengl/c.zig @@ -0,0 +1,3 @@ +pub usingnamespace @cImport({ + @cInclude("glad/gl.h"); +}); diff --git a/pkg/opengl/draw.zig b/pkg/opengl/draw.zig new file mode 100644 index 000000000..ea6b63103 --- /dev/null +++ b/pkg/opengl/draw.zig @@ -0,0 +1,59 @@ +const c = @import("c.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); + +pub fn clearColor(r: f32, g: f32, b: f32, a: f32) void { + glad.context.ClearColor.?(r, g, b, a); +} + +pub fn clear(mask: c.GLbitfield) void { + glad.context.Clear.?(mask); +} + +pub fn drawArrays(mode: c.GLenum, first: c.GLint, count: c.GLsizei) !void { + glad.context.DrawArrays.?(mode, first, count); + try errors.getError(); +} + +pub fn drawElements(mode: c.GLenum, count: c.GLsizei, typ: c.GLenum, offset: usize) !void { + const offsetPtr = if (offset == 0) null else @as(*const anyopaque, @ptrFromInt(offset)); + glad.context.DrawElements.?(mode, count, typ, offsetPtr); + try errors.getError(); +} + +pub fn drawElementsInstanced( + mode: c.GLenum, + count: c.GLsizei, + typ: c.GLenum, + primcount: usize, +) !void { + glad.context.DrawElementsInstanced.?(mode, count, typ, null, @intCast(primcount)); + try errors.getError(); +} + +pub fn enable(cap: c.GLenum) !void { + glad.context.Enable.?(cap); + try errors.getError(); +} + +pub fn frontFace(mode: c.GLenum) !void { + glad.context.FrontFace.?(mode); + try errors.getError(); +} + +pub fn blendFunc(sfactor: c.GLenum, dfactor: c.GLenum) !void { + glad.context.BlendFunc.?(sfactor, dfactor); + try errors.getError(); +} + +pub fn viewport(x: c.GLint, y: c.GLint, width: c.GLsizei, height: c.GLsizei) !void { + glad.context.Viewport.?(x, y, width, height); +} + +pub fn pixelStore(mode: c.GLenum, value: anytype) !void { + switch (@typeInfo(@TypeOf(value))) { + .ComptimeInt, .Int => glad.context.PixelStorei.?(mode, value), + else => unreachable, + } + try errors.getError(); +} diff --git a/pkg/opengl/errors.zig b/pkg/opengl/errors.zig new file mode 100644 index 000000000..86639a53a --- /dev/null +++ b/pkg/opengl/errors.zig @@ -0,0 +1,33 @@ +const std = @import("std"); +const c = @import("c.zig"); +const glad = @import("glad.zig"); + +pub const Error = error{ + InvalidEnum, + InvalidValue, + InvalidOperation, + InvalidFramebufferOperation, + OutOfMemory, + + Unknown, +}; + +/// getError returns the error (if any) from the last OpenGL operation. +pub fn getError() Error!void { + return switch (glad.context.GetError.?()) { + c.GL_NO_ERROR => {}, + c.GL_INVALID_ENUM => Error.InvalidEnum, + c.GL_INVALID_VALUE => Error.InvalidValue, + c.GL_INVALID_OPERATION => Error.InvalidOperation, + c.GL_INVALID_FRAMEBUFFER_OPERATION => Error.InvalidFramebufferOperation, + c.GL_OUT_OF_MEMORY => Error.OutOfMemory, + else => Error.Unknown, + }; +} + +/// mustError just calls getError but always results in an error being returned. +/// If getError has no error, then Unknown is returned. +pub fn mustError() Error!void { + try getError(); + return Error.Unknown; +} diff --git a/pkg/opengl/extensions.zig b/pkg/opengl/extensions.zig new file mode 100644 index 000000000..ca8a4973d --- /dev/null +++ b/pkg/opengl/extensions.zig @@ -0,0 +1,32 @@ +const std = @import("std"); +const c = @import("c.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); + +/// Returns the number of extensions. +pub fn len() !u32 { + var n: c.GLint = undefined; + glad.context.GetIntegerv.?(c.GL_NUM_EXTENSIONS, &n); + try errors.getError(); + return @intCast(n); +} + +/// Returns an iterator for the extensions. +pub fn iterator() !Iterator { + return Iterator{ .len = try len() }; +} + +/// Iterator for the available extensions. +pub const Iterator = struct { + /// The total number of extensions. + len: c.GLuint = 0, + i: c.GLuint = 0, + + pub fn next(self: *Iterator) !?[]const u8 { + if (self.i >= self.len) return null; + const res = glad.context.GetStringi.?(c.GL_EXTENSIONS, self.i); + try errors.getError(); + self.i += 1; + return std.mem.sliceTo(res, 0); + } +}; diff --git a/pkg/opengl/glad.zig b/pkg/opengl/glad.zig new file mode 100644 index 000000000..4ee85c549 --- /dev/null +++ b/pkg/opengl/glad.zig @@ -0,0 +1,45 @@ +const std = @import("std"); +const c = @import("c.zig"); + +pub const Context = c.GladGLContext; + +/// This is the current context. Set this var manually prior to calling +/// any of this package's functions. I know its nasty to have a global but +/// this makes it match OpenGL API styles where it also operates on a +/// threadlocal global. +pub threadlocal var context: Context = undefined; + +/// Initialize Glad. This is guaranteed to succeed if no errors are returned. +/// The getProcAddress param is an anytype so that we can accept multiple +/// forms of the function depending on what we're interfacing with. +pub fn load(getProcAddress: anytype) !c_int { + const GlProc = *const fn () callconv(.C) void; + const GlfwFn = *const fn ([*:0]const u8) callconv(.C) ?GlProc; + + const res = switch (@TypeOf(getProcAddress)) { + // glfw + GlfwFn => c.gladLoadGLContext(&context, @ptrCast(getProcAddress)), + + // null proc address means that we are just loading the globally + // pointed gl functions + @TypeOf(null) => c.gladLoaderLoadGLContext(&context), + + // try as-is. If this introduces a compiler error, then add a new case. + else => c.gladLoadGLContext(&context, getProcAddress), + }; + if (res == 0) return error.GLInitFailed; + return res; +} + +pub fn unload() void { + c.gladLoaderUnloadGLContext(&context); + context = undefined; +} + +pub fn versionMajor(res: c_uint) c_uint { + return c.GLAD_VERSION_MAJOR(res); +} + +pub fn versionMinor(res: c_uint) c_uint { + return c.GLAD_VERSION_MINOR(res); +} diff --git a/pkg/opengl/main.zig b/pkg/opengl/main.zig new file mode 100644 index 000000000..79d32acea --- /dev/null +++ b/pkg/opengl/main.zig @@ -0,0 +1,23 @@ +//! OpenGL bindings. +//! +//! These are purpose-built for usage within this program. While they closely +//! align with the OpenGL C APIs, they aren't meant to be general purpose, +//! they aren't meant to have 100% API coverage, and they aren't meant to +//! be hyper-performant. +//! +//! For performance-intensive or unsupported aspects of OpenGL, the C +//! API is exposed via the `c` constant. +//! +//! WARNING: Lots of performance improvements that we can make with Zig +//! comptime help. I'm deferring this until later but have some fun ideas. + +pub const c = @import("c.zig"); +pub const glad = @import("glad.zig"); +pub usingnamespace @import("draw.zig"); + +pub const ext = @import("extensions.zig"); +pub const Buffer = @import("Buffer.zig"); +pub const Program = @import("Program.zig"); +pub const Shader = @import("Shader.zig"); +pub const Texture = @import("Texture.zig"); +pub const VertexArray = @import("VertexArray.zig"); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 32781a7eb..ce442ed48 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -7,6 +7,7 @@ const glfw = @import("glfw"); const assert = std.debug.assert; const testing = std.testing; const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); const font = @import("../font/main.zig"); @@ -14,7 +15,7 @@ const imgui = @import("imgui"); const renderer = @import("../renderer.zig"); const terminal = @import("../terminal/main.zig"); const Terminal = terminal.Terminal; -const gl = @import("opengl/main.zig"); +const gl = @import("opengl"); const trace = @import("tracy").trace; const math = @import("../math.zig"); const Surface = @import("../Surface.zig"); @@ -226,8 +227,10 @@ const GPUCellMode = enum(u8) { /// configuration. This must be exported so that we don't need to /// pass around Config pointers which makes memory management a pain. pub const DerivedConfig = struct { + arena: ArenaAllocator, + font_thicken: bool, - font_features: std.ArrayList([]const u8), + font_features: std.ArrayListUnmanaged([]const u8), font_styles: font.Group.StyleStatus, cursor_color: ?terminal.color.RGB, cursor_text: ?terminal.color.RGB, @@ -238,17 +241,21 @@ pub const DerivedConfig = struct { selection_background: ?terminal.color.RGB, selection_foreground: ?terminal.color.RGB, invert_selection_fg_bg: bool, + custom_shaders: std.ArrayListUnmanaged([]const u8), pub fn init( alloc_gpa: Allocator, config: *const configpkg.Config, ) !DerivedConfig { + var arena = ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Copy our shaders + const custom_shaders = try config.@"custom-shader".value.list.clone(alloc); + // Copy our font features - var font_features = features: { - var clone = try config.@"font-feature".list.clone(alloc_gpa); - break :features clone.toManaged(alloc_gpa); - }; - errdefer font_features.deinit(); + const font_features = try config.@"font-feature".list.clone(alloc); // Get our font styles var font_styles = font.Group.StyleStatus.initFill(true); @@ -287,11 +294,15 @@ pub const DerivedConfig = struct { bg.toTerminalRGB() else null, + + .custom_shaders = custom_shaders, + + .arena = arena, }; } pub fn deinit(self: *DerivedConfig) void { - self.font_features.deinit(); + self.arena.deinit(); } }; diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 45c225d85..92b340e76 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -335,5 +335,22 @@ test "shadertoy to msl" { defer alloc.free(msl); } +test "shadertoy to glsl" { + const testing = std.testing; + const alloc = testing.allocator; + + const src = try testGlslZ(alloc, test_crt); + defer alloc.free(src); + + var spvlist = std.ArrayListAligned(u8, @alignOf(u32)).init(alloc); + defer spvlist.deinit(); + try spirvFromGlsl(spvlist.writer(), null, src); + + const glsl = try glslFromSpv(alloc, spvlist.items); + defer alloc.free(glsl); + + //log.warn("glsl={s}", .{glsl}); +} + const test_crt = @embedFile("shaders/test_shadertoy_crt.glsl"); const test_invalid = @embedFile("shaders/test_shadertoy_invalid.glsl"); From 3aa217ad2eb8cab2c908c39f8f51aeb5d0301406 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 21:44:02 -0800 Subject: [PATCH 31/55] pkg/opengl: add Framebuffer APIs --- pkg/opengl/Framebuffer.zig | 83 ++++++++++++++++++++++++++++++++++++++ pkg/opengl/main.zig | 1 + 2 files changed, 84 insertions(+) create mode 100644 pkg/opengl/Framebuffer.zig diff --git a/pkg/opengl/Framebuffer.zig b/pkg/opengl/Framebuffer.zig new file mode 100644 index 000000000..6335fa538 --- /dev/null +++ b/pkg/opengl/Framebuffer.zig @@ -0,0 +1,83 @@ +const Framebuffer = @This(); + +const std = @import("std"); +const c = @import("c.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); +const Texture = @import("Texture.zig"); + +id: c.GLuint, + +/// Create a single buffer. +pub fn create() !Framebuffer { + var fbo: c.GLuint = undefined; + glad.context.GenFramebuffers.?(1, &fbo); + return .{ .id = fbo }; +} + +pub fn destroy(v: Framebuffer) void { + glad.context.DeleteFramebuffers.?(1, &v.id); +} + +pub fn bind(v: Framebuffer, target: Target) !Binding { + glad.context.BindFramebuffer.?(@intFromEnum(target), v.id); + return .{ .target = target }; +} + +/// Enum for possible binding targets. +pub const Target = enum(c_uint) { + framebuffer = c.GL_FRAMEBUFFER, + draw = c.GL_DRAW_FRAMEBUFFER, + read = c.GL_READ_FRAMEBUFFER, + _, +}; + +pub const Attachment = enum(c_uint) { + color0 = c.GL_COLOR_ATTACHMENT0, + depth = c.GL_DEPTH_ATTACHMENT, + stencil = c.GL_STENCIL_ATTACHMENT, + depth_stencil = c.GL_DEPTH_STENCIL_ATTACHMENT, + _, +}; + +pub const Status = enum(c_uint) { + complete = c.GL_FRAMEBUFFER_COMPLETE, + undefined = c.GL_FRAMEBUFFER_UNDEFINED, + incomplete_attachment = c.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT, + incomplete_missing_attachment = c.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT, + incomplete_draw_buffer = c.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER, + incomplete_read_buffer = c.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER, + unsupported = c.GL_FRAMEBUFFER_UNSUPPORTED, + incomplete_multisample = c.GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE, + incomplete_layer_targets = c.GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS, + _, +}; + +pub const Binding = struct { + target: Target, + + pub fn unbind(self: Binding) void { + glad.context.BindFramebuffer.?(@intFromEnum(self.target), 0); + } + + pub fn texture2D( + self: Binding, + attachment: Attachment, + textarget: Texture.Target, + texture: Texture, + level: c.GLint, + ) !void { + glad.context.FramebufferTexture2D.?( + @intFromEnum(self.target), + @intFromEnum(attachment), + @intFromEnum(textarget), + texture.id, + level, + ); + try errors.getError(); + } + + pub fn checkStatus(self: Binding) Status { + return @enumFromInt(glad.context.CheckFramebufferStatus.?(self.target)); + } +}; diff --git a/pkg/opengl/main.zig b/pkg/opengl/main.zig index 79d32acea..9045beabf 100644 --- a/pkg/opengl/main.zig +++ b/pkg/opengl/main.zig @@ -17,6 +17,7 @@ pub usingnamespace @import("draw.zig"); pub const ext = @import("extensions.zig"); pub const Buffer = @import("Buffer.zig"); +pub const Framebuffer = @import("Framebuffer.zig"); pub const Program = @import("Program.zig"); pub const Shader = @import("Shader.zig"); pub const Texture = @import("Texture.zig"); From 46dd084ee9ba47997ed1f895fc2b0fc7bf39c480 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 08:27:06 -0800 Subject: [PATCH 32/55] renderer/opengl: wip --- src/renderer/OpenGL.zig | 33 +++++++++++++++++++++++++++--- src/renderer/shaders/custom.v.glsl | 7 +++++++ src/renderer/shadertoy.zig | 7 ++++--- 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 src/renderer/shaders/custom.v.glsl diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index ce442ed48..7d91cc9a9 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -8,6 +8,7 @@ const assert = std.debug.assert; const testing = std.testing; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; +const shadertoy = @import("shadertoy.zig"); const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); const font = @import("../font/main.zig"); @@ -320,7 +321,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { options.config.font_thicken, ); - var gl_state = try GLState.init(options.font_group); + var gl_state = try GLState.init(alloc, options.config, options.font_group); errdefer gl_state.deinit(); return OpenGL{ @@ -439,7 +440,7 @@ pub fn displayRealize(self: *OpenGL) !void { ); // Make our new state - var gl_state = try GLState.init(self.font_group); + var gl_state = try GLState.init(self.alloc, self.config, self.font_group); errdefer gl_state.deinit(); // Unrealize if we have to @@ -1579,7 +1580,33 @@ const GLState = struct { texture: gl.Texture, texture_color: gl.Texture, - pub fn init(font_group: *font.GroupCache) !GLState { + pub fn init( + alloc: Allocator, + config: DerivedConfig, + font_group: *font.GroupCache, + ) !GLState { + var arena = ArenaAllocator.init(alloc); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + // Load our custom shaders + const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( + arena_alloc, + config.custom_shaders.items, + .glsl, + ) catch |err| err: { + log.warn("error loading custom shaders err={}", .{err}); + break :err &.{}; + }; + + if (custom_shaders.len > 0) { + const cp = try gl.Program.createVF( + @embedFile("shaders/custom.v.glsl"), + custom_shaders[0], + ); + _ = cp; + } + // Blending for text. We use GL_ONE here because we should be using // premultiplied alpha for all our colors in our fragment shaders. // This avoids having a blurry border where transparency is expected on diff --git a/src/renderer/shaders/custom.v.glsl b/src/renderer/shaders/custom.v.glsl new file mode 100644 index 000000000..ff6f60541 --- /dev/null +++ b/src/renderer/shaders/custom.v.glsl @@ -0,0 +1,7 @@ +#version 330 core + +layout (location = 0) in vec2 position; + +void main(){ + gl_Position = vec4(position.x, position.y, 0.0f, 1.0f); +} diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 92b340e76..90541fda9 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -9,7 +9,7 @@ const spvcross = @import("spirv_cross"); const log = std.log.scoped(.shadertoy); /// The target to load shaders for. -pub const Target = enum { msl }; +pub const Target = enum { glsl, msl }; /// Load a set of shaders from files and convert them to the target /// format. The shader order is preserved. @@ -87,6 +87,7 @@ pub fn loadFromFile( return switch (target) { // Important: using the alloc_gpa here on purpose because this // is the final result that will be returned to the caller. + .glsl => try glslFromSpv(alloc_gpa, spirv), .msl => try mslFromSpv(alloc_gpa, spirv), }; } @@ -203,7 +204,7 @@ pub fn glslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 { if (c.spvc_compiler_options_set_uint( options, c.SPVC_COMPILER_OPTION_GLSL_VERSION, - 330, + 430, ) != c.SPVC_SUCCESS) { return error.SpvcFailed; } @@ -349,7 +350,7 @@ test "shadertoy to glsl" { const glsl = try glslFromSpv(alloc, spvlist.items); defer alloc.free(glsl); - //log.warn("glsl={s}", .{glsl}); + log.warn("glsl={s}", .{glsl}); } const test_crt = @embedFile("shaders/test_shadertoy_crt.glsl"); From fb0929a11b562b81f031967776ed6efe6b1bbfb4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 08:52:34 -0800 Subject: [PATCH 33/55] renderer/opengl: extract cell program state to dedicated struct --- pkg/opengl/Buffer.zig | 23 ++- pkg/opengl/Program.zig | 35 +++-- pkg/opengl/VertexArray.zig | 19 ++- src/apprt/gtk/ImguiWidget.zig | 2 +- src/renderer/OpenGL.zig | 119 +++------------ src/renderer/opengl/Buffer.zig | 218 ---------------------------- src/renderer/opengl/CellProgram.zig | 183 +++++++++++++++++++++++ src/renderer/opengl/Program.zig | 128 ---------------- src/renderer/opengl/Shader.zig | 56 ------- src/renderer/opengl/Texture.zig | 163 --------------------- src/renderer/opengl/VertexArray.zig | 29 ---- src/renderer/opengl/c.zig | 3 - src/renderer/opengl/draw.zig | 59 -------- src/renderer/opengl/errors.zig | 33 ----- src/renderer/opengl/extensions.zig | 32 ---- src/renderer/opengl/glad.zig | 45 ------ src/renderer/opengl/main.zig | 23 --- src/renderer/shadertoy.zig | 10 +- 18 files changed, 250 insertions(+), 930 deletions(-) delete mode 100644 src/renderer/opengl/Buffer.zig create mode 100644 src/renderer/opengl/CellProgram.zig delete mode 100644 src/renderer/opengl/Program.zig delete mode 100644 src/renderer/opengl/Shader.zig delete mode 100644 src/renderer/opengl/Texture.zig delete mode 100644 src/renderer/opengl/VertexArray.zig delete mode 100644 src/renderer/opengl/c.zig delete mode 100644 src/renderer/opengl/draw.zig delete mode 100644 src/renderer/opengl/errors.zig delete mode 100644 src/renderer/opengl/extensions.zig delete mode 100644 src/renderer/opengl/glad.zig delete mode 100644 src/renderer/opengl/main.zig diff --git a/pkg/opengl/Buffer.zig b/pkg/opengl/Buffer.zig index b794ca4f0..c004dc3f4 100644 --- a/pkg/opengl/Buffer.zig +++ b/pkg/opengl/Buffer.zig @@ -36,7 +36,7 @@ pub const Binding = struct { /// Sets the data of this bound buffer. The data can be any array-like /// type. The size of the data is automatically determined based on the type. - pub inline fn setData( + pub fn setData( b: Binding, data: anytype, usage: Usage, @@ -48,7 +48,7 @@ pub const Binding = struct { /// Sets the data of this bound buffer. The data can be any array-like /// type. The size of the data is automatically determined based on the type. - pub inline fn setSubData( + pub fn setSubData( b: Binding, offset: usize, data: anytype, @@ -61,7 +61,7 @@ pub const Binding = struct { /// Sets the buffer data with a null buffer that is expected to be /// filled in the future using subData. This requires the type just so /// we can setup the data size. - pub inline fn setDataNull( + pub fn setDataNull( b: Binding, comptime T: type, usage: Usage, @@ -71,7 +71,7 @@ pub const Binding = struct { } /// Same as setDataNull but lets you manually specify the buffer size. - pub inline fn setDataNullManual( + pub fn setDataNullManual( b: Binding, size: usize, usage: Usage, @@ -106,7 +106,7 @@ pub const Binding = struct { }; } - pub inline fn enableAttribArray(_: Binding, idx: c.GLuint) !void { + pub fn enableAttribArray(_: Binding, idx: c.GLuint) !void { glad.context.EnableVertexAttribArray.?(idx); } @@ -158,7 +158,7 @@ pub const Binding = struct { try errors.getError(); } - pub inline fn attributeAdvanced( + pub fn attributeAdvanced( _: Binding, idx: c.GLuint, size: c.GLint, @@ -177,7 +177,7 @@ pub const Binding = struct { try errors.getError(); } - pub inline fn attributeIAdvanced( + pub fn attributeIAdvanced( _: Binding, idx: c.GLuint, size: c.GLint, @@ -194,25 +194,24 @@ pub const Binding = struct { try errors.getError(); } - pub inline fn unbind(b: *Binding) void { + pub fn unbind(b: Binding) void { glad.context.BindBuffer.?(@intFromEnum(b.target), 0); - b.* = undefined; } }; /// Create a single buffer. -pub inline fn create() !Buffer { +pub fn create() !Buffer { var vbo: c.GLuint = undefined; glad.context.GenBuffers.?(1, &vbo); return Buffer{ .id = vbo }; } /// glBindBuffer -pub inline fn bind(v: Buffer, target: Target) !Binding { +pub fn bind(v: Buffer, target: Target) !Binding { glad.context.BindBuffer.?(@intFromEnum(target), v.id); return Binding{ .target = target }; } -pub inline fn destroy(v: Buffer) void { +pub fn destroy(v: Buffer) void { glad.context.DeleteBuffers.?(1, &v.id); } diff --git a/pkg/opengl/Program.zig b/pkg/opengl/Program.zig index d266bd226..e8a691f17 100644 --- a/pkg/opengl/Program.zig +++ b/pkg/opengl/Program.zig @@ -11,23 +11,22 @@ const glad = @import("glad.zig"); id: c.GLuint, -const Binding = struct { - pub inline fn unbind(_: Binding) void { +pub const Binding = struct { + pub fn unbind(_: Binding) void { glad.context.UseProgram.?(0); } }; -pub inline fn create() !Program { +pub fn create() !Program { const id = glad.context.CreateProgram.?(); if (id == 0) try errors.mustError(); - log.debug("program created id={}", .{id}); - return Program{ .id = id }; + return .{ .id = id }; } /// Create a program from a vertex and fragment shader source. This will /// compile and link the vertex and fragment shader. -pub inline fn createVF(vsrc: [:0]const u8, fsrc: [:0]const u8) !Program { +pub fn createVF(vsrc: [:0]const u8, fsrc: [:0]const u8) !Program { const vs = try Shader.create(c.GL_VERTEX_SHADER); try vs.setSourceAndCompile(vsrc); defer vs.destroy(); @@ -44,12 +43,18 @@ pub inline fn createVF(vsrc: [:0]const u8, fsrc: [:0]const u8) !Program { return p; } -pub inline fn attachShader(p: Program, s: Shader) !void { +pub fn destroy(p: Program) void { + assert(p.id != 0); + glad.context.DeleteProgram.?(p.id); + log.debug("program destroyed id={}", .{p.id}); +} + +pub fn attachShader(p: Program, s: Shader) !void { glad.context.AttachShader.?(p.id, s.id); try errors.getError(); } -pub inline fn link(p: Program) !void { +pub fn link(p: Program) !void { glad.context.LinkProgram.?(p.id); // Check if linking succeeded @@ -67,14 +72,14 @@ pub inline fn link(p: Program) !void { return error.CompileFailed; } -pub inline fn use(p: Program) !Binding { +pub fn use(p: Program) !Binding { glad.context.UseProgram.?(p.id); try errors.getError(); - return Binding{}; + return .{}; } /// Requires the program is currently in use. -pub inline fn setUniform( +pub fn setUniform( p: Program, n: [:0]const u8, value: anytype, @@ -115,14 +120,8 @@ pub inline fn setUniform( // // NOTE(mitchellh): we can add a dynamic version that uses an allocator // if we ever need it. -pub inline fn getInfoLog(s: Program) [512]u8 { +pub fn getInfoLog(s: Program) [512]u8 { var msg: [512]u8 = undefined; glad.context.GetProgramInfoLog.?(s.id, msg.len, null, &msg); return msg; } - -pub inline fn destroy(p: Program) void { - assert(p.id != 0); - glad.context.DeleteProgram.?(p.id); - log.debug("program destroyed id={}", .{p.id}); -} diff --git a/pkg/opengl/VertexArray.zig b/pkg/opengl/VertexArray.zig index b86794042..4071c3a2a 100644 --- a/pkg/opengl/VertexArray.zig +++ b/pkg/opengl/VertexArray.zig @@ -7,23 +7,26 @@ const errors = @import("errors.zig"); id: c.GLuint, /// Create a single vertex array object. -pub inline fn create() !VertexArray { +pub fn create() !VertexArray { var vao: c.GLuint = undefined; glad.context.GenVertexArrays.?(1, &vao); return VertexArray{ .id = vao }; } -// Unbind any active vertex array. -pub inline fn unbind() !void { - glad.context.BindVertexArray.?(0); -} - /// glBindVertexArray -pub inline fn bind(v: VertexArray) !void { +pub fn bind(v: VertexArray) !Binding { glad.context.BindVertexArray.?(v.id); try errors.getError(); + return .{}; } -pub inline fn destroy(v: VertexArray) void { +pub fn destroy(v: VertexArray) void { glad.context.DeleteVertexArrays.?(1, &v.id); } + +pub const Binding = struct { + pub fn unbind(self: Binding) void { + _ = self; + glad.context.BindVertexArray.?(0); + } +}; diff --git a/src/apprt/gtk/ImguiWidget.zig b/src/apprt/gtk/ImguiWidget.zig index eb9b97f06..d0ce195f8 100644 --- a/src/apprt/gtk/ImguiWidget.zig +++ b/src/apprt/gtk/ImguiWidget.zig @@ -6,7 +6,7 @@ const assert = std.debug.assert; const cimgui = @import("cimgui"); const c = @import("c.zig"); const key = @import("key.zig"); -const gl = @import("../../renderer/opengl/main.zig"); +const gl = @import("opengl"); const input = @import("../../input.zig"); const log = std.log.scoped(.gtk_imgui_widget); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 7d91cc9a9..1d9bf6b81 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -21,6 +21,8 @@ const trace = @import("tracy").trace; const math = @import("../math.zig"); const Surface = @import("../Surface.zig"); +const CellProgram = @import("opengl/CellProgram.zig"); + const log = std.log.scoped(.grid); /// The runtime can request a single-threaded draw by setting this boolean @@ -132,7 +134,7 @@ const SetScreenSize = struct { ); // Update the projection uniform within our shader - try gl_state.program.setUniform( + try gl_state.cell_program.program.setUniform( "projection", // 2D orthographic projection with the full w/h @@ -152,18 +154,18 @@ const SetFontSize = struct { fn apply(self: SetFontSize, r: *const OpenGL) !void { const gl_state = r.gl_state orelse return error.OpenGLUninitialized; - try gl_state.program.setUniform( + try gl_state.cell_program.program.setUniform( "cell_size", @Vector(2, f32){ @floatFromInt(self.metrics.cell_width), @floatFromInt(self.metrics.cell_height), }, ); - try gl_state.program.setUniform( + try gl_state.cell_program.program.setUniform( "strikethrough_position", @as(f32, @floatFromInt(self.metrics.strikethrough_position)), ); - try gl_state.program.setUniform( + try gl_state.cell_program.program.setUniform( "strikethrough_thickness", @as(f32, @floatFromInt(self.metrics.strikethrough_thickness)), ); @@ -1471,17 +1473,9 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { ); gl.clear(gl.c.GL_COLOR_BUFFER_BIT); - // Setup our VAO - try gl_state.vao.bind(); - defer gl.VertexArray.unbind() catch null; - - // Bind EBO - var ebobind = try gl_state.ebo.bind(.ElementArrayBuffer); - defer ebobind.unbind(); - - // Bind VBO and set data - var binding = try gl_state.vbo.bind(.ArrayBuffer); - defer binding.unbind(); + // Bind our cell program state, buffers + const bind = try gl_state.cell_program.bind(); + defer bind.unbind(); // Bind our textures try gl.Texture.active(gl.c.GL_TEXTURE0); @@ -1492,10 +1486,6 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { var texbind1 = try gl_state.texture_color.bind(.@"2D"); defer texbind1.unbind(); - // Pick our shader to use - const pbind = try gl_state.program.use(); - defer pbind.unbind(); - // If we have deferred operations, run them. if (self.deferred_screen_size) |v| { try v.apply(self); @@ -1506,8 +1496,8 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { self.deferred_font_size = null; } - try self.drawCells(binding, self.cells_bg); - try self.drawCells(binding, self.cells); + try self.drawCells(bind.vbo, self.cells_bg); + try self.drawCells(bind.vbo, self.cells); // Swap our window buffers switch (apprt.runtime) { @@ -1573,10 +1563,7 @@ fn drawCells( /// easy to create/destroy these as a set in situations i.e. where the /// OpenGL context is replaced. const GLState = struct { - program: gl.Program, - vao: gl.VertexArray, - ebo: gl.Buffer, - vbo: gl.Buffer, + cell_program: CellProgram, texture: gl.Texture, texture_color: gl.Texture, @@ -1614,74 +1601,6 @@ const GLState = struct { try gl.enable(gl.c.GL_BLEND); try gl.blendFunc(gl.c.GL_ONE, gl.c.GL_ONE_MINUS_SRC_ALPHA); - // Shader - const program = try gl.Program.createVF( - @embedFile("shaders/cell.v.glsl"), - @embedFile("shaders/cell.f.glsl"), - ); - - // Set our cell dimensions - const pbind = try program.use(); - defer pbind.unbind(); - - // Set all of our texture indexes - try program.setUniform("text", 0); - try program.setUniform("text_color", 1); - - // Setup our VAO - const vao = try gl.VertexArray.create(); - errdefer vao.destroy(); - try vao.bind(); - defer gl.VertexArray.unbind() catch null; - - // Element buffer (EBO) - const ebo = try gl.Buffer.create(); - errdefer ebo.destroy(); - var ebobind = try ebo.bind(.ElementArrayBuffer); - defer ebobind.unbind(); - try ebobind.setData([6]u8{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }, .StaticDraw); - - // Vertex buffer (VBO) - const vbo = try gl.Buffer.create(); - errdefer vbo.destroy(); - var vbobind = try vbo.bind(.ArrayBuffer); - defer vbobind.unbind(); - var offset: usize = 0; - try vbobind.attributeAdvanced(0, 2, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(GPUCell), offset); - offset += 2 * @sizeOf(u16); - try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(GPUCell), offset); - offset += 2 * @sizeOf(u32); - try vbobind.attributeAdvanced(2, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(GPUCell), offset); - offset += 2 * @sizeOf(u32); - try vbobind.attributeAdvanced(3, 2, gl.c.GL_INT, false, @sizeOf(GPUCell), offset); - offset += 2 * @sizeOf(i32); - try vbobind.attributeAdvanced(4, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(GPUCell), offset); - offset += 4 * @sizeOf(u8); - try vbobind.attributeAdvanced(5, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(GPUCell), offset); - offset += 4 * @sizeOf(u8); - try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(GPUCell), offset); - offset += 1 * @sizeOf(u8); - try vbobind.attributeIAdvanced(7, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(GPUCell), offset); - try vbobind.enableAttribArray(0); - try vbobind.enableAttribArray(1); - try vbobind.enableAttribArray(2); - try vbobind.enableAttribArray(3); - try vbobind.enableAttribArray(4); - try vbobind.enableAttribArray(5); - try vbobind.enableAttribArray(6); - try vbobind.enableAttribArray(7); - try vbobind.attributeDivisor(0, 1); - try vbobind.attributeDivisor(1, 1); - try vbobind.attributeDivisor(2, 1); - try vbobind.attributeDivisor(3, 1); - try vbobind.attributeDivisor(4, 1); - try vbobind.attributeDivisor(5, 1); - try vbobind.attributeDivisor(6, 1); - try vbobind.attributeDivisor(7, 1); - // Build our texture const tex = try gl.Texture.create(); errdefer tex.destroy(); @@ -1724,11 +1643,12 @@ const GLState = struct { ); } + // Build our cell renderer + const cell_program = try CellProgram.init(); + errdefer cell_program.deinit(); + return .{ - .program = program, - .vao = vao, - .ebo = ebo, - .vbo = vbo, + .cell_program = cell_program, .texture = tex, .texture_color = tex_color, }; @@ -1737,9 +1657,6 @@ const GLState = struct { pub fn deinit(self: *GLState) void { self.texture.destroy(); self.texture_color.destroy(); - self.vbo.destroy(); - self.ebo.destroy(); - self.vao.destroy(); - self.program.destroy(); + self.cell_program.deinit(); } }; diff --git a/src/renderer/opengl/Buffer.zig b/src/renderer/opengl/Buffer.zig deleted file mode 100644 index b794ca4f0..000000000 --- a/src/renderer/opengl/Buffer.zig +++ /dev/null @@ -1,218 +0,0 @@ -const Buffer = @This(); - -const std = @import("std"); -const c = @import("c.zig"); -const errors = @import("errors.zig"); -const glad = @import("glad.zig"); - -id: c.GLuint, - -/// Enum for possible binding targets. -pub const Target = enum(c_uint) { - ArrayBuffer = c.GL_ARRAY_BUFFER, - ElementArrayBuffer = c.GL_ELEMENT_ARRAY_BUFFER, - _, -}; - -/// Enum for possible buffer usages. -pub const Usage = enum(c_uint) { - StreamDraw = c.GL_STREAM_DRAW, - StreamRead = c.GL_STREAM_READ, - StreamCopy = c.GL_STREAM_COPY, - StaticDraw = c.GL_STATIC_DRAW, - StaticRead = c.GL_STATIC_READ, - StaticCopy = c.GL_STATIC_COPY, - DynamicDraw = c.GL_DYNAMIC_DRAW, - DynamicRead = c.GL_DYNAMIC_READ, - DynamicCopy = c.GL_DYNAMIC_COPY, - _, -}; - -/// Binding is a bound buffer. By using this for functions that operate -/// on bound buffers, you can easily defer unbinding and in safety-enabled -/// modes verify that unbound buffers are never accessed. -pub const Binding = struct { - target: Target, - - /// Sets the data of this bound buffer. The data can be any array-like - /// type. The size of the data is automatically determined based on the type. - pub inline fn setData( - b: Binding, - data: anytype, - usage: Usage, - ) !void { - const info = dataInfo(&data); - glad.context.BufferData.?(@intFromEnum(b.target), info.size, info.ptr, @intFromEnum(usage)); - try errors.getError(); - } - - /// Sets the data of this bound buffer. The data can be any array-like - /// type. The size of the data is automatically determined based on the type. - pub inline fn setSubData( - b: Binding, - offset: usize, - data: anytype, - ) !void { - const info = dataInfo(data); - glad.context.BufferSubData.?(@intFromEnum(b.target), @intCast(offset), info.size, info.ptr); - try errors.getError(); - } - - /// Sets the buffer data with a null buffer that is expected to be - /// filled in the future using subData. This requires the type just so - /// we can setup the data size. - pub inline fn setDataNull( - b: Binding, - comptime T: type, - usage: Usage, - ) !void { - glad.context.BufferData.?(@intFromEnum(b.target), @sizeOf(T), null, @intFromEnum(usage)); - try errors.getError(); - } - - /// Same as setDataNull but lets you manually specify the buffer size. - pub inline fn setDataNullManual( - b: Binding, - size: usize, - usage: Usage, - ) !void { - glad.context.BufferData.?(@intFromEnum(b.target), @intCast(size), null, @intFromEnum(usage)); - try errors.getError(); - } - - fn dataInfo(data: anytype) struct { - size: isize, - ptr: *const anyopaque, - } { - return switch (@typeInfo(@TypeOf(data))) { - .Pointer => |ptr| switch (ptr.size) { - .One => .{ - .size = @sizeOf(ptr.child) * data.len, - .ptr = data, - }, - .Slice => .{ - .size = @intCast(@sizeOf(ptr.child) * data.len), - .ptr = data.ptr, - }, - else => { - std.log.err("invalid buffer data pointer size: {}", .{ptr.size}); - unreachable; - }, - }, - else => { - std.log.err("invalid buffer data type: {s}", .{@tagName(@typeInfo(@TypeOf(data)))}); - unreachable; - }, - }; - } - - pub inline fn enableAttribArray(_: Binding, idx: c.GLuint) !void { - glad.context.EnableVertexAttribArray.?(idx); - } - - /// Shorthand for vertexAttribPointer that is specialized towards the - /// common use case of specifying an array of homogeneous types that - /// don't need normalization. This also enables the attribute at idx. - pub fn attribute( - b: Binding, - idx: c.GLuint, - size: c.GLint, - comptime T: type, - offset: usize, - ) !void { - const info: struct { - // Type of the each component in the array. - typ: c.GLenum, - - // The byte offset between each full set of attributes. - stride: c.GLsizei, - - // The size of each component used in calculating the offset. - offset: usize, - } = switch (@typeInfo(T)) { - .Array => |ary| .{ - .typ = switch (ary.child) { - f32 => c.GL_FLOAT, - else => @compileError("unsupported array child type"), - }, - .offset = @sizeOf(ary.child), - .stride = @sizeOf(T), - }, - else => @compileError("unsupported type"), - }; - - try b.attributeAdvanced( - idx, - size, - info.typ, - false, - info.stride, - offset * info.offset, - ); - try b.enableAttribArray(idx); - } - - /// VertexAttribDivisor - pub fn attributeDivisor(_: Binding, idx: c.GLuint, divisor: c.GLuint) !void { - glad.context.VertexAttribDivisor.?(idx, divisor); - try errors.getError(); - } - - pub inline fn attributeAdvanced( - _: Binding, - idx: c.GLuint, - size: c.GLint, - typ: c.GLenum, - normalized: bool, - stride: c.GLsizei, - offset: usize, - ) !void { - const normalized_c: c.GLboolean = if (normalized) c.GL_TRUE else c.GL_FALSE; - const offsetPtr = if (offset > 0) - @as(*const anyopaque, @ptrFromInt(offset)) - else - null; - - glad.context.VertexAttribPointer.?(idx, size, typ, normalized_c, stride, offsetPtr); - try errors.getError(); - } - - pub inline fn attributeIAdvanced( - _: Binding, - idx: c.GLuint, - size: c.GLint, - typ: c.GLenum, - stride: c.GLsizei, - offset: usize, - ) !void { - const offsetPtr = if (offset > 0) - @as(*const anyopaque, @ptrFromInt(offset)) - else - null; - - glad.context.VertexAttribIPointer.?(idx, size, typ, stride, offsetPtr); - try errors.getError(); - } - - pub inline fn unbind(b: *Binding) void { - glad.context.BindBuffer.?(@intFromEnum(b.target), 0); - b.* = undefined; - } -}; - -/// Create a single buffer. -pub inline fn create() !Buffer { - var vbo: c.GLuint = undefined; - glad.context.GenBuffers.?(1, &vbo); - return Buffer{ .id = vbo }; -} - -/// glBindBuffer -pub inline fn bind(v: Buffer, target: Target) !Binding { - glad.context.BindBuffer.?(@intFromEnum(target), v.id); - return Binding{ .target = target }; -} - -pub inline fn destroy(v: Buffer) void { - glad.context.DeleteBuffers.?(1, &v.id); -} diff --git a/src/renderer/opengl/CellProgram.zig b/src/renderer/opengl/CellProgram.zig new file mode 100644 index 000000000..2f3ce3742 --- /dev/null +++ b/src/renderer/opengl/CellProgram.zig @@ -0,0 +1,183 @@ +/// The OpenGL program for rendering terminal cells. +const CellProgram = @This(); + +const std = @import("std"); +const gl = @import("opengl"); + +program: gl.Program, +vao: gl.VertexArray, +ebo: gl.Buffer, +vbo: gl.Buffer, + +/// The raw structure that maps directly to the buffer sent to the vertex shader. +/// This must be "extern" so that the field order is not reordered by the +/// Zig compiler. +const Cell = extern struct { + /// vec2 grid_coord + grid_col: u16, + grid_row: u16, + + /// vec2 glyph_pos + glyph_x: u32 = 0, + glyph_y: u32 = 0, + + /// vec2 glyph_size + glyph_width: u32 = 0, + glyph_height: u32 = 0, + + /// vec2 glyph_size + glyph_offset_x: i32 = 0, + glyph_offset_y: i32 = 0, + + /// vec4 fg_color_in + fg_r: u8, + fg_g: u8, + fg_b: u8, + fg_a: u8, + + /// vec4 bg_color_in + bg_r: u8, + bg_g: u8, + bg_b: u8, + bg_a: u8, + + /// uint mode + mode: CellMode, + + /// The width in grid cells that a rendering takes. + grid_width: u8, +}; + +const CellMode = enum(u8) { + bg = 1, + fg = 2, + fg_color = 7, + strikethrough = 8, + + // Non-exhaustive because masks change it + _, + + /// Apply a mask to the mode. + pub fn mask(self: CellMode, m: CellMode) CellMode { + return @enumFromInt(@intFromEnum(self) | @intFromEnum(m)); + } +}; + +pub fn init() !CellProgram { + // Load and compile our shaders. + const program = try gl.Program.createVF( + @embedFile("../shaders/cell.v.glsl"), + @embedFile("../shaders/cell.f.glsl"), + ); + + // Set our cell dimensions + const pbind = try program.use(); + defer pbind.unbind(); + + // Set all of our texture indexes + try program.setUniform("text", 0); + try program.setUniform("text_color", 1); + + // Setup our VAO + const vao = try gl.VertexArray.create(); + errdefer vao.destroy(); + const vaobind = try vao.bind(); + defer vaobind.unbind(); + + // Element buffer (EBO) + const ebo = try gl.Buffer.create(); + errdefer ebo.destroy(); + var ebobind = try ebo.bind(.ElementArrayBuffer); + defer ebobind.unbind(); + try ebobind.setData([6]u8{ + 0, 1, 3, // Top-left triangle + 1, 2, 3, // Bottom-right triangle + }, .StaticDraw); + + // Vertex buffer (VBO) + const vbo = try gl.Buffer.create(); + errdefer vbo.destroy(); + var vbobind = try vbo.bind(.ArrayBuffer); + defer vbobind.unbind(); + var offset: usize = 0; + try vbobind.attributeAdvanced(0, 2, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(Cell), offset); + offset += 2 * @sizeOf(u16); + try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Cell), offset); + offset += 2 * @sizeOf(u32); + try vbobind.attributeAdvanced(2, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Cell), offset); + offset += 2 * @sizeOf(u32); + try vbobind.attributeAdvanced(3, 2, gl.c.GL_INT, false, @sizeOf(Cell), offset); + offset += 2 * @sizeOf(i32); + try vbobind.attributeAdvanced(4, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Cell), offset); + offset += 4 * @sizeOf(u8); + try vbobind.attributeAdvanced(5, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Cell), offset); + offset += 4 * @sizeOf(u8); + try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset); + offset += 1 * @sizeOf(u8); + try vbobind.attributeIAdvanced(7, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset); + try vbobind.enableAttribArray(0); + try vbobind.enableAttribArray(1); + try vbobind.enableAttribArray(2); + try vbobind.enableAttribArray(3); + try vbobind.enableAttribArray(4); + try vbobind.enableAttribArray(5); + try vbobind.enableAttribArray(6); + try vbobind.enableAttribArray(7); + try vbobind.attributeDivisor(0, 1); + try vbobind.attributeDivisor(1, 1); + try vbobind.attributeDivisor(2, 1); + try vbobind.attributeDivisor(3, 1); + try vbobind.attributeDivisor(4, 1); + try vbobind.attributeDivisor(5, 1); + try vbobind.attributeDivisor(6, 1); + try vbobind.attributeDivisor(7, 1); + + return .{ + .program = program, + .vao = vao, + .ebo = ebo, + .vbo = vbo, + }; +} + +pub fn bind(self: CellProgram) !Binding { + const program = try self.program.use(); + errdefer program.unbind(); + + const vao = try self.vao.bind(); + errdefer vao.unbind(); + + const ebo = try self.ebo.bind(.ElementArrayBuffer); + errdefer ebo.unbind(); + + const vbo = try self.vbo.bind(.ArrayBuffer); + errdefer vbo.unbind(); + + return .{ + .program = program, + .vao = vao, + .ebo = ebo, + .vbo = vbo, + }; +} + +pub fn deinit(self: CellProgram) void { + self.vbo.destroy(); + self.ebo.destroy(); + self.vao.destroy(); + self.program.destroy(); +} + +pub const Binding = struct { + program: gl.Program.Binding, + vao: gl.VertexArray.Binding, + ebo: gl.Buffer.Binding, + vbo: gl.Buffer.Binding, + + pub fn unbind(self: Binding) void { + self.vbo.unbind(); + self.ebo.unbind(); + self.vao.unbind(); + self.program.unbind(); + } +}; diff --git a/src/renderer/opengl/Program.zig b/src/renderer/opengl/Program.zig deleted file mode 100644 index d266bd226..000000000 --- a/src/renderer/opengl/Program.zig +++ /dev/null @@ -1,128 +0,0 @@ -const Program = @This(); - -const std = @import("std"); -const assert = std.debug.assert; -const log = std.log.scoped(.opengl); - -const c = @import("c.zig"); -const Shader = @import("Shader.zig"); -const errors = @import("errors.zig"); -const glad = @import("glad.zig"); - -id: c.GLuint, - -const Binding = struct { - pub inline fn unbind(_: Binding) void { - glad.context.UseProgram.?(0); - } -}; - -pub inline fn create() !Program { - const id = glad.context.CreateProgram.?(); - if (id == 0) try errors.mustError(); - - log.debug("program created id={}", .{id}); - return Program{ .id = id }; -} - -/// Create a program from a vertex and fragment shader source. This will -/// compile and link the vertex and fragment shader. -pub inline fn createVF(vsrc: [:0]const u8, fsrc: [:0]const u8) !Program { - const vs = try Shader.create(c.GL_VERTEX_SHADER); - try vs.setSourceAndCompile(vsrc); - defer vs.destroy(); - - const fs = try Shader.create(c.GL_FRAGMENT_SHADER); - try fs.setSourceAndCompile(fsrc); - defer fs.destroy(); - - const p = try create(); - try p.attachShader(vs); - try p.attachShader(fs); - try p.link(); - - return p; -} - -pub inline fn attachShader(p: Program, s: Shader) !void { - glad.context.AttachShader.?(p.id, s.id); - try errors.getError(); -} - -pub inline fn link(p: Program) !void { - glad.context.LinkProgram.?(p.id); - - // Check if linking succeeded - var success: c_int = undefined; - glad.context.GetProgramiv.?(p.id, c.GL_LINK_STATUS, &success); - if (success == c.GL_TRUE) { - log.debug("program linked id={}", .{p.id}); - return; - } - - log.err("program link failure id={} message={s}", .{ - p.id, - std.mem.sliceTo(&p.getInfoLog(), 0), - }); - return error.CompileFailed; -} - -pub inline fn use(p: Program) !Binding { - glad.context.UseProgram.?(p.id); - try errors.getError(); - return Binding{}; -} - -/// Requires the program is currently in use. -pub inline fn setUniform( - p: Program, - n: [:0]const u8, - value: anytype, -) !void { - const loc = glad.context.GetUniformLocation.?( - p.id, - @ptrCast(n.ptr), - ); - if (loc < 0) { - return error.UniformNameInvalid; - } - try errors.getError(); - - // Perform the correct call depending on the type of the value. - switch (@TypeOf(value)) { - comptime_int => glad.context.Uniform1i.?(loc, value), - f32 => glad.context.Uniform1f.?(loc, value), - @Vector(2, f32) => glad.context.Uniform2f.?(loc, value[0], value[1]), - @Vector(3, f32) => glad.context.Uniform3f.?(loc, value[0], value[1], value[2]), - @Vector(4, f32) => glad.context.Uniform4f.?(loc, value[0], value[1], value[2], value[3]), - [4]@Vector(4, f32) => glad.context.UniformMatrix4fv.?( - loc, - 1, - c.GL_FALSE, - @ptrCast(&value), - ), - else => { - log.warn("unsupported uniform type {}", .{@TypeOf(value)}); - unreachable; - }, - } - try errors.getError(); -} - -/// getInfoLog returns the info log for this program. This attempts to -/// keep the log fully stack allocated and is therefore limited to a max -/// amount of elements. -// -// NOTE(mitchellh): we can add a dynamic version that uses an allocator -// if we ever need it. -pub inline fn getInfoLog(s: Program) [512]u8 { - var msg: [512]u8 = undefined; - glad.context.GetProgramInfoLog.?(s.id, msg.len, null, &msg); - return msg; -} - -pub inline fn destroy(p: Program) void { - assert(p.id != 0); - glad.context.DeleteProgram.?(p.id); - log.debug("program destroyed id={}", .{p.id}); -} diff --git a/src/renderer/opengl/Shader.zig b/src/renderer/opengl/Shader.zig deleted file mode 100644 index beaae9e94..000000000 --- a/src/renderer/opengl/Shader.zig +++ /dev/null @@ -1,56 +0,0 @@ -const Shader = @This(); - -const std = @import("std"); -const assert = std.debug.assert; -const log = std.log.scoped(.opengl); - -const c = @import("c.zig"); -const errors = @import("errors.zig"); -const glad = @import("glad.zig"); - -id: c.GLuint, - -pub inline fn create(typ: c.GLenum) errors.Error!Shader { - const id = glad.context.CreateShader.?(typ); - if (id == 0) { - try errors.mustError(); - unreachable; - } - - log.debug("shader created id={}", .{id}); - return Shader{ .id = id }; -} - -/// Set the source and compile a shader. -pub inline fn setSourceAndCompile(s: Shader, source: [:0]const u8) !void { - glad.context.ShaderSource.?(s.id, 1, &@as([*c]const u8, @ptrCast(source)), null); - glad.context.CompileShader.?(s.id); - - // Check if compilation succeeded - var success: c_int = undefined; - glad.context.GetShaderiv.?(s.id, c.GL_COMPILE_STATUS, &success); - if (success == c.GL_TRUE) return; - log.err("shader compilation failure id={} message={s}", .{ - s.id, - std.mem.sliceTo(&s.getInfoLog(), 0), - }); - return error.CompileFailed; -} - -/// getInfoLog returns the info log for this shader. This attempts to -/// keep the log fully stack allocated and is therefore limited to a max -/// amount of elements. -// -// NOTE(mitchellh): we can add a dynamic version that uses an allocator -// if we ever need it. -pub inline fn getInfoLog(s: Shader) [512]u8 { - var msg: [512]u8 = undefined; - glad.context.GetShaderInfoLog.?(s.id, msg.len, null, &msg); - return msg; -} - -pub inline fn destroy(s: Shader) void { - assert(s.id != 0); - glad.context.DeleteShader.?(s.id); - log.debug("shader destroyed id={}", .{s.id}); -} diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig deleted file mode 100644 index 91a65b565..000000000 --- a/src/renderer/opengl/Texture.zig +++ /dev/null @@ -1,163 +0,0 @@ -const Texture = @This(); - -const std = @import("std"); -const c = @import("c.zig"); -const errors = @import("errors.zig"); -const glad = @import("glad.zig"); - -id: c.GLuint, - -pub inline fn active(target: c.GLenum) !void { - glad.context.ActiveTexture.?(target); - try errors.getError(); -} - -/// Enun for possible texture binding targets. -pub const Target = enum(c_uint) { - @"1D" = c.GL_TEXTURE_1D, - @"2D" = c.GL_TEXTURE_2D, - @"3D" = c.GL_TEXTURE_3D, - @"1DArray" = c.GL_TEXTURE_1D_ARRAY, - @"2DArray" = c.GL_TEXTURE_2D_ARRAY, - Rectangle = c.GL_TEXTURE_RECTANGLE, - CubeMap = c.GL_TEXTURE_CUBE_MAP, - Buffer = c.GL_TEXTURE_BUFFER, - @"2DMultisample" = c.GL_TEXTURE_2D_MULTISAMPLE, - @"2DMultisampleArray" = c.GL_TEXTURE_2D_MULTISAMPLE_ARRAY, -}; - -/// Enum for possible texture parameters. -pub const Parameter = enum(c_uint) { - BaseLevel = c.GL_TEXTURE_BASE_LEVEL, - CompareFunc = c.GL_TEXTURE_COMPARE_FUNC, - CompareMode = c.GL_TEXTURE_COMPARE_MODE, - LodBias = c.GL_TEXTURE_LOD_BIAS, - MinFilter = c.GL_TEXTURE_MIN_FILTER, - MagFilter = c.GL_TEXTURE_MAG_FILTER, - MinLod = c.GL_TEXTURE_MIN_LOD, - MaxLod = c.GL_TEXTURE_MAX_LOD, - MaxLevel = c.GL_TEXTURE_MAX_LEVEL, - SwizzleR = c.GL_TEXTURE_SWIZZLE_R, - SwizzleG = c.GL_TEXTURE_SWIZZLE_G, - SwizzleB = c.GL_TEXTURE_SWIZZLE_B, - SwizzleA = c.GL_TEXTURE_SWIZZLE_A, - WrapS = c.GL_TEXTURE_WRAP_S, - WrapT = c.GL_TEXTURE_WRAP_T, - WrapR = c.GL_TEXTURE_WRAP_R, -}; - -/// Internal format enum for texture images. -pub const InternalFormat = enum(c_int) { - Red = c.GL_RED, - RGBA = c.GL_RGBA, - - // There are so many more that I haven't filled in. - _, -}; - -/// Format for texture images -pub const Format = enum(c_uint) { - Red = c.GL_RED, - BGRA = c.GL_BGRA, - - // There are so many more that I haven't filled in. - _, -}; - -/// Data type for texture images. -pub const DataType = enum(c_uint) { - UnsignedByte = c.GL_UNSIGNED_BYTE, - - // There are so many more that I haven't filled in. - _, -}; - -pub const Binding = struct { - target: Target, - - pub inline fn unbind(b: *Binding) void { - glad.context.BindTexture.?(@intFromEnum(b.target), 0); - b.* = undefined; - } - - pub fn generateMipmap(b: Binding) void { - glad.context.GenerateMipmap.?(@intFromEnum(b.target)); - } - - pub fn parameter(b: Binding, name: Parameter, value: anytype) !void { - switch (@TypeOf(value)) { - c.GLint => glad.context.TexParameteri.?( - @intFromEnum(b.target), - @intFromEnum(name), - value, - ), - else => unreachable, - } - } - - pub fn image2D( - b: Binding, - level: c.GLint, - internal_format: InternalFormat, - width: c.GLsizei, - height: c.GLsizei, - border: c.GLint, - format: Format, - typ: DataType, - data: ?*const anyopaque, - ) !void { - glad.context.TexImage2D.?( - @intFromEnum(b.target), - level, - @intFromEnum(internal_format), - width, - height, - border, - @intFromEnum(format), - @intFromEnum(typ), - data, - ); - } - - pub fn subImage2D( - b: Binding, - level: c.GLint, - xoffset: c.GLint, - yoffset: c.GLint, - width: c.GLsizei, - height: c.GLsizei, - format: Format, - typ: DataType, - data: ?*const anyopaque, - ) !void { - glad.context.TexSubImage2D.?( - @intFromEnum(b.target), - level, - xoffset, - yoffset, - width, - height, - @intFromEnum(format), - @intFromEnum(typ), - data, - ); - } -}; - -/// Create a single texture. -pub inline fn create() !Texture { - var id: c.GLuint = undefined; - glad.context.GenTextures.?(1, &id); - return Texture{ .id = id }; -} - -/// glBindTexture -pub inline fn bind(v: Texture, target: Target) !Binding { - glad.context.BindTexture.?(@intFromEnum(target), v.id); - try errors.getError(); - return Binding{ .target = target }; -} - -pub inline fn destroy(v: Texture) void { - glad.context.DeleteTextures.?(1, &v.id); -} diff --git a/src/renderer/opengl/VertexArray.zig b/src/renderer/opengl/VertexArray.zig deleted file mode 100644 index b86794042..000000000 --- a/src/renderer/opengl/VertexArray.zig +++ /dev/null @@ -1,29 +0,0 @@ -const VertexArray = @This(); - -const c = @import("c.zig"); -const glad = @import("glad.zig"); -const errors = @import("errors.zig"); - -id: c.GLuint, - -/// Create a single vertex array object. -pub inline fn create() !VertexArray { - var vao: c.GLuint = undefined; - glad.context.GenVertexArrays.?(1, &vao); - return VertexArray{ .id = vao }; -} - -// Unbind any active vertex array. -pub inline fn unbind() !void { - glad.context.BindVertexArray.?(0); -} - -/// glBindVertexArray -pub inline fn bind(v: VertexArray) !void { - glad.context.BindVertexArray.?(v.id); - try errors.getError(); -} - -pub inline fn destroy(v: VertexArray) void { - glad.context.DeleteVertexArrays.?(1, &v.id); -} diff --git a/src/renderer/opengl/c.zig b/src/renderer/opengl/c.zig deleted file mode 100644 index 8f4a0f22f..000000000 --- a/src/renderer/opengl/c.zig +++ /dev/null @@ -1,3 +0,0 @@ -pub usingnamespace @cImport({ - @cInclude("glad/gl.h"); -}); diff --git a/src/renderer/opengl/draw.zig b/src/renderer/opengl/draw.zig deleted file mode 100644 index ea6b63103..000000000 --- a/src/renderer/opengl/draw.zig +++ /dev/null @@ -1,59 +0,0 @@ -const c = @import("c.zig"); -const errors = @import("errors.zig"); -const glad = @import("glad.zig"); - -pub fn clearColor(r: f32, g: f32, b: f32, a: f32) void { - glad.context.ClearColor.?(r, g, b, a); -} - -pub fn clear(mask: c.GLbitfield) void { - glad.context.Clear.?(mask); -} - -pub fn drawArrays(mode: c.GLenum, first: c.GLint, count: c.GLsizei) !void { - glad.context.DrawArrays.?(mode, first, count); - try errors.getError(); -} - -pub fn drawElements(mode: c.GLenum, count: c.GLsizei, typ: c.GLenum, offset: usize) !void { - const offsetPtr = if (offset == 0) null else @as(*const anyopaque, @ptrFromInt(offset)); - glad.context.DrawElements.?(mode, count, typ, offsetPtr); - try errors.getError(); -} - -pub fn drawElementsInstanced( - mode: c.GLenum, - count: c.GLsizei, - typ: c.GLenum, - primcount: usize, -) !void { - glad.context.DrawElementsInstanced.?(mode, count, typ, null, @intCast(primcount)); - try errors.getError(); -} - -pub fn enable(cap: c.GLenum) !void { - glad.context.Enable.?(cap); - try errors.getError(); -} - -pub fn frontFace(mode: c.GLenum) !void { - glad.context.FrontFace.?(mode); - try errors.getError(); -} - -pub fn blendFunc(sfactor: c.GLenum, dfactor: c.GLenum) !void { - glad.context.BlendFunc.?(sfactor, dfactor); - try errors.getError(); -} - -pub fn viewport(x: c.GLint, y: c.GLint, width: c.GLsizei, height: c.GLsizei) !void { - glad.context.Viewport.?(x, y, width, height); -} - -pub fn pixelStore(mode: c.GLenum, value: anytype) !void { - switch (@typeInfo(@TypeOf(value))) { - .ComptimeInt, .Int => glad.context.PixelStorei.?(mode, value), - else => unreachable, - } - try errors.getError(); -} diff --git a/src/renderer/opengl/errors.zig b/src/renderer/opengl/errors.zig deleted file mode 100644 index 86639a53a..000000000 --- a/src/renderer/opengl/errors.zig +++ /dev/null @@ -1,33 +0,0 @@ -const std = @import("std"); -const c = @import("c.zig"); -const glad = @import("glad.zig"); - -pub const Error = error{ - InvalidEnum, - InvalidValue, - InvalidOperation, - InvalidFramebufferOperation, - OutOfMemory, - - Unknown, -}; - -/// getError returns the error (if any) from the last OpenGL operation. -pub fn getError() Error!void { - return switch (glad.context.GetError.?()) { - c.GL_NO_ERROR => {}, - c.GL_INVALID_ENUM => Error.InvalidEnum, - c.GL_INVALID_VALUE => Error.InvalidValue, - c.GL_INVALID_OPERATION => Error.InvalidOperation, - c.GL_INVALID_FRAMEBUFFER_OPERATION => Error.InvalidFramebufferOperation, - c.GL_OUT_OF_MEMORY => Error.OutOfMemory, - else => Error.Unknown, - }; -} - -/// mustError just calls getError but always results in an error being returned. -/// If getError has no error, then Unknown is returned. -pub fn mustError() Error!void { - try getError(); - return Error.Unknown; -} diff --git a/src/renderer/opengl/extensions.zig b/src/renderer/opengl/extensions.zig deleted file mode 100644 index ca8a4973d..000000000 --- a/src/renderer/opengl/extensions.zig +++ /dev/null @@ -1,32 +0,0 @@ -const std = @import("std"); -const c = @import("c.zig"); -const errors = @import("errors.zig"); -const glad = @import("glad.zig"); - -/// Returns the number of extensions. -pub fn len() !u32 { - var n: c.GLint = undefined; - glad.context.GetIntegerv.?(c.GL_NUM_EXTENSIONS, &n); - try errors.getError(); - return @intCast(n); -} - -/// Returns an iterator for the extensions. -pub fn iterator() !Iterator { - return Iterator{ .len = try len() }; -} - -/// Iterator for the available extensions. -pub const Iterator = struct { - /// The total number of extensions. - len: c.GLuint = 0, - i: c.GLuint = 0, - - pub fn next(self: *Iterator) !?[]const u8 { - if (self.i >= self.len) return null; - const res = glad.context.GetStringi.?(c.GL_EXTENSIONS, self.i); - try errors.getError(); - self.i += 1; - return std.mem.sliceTo(res, 0); - } -}; diff --git a/src/renderer/opengl/glad.zig b/src/renderer/opengl/glad.zig deleted file mode 100644 index 4ee85c549..000000000 --- a/src/renderer/opengl/glad.zig +++ /dev/null @@ -1,45 +0,0 @@ -const std = @import("std"); -const c = @import("c.zig"); - -pub const Context = c.GladGLContext; - -/// This is the current context. Set this var manually prior to calling -/// any of this package's functions. I know its nasty to have a global but -/// this makes it match OpenGL API styles where it also operates on a -/// threadlocal global. -pub threadlocal var context: Context = undefined; - -/// Initialize Glad. This is guaranteed to succeed if no errors are returned. -/// The getProcAddress param is an anytype so that we can accept multiple -/// forms of the function depending on what we're interfacing with. -pub fn load(getProcAddress: anytype) !c_int { - const GlProc = *const fn () callconv(.C) void; - const GlfwFn = *const fn ([*:0]const u8) callconv(.C) ?GlProc; - - const res = switch (@TypeOf(getProcAddress)) { - // glfw - GlfwFn => c.gladLoadGLContext(&context, @ptrCast(getProcAddress)), - - // null proc address means that we are just loading the globally - // pointed gl functions - @TypeOf(null) => c.gladLoaderLoadGLContext(&context), - - // try as-is. If this introduces a compiler error, then add a new case. - else => c.gladLoadGLContext(&context, getProcAddress), - }; - if (res == 0) return error.GLInitFailed; - return res; -} - -pub fn unload() void { - c.gladLoaderUnloadGLContext(&context); - context = undefined; -} - -pub fn versionMajor(res: c_uint) c_uint { - return c.GLAD_VERSION_MAJOR(res); -} - -pub fn versionMinor(res: c_uint) c_uint { - return c.GLAD_VERSION_MINOR(res); -} diff --git a/src/renderer/opengl/main.zig b/src/renderer/opengl/main.zig deleted file mode 100644 index 79d32acea..000000000 --- a/src/renderer/opengl/main.zig +++ /dev/null @@ -1,23 +0,0 @@ -//! OpenGL bindings. -//! -//! These are purpose-built for usage within this program. While they closely -//! align with the OpenGL C APIs, they aren't meant to be general purpose, -//! they aren't meant to have 100% API coverage, and they aren't meant to -//! be hyper-performant. -//! -//! For performance-intensive or unsupported aspects of OpenGL, the C -//! API is exposed via the `c` constant. -//! -//! WARNING: Lots of performance improvements that we can make with Zig -//! comptime help. I'm deferring this until later but have some fun ideas. - -pub const c = @import("c.zig"); -pub const glad = @import("glad.zig"); -pub usingnamespace @import("draw.zig"); - -pub const ext = @import("extensions.zig"); -pub const Buffer = @import("Buffer.zig"); -pub const Program = @import("Program.zig"); -pub const Shader = @import("Shader.zig"); -pub const Texture = @import("Texture.zig"); -pub const VertexArray = @import("VertexArray.zig"); diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 90541fda9..5f7dc402e 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -198,13 +198,21 @@ pub fn mslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 { /// Convert SPIR-V binary to GLSL.. pub fn glslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 { + // Our minimum version for shadertoy shaders is OpenGL 4.2 because + // Spirv-Cross generates binding locations for uniforms which is + // only supported in OpenGL 4.2 and above. + // + // If we can figure out a way to NOT do this then we can lower this + // version. + const GLSL_VERSION = 420; + const c = spvcross.c; return try spvCross(alloc, c.SPVC_BACKEND_GLSL, spv, (struct { fn setOptions(options: c.spvc_compiler_options) error{SpvcFailed}!void { if (c.spvc_compiler_options_set_uint( options, c.SPVC_COMPILER_OPTION_GLSL_VERSION, - 430, + GLSL_VERSION, ) != c.SPVC_SUCCESS) { return error.SpvcFailed; } From cc630f10ac9f860effb9f20fc52c5b028d3d7619 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 08:54:28 -0800 Subject: [PATCH 34/55] renderer/opengl: only one GPUCell --- src/renderer/OpenGL.zig | 70 ++++------------------------- src/renderer/opengl/CellProgram.zig | 4 +- 2 files changed, 10 insertions(+), 64 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 1d9bf6b81..3abf277e2 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -49,8 +49,8 @@ screen_size: ?renderer.ScreenSize, /// The current set of cells to render. Each set of cells goes into /// a separate shader call. -cells_bg: std.ArrayListUnmanaged(GPUCell), -cells: std.ArrayListUnmanaged(GPUCell), +cells_bg: std.ArrayListUnmanaged(CellProgram.Cell), +cells: std.ArrayListUnmanaged(CellProgram.Cell), /// The size of the cells list that was sent to the GPU. This is used /// to detect when the cells array was reallocated/resized and handle that @@ -172,60 +172,6 @@ const SetFontSize = struct { } }; -/// The raw structure that maps directly to the buffer sent to the vertex shader. -/// This must be "extern" so that the field order is not reordered by the -/// Zig compiler. -const GPUCell = extern struct { - /// vec2 grid_coord - grid_col: u16, - grid_row: u16, - - /// vec2 glyph_pos - glyph_x: u32 = 0, - glyph_y: u32 = 0, - - /// vec2 glyph_size - glyph_width: u32 = 0, - glyph_height: u32 = 0, - - /// vec2 glyph_size - glyph_offset_x: i32 = 0, - glyph_offset_y: i32 = 0, - - /// vec4 fg_color_in - fg_r: u8, - fg_g: u8, - fg_b: u8, - fg_a: u8, - - /// vec4 bg_color_in - bg_r: u8, - bg_g: u8, - bg_b: u8, - bg_a: u8, - - /// uint mode - mode: GPUCellMode, - - /// The width in grid cells that a rendering takes. - grid_width: u8, -}; - -const GPUCellMode = enum(u8) { - bg = 1, - fg = 2, - fg_color = 7, - strikethrough = 8, - - // Non-exhaustive because masks change it - _, - - /// Apply a mask to the mode. - pub fn mask(self: GPUCellMode, m: GPUCellMode) GPUCellMode { - return @enumFromInt(@intFromEnum(self) | @intFromEnum(m)); - } -}; - /// The configuration for this renderer that is derived from the main /// configuration. This must be exported so that we don't need to /// pass around Config pointers which makes memory management a pain. @@ -738,7 +684,7 @@ pub fn rebuildCells( // This is the cell that has [mode == .fg] and is underneath our cursor. // We keep track of it so that we can invert the colors so the character // remains visible. - var cursor_cell: ?GPUCell = null; + var cursor_cell: ?CellProgram.Cell = null; // Build each cell var rowIter = screen.rowIterator(.viewport); @@ -980,7 +926,7 @@ fn addCursor( self: *OpenGL, screen: *terminal.Screen, cursor_style: renderer.CursorStyle, -) ?*const GPUCell { +) ?*const CellProgram.Cell { // Add the cursor. We render the cursor over the wide character if // we're on the wide characer tail. const wide, const x = cell: { @@ -1211,7 +1157,7 @@ pub fn updateCell( // If we're rendering a color font, we use the color atlas const presentation = try self.font_group.group.presentationFromIndex(shaper_run.font_index); - const mode: GPUCellMode = switch (presentation) { + const mode: CellProgram.CellMode = switch (presentation) { .text => .fg, .emoji => .fg_color, }; @@ -1515,7 +1461,7 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { fn drawCells( self: *OpenGL, binding: gl.Buffer.Binding, - cells: std.ArrayListUnmanaged(GPUCell), + cells: std.ArrayListUnmanaged(CellProgram.Cell), ) !void { // If we have no cells to render, then we render nothing. if (cells.items.len == 0) return; @@ -1532,7 +1478,7 @@ fn drawCells( }); try binding.setDataNullManual( - @sizeOf(GPUCell) * cells.capacity, + @sizeOf(CellProgram.Cell) * cells.capacity, .StaticDraw, ); @@ -1544,7 +1490,7 @@ fn drawCells( if (self.gl_cells_written < cells.items.len) { const data = cells.items[self.gl_cells_written..]; // log.info("sending {} cells to GPU", .{data.len}); - try binding.setSubData(self.gl_cells_written * @sizeOf(GPUCell), data); + try binding.setSubData(self.gl_cells_written * @sizeOf(CellProgram.Cell), data); self.gl_cells_written += data.len; assert(data.len > 0); diff --git a/src/renderer/opengl/CellProgram.zig b/src/renderer/opengl/CellProgram.zig index 2f3ce3742..8e9d81556 100644 --- a/src/renderer/opengl/CellProgram.zig +++ b/src/renderer/opengl/CellProgram.zig @@ -12,7 +12,7 @@ vbo: gl.Buffer, /// The raw structure that maps directly to the buffer sent to the vertex shader. /// This must be "extern" so that the field order is not reordered by the /// Zig compiler. -const Cell = extern struct { +pub const Cell = extern struct { /// vec2 grid_coord grid_col: u16, grid_row: u16, @@ -48,7 +48,7 @@ const Cell = extern struct { grid_width: u8, }; -const CellMode = enum(u8) { +pub const CellMode = enum(u8) { bg = 1, fg = 2, fg_color = 7, From 3502db0f5f2553a875d22681f4395f6a4834c9c8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 09:07:42 -0800 Subject: [PATCH 35/55] renderer/opengl: start custom program work --- src/renderer/OpenGL.zig | 24 ++++++++++-------- src/renderer/opengl/CellProgram.zig | 1 + src/renderer/opengl/CustomProgram.zig | 36 +++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 src/renderer/opengl/CustomProgram.zig diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 3abf277e2..dd8e42dda 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -22,6 +22,7 @@ const math = @import("../math.zig"); const Surface = @import("../Surface.zig"); const CellProgram = @import("opengl/CellProgram.zig"); +const CustomProgram = @import("opengl/CustomProgram.zig"); const log = std.log.scoped(.grid); @@ -296,7 +297,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { pub fn deinit(self: *OpenGL) void { self.font_shaper.deinit(); - if (self.gl_state) |*v| v.deinit(); + if (self.gl_state) |*v| v.deinit(self.alloc); self.cells.deinit(self.alloc); self.cells_bg.deinit(self.alloc); @@ -370,7 +371,7 @@ pub fn displayUnrealized(self: *OpenGL) void { defer if (single_threaded_draw) self.draw_mutex.unlock(); if (self.gl_state) |*v| { - v.deinit(); + v.deinit(self.alloc); self.gl_state = null; } } @@ -392,7 +393,7 @@ pub fn displayRealize(self: *OpenGL) !void { errdefer gl_state.deinit(); // Unrealize if we have to - if (self.gl_state) |*v| v.deinit(); + if (self.gl_state) |*v| v.deinit(self.alloc); // Set our new state self.gl_state = gl_state; @@ -1512,6 +1513,7 @@ const GLState = struct { cell_program: CellProgram, texture: gl.Texture, texture_color: gl.Texture, + custom_programs: []const CustomProgram, pub fn init( alloc: Allocator, @@ -1532,12 +1534,11 @@ const GLState = struct { break :err &.{}; }; - if (custom_shaders.len > 0) { - const cp = try gl.Program.createVF( - @embedFile("shaders/custom.v.glsl"), - custom_shaders[0], - ); - _ = cp; + // Create our custom programs + const custom_programs = try CustomProgram.createList(alloc, custom_shaders); + errdefer { + for (custom_programs) |p| p.deinit(); + alloc.free(custom_programs); } // Blending for text. We use GL_ONE here because we should be using @@ -1595,12 +1596,15 @@ const GLState = struct { return .{ .cell_program = cell_program, + .custom_programs = custom_programs, .texture = tex, .texture_color = tex_color, }; } - pub fn deinit(self: *GLState) void { + pub fn deinit(self: *GLState, alloc: Allocator) void { + for (self.custom_programs) |p| p.deinit(); + alloc.free(self.custom_programs); self.texture.destroy(); self.texture_color.destroy(); self.cell_program.deinit(); diff --git a/src/renderer/opengl/CellProgram.zig b/src/renderer/opengl/CellProgram.zig index 8e9d81556..aa308d44a 100644 --- a/src/renderer/opengl/CellProgram.zig +++ b/src/renderer/opengl/CellProgram.zig @@ -69,6 +69,7 @@ pub fn init() !CellProgram { @embedFile("../shaders/cell.v.glsl"), @embedFile("../shaders/cell.f.glsl"), ); + errdefer program.destroy(); // Set our cell dimensions const pbind = try program.use(); diff --git a/src/renderer/opengl/CustomProgram.zig b/src/renderer/opengl/CustomProgram.zig new file mode 100644 index 000000000..97340dcc6 --- /dev/null +++ b/src/renderer/opengl/CustomProgram.zig @@ -0,0 +1,36 @@ +/// The OpenGL program for custom shaders. +const CustomProgram = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const gl = @import("opengl"); + +program: gl.Program, + +pub fn createList(alloc: Allocator, srcs: []const [:0]const u8) ![]const CustomProgram { + var programs = std.ArrayList(CustomProgram).init(alloc); + defer programs.deinit(); + errdefer for (programs.items) |program| program.deinit(); + + for (srcs) |src| { + try programs.append(try CustomProgram.init(src)); + } + + return try programs.toOwnedSlice(); +} + +pub fn init(src: [:0]const u8) !CustomProgram { + const program = try gl.Program.createVF( + @embedFile("../shaders/custom.v.glsl"), + src, + ); + errdefer program.destroy(); + + return .{ + .program = program, + }; +} + +pub fn deinit(self: CustomProgram) void { + self.program.destroy(); +} From 1fedc912f0a7e246a5253f97fc81e408f9e3c4dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 09:32:52 -0800 Subject: [PATCH 36/55] renderer/opengl: create ubos --- pkg/opengl/Buffer.zig | 97 ++++++++++++++++----------- src/renderer/OpenGL.zig | 2 +- src/renderer/opengl/CellProgram.zig | 10 +-- src/renderer/opengl/CustomProgram.zig | 20 ++++++ 4 files changed, 85 insertions(+), 44 deletions(-) diff --git a/pkg/opengl/Buffer.zig b/pkg/opengl/Buffer.zig index c004dc3f4..fca94e0fc 100644 --- a/pkg/opengl/Buffer.zig +++ b/pkg/opengl/Buffer.zig @@ -7,26 +7,22 @@ const glad = @import("glad.zig"); id: c.GLuint, -/// Enum for possible binding targets. -pub const Target = enum(c_uint) { - ArrayBuffer = c.GL_ARRAY_BUFFER, - ElementArrayBuffer = c.GL_ELEMENT_ARRAY_BUFFER, - _, -}; +/// Create a single buffer. +pub fn create() !Buffer { + var vbo: c.GLuint = undefined; + glad.context.GenBuffers.?(1, &vbo); + return Buffer{ .id = vbo }; +} -/// Enum for possible buffer usages. -pub const Usage = enum(c_uint) { - StreamDraw = c.GL_STREAM_DRAW, - StreamRead = c.GL_STREAM_READ, - StreamCopy = c.GL_STREAM_COPY, - StaticDraw = c.GL_STATIC_DRAW, - StaticRead = c.GL_STATIC_READ, - StaticCopy = c.GL_STATIC_COPY, - DynamicDraw = c.GL_DYNAMIC_DRAW, - DynamicRead = c.GL_DYNAMIC_READ, - DynamicCopy = c.GL_DYNAMIC_COPY, - _, -}; +/// glBindBuffer +pub fn bind(v: Buffer, target: Target) !Binding { + glad.context.BindBuffer.?(@intFromEnum(target), v.id); + return Binding{ .target = target }; +} + +pub fn destroy(v: Buffer) void { + glad.context.DeleteBuffers.?(1, &v.id); +} /// Binding is a bound buffer. By using this for functions that operate /// on bound buffers, you can easily defer unbinding and in safety-enabled @@ -42,7 +38,12 @@ pub const Binding = struct { usage: Usage, ) !void { const info = dataInfo(&data); - glad.context.BufferData.?(@intFromEnum(b.target), info.size, info.ptr, @intFromEnum(usage)); + glad.context.BufferData.?( + @intFromEnum(b.target), + info.size, + info.ptr, + @intFromEnum(usage), + ); try errors.getError(); } @@ -54,7 +55,12 @@ pub const Binding = struct { data: anytype, ) !void { const info = dataInfo(data); - glad.context.BufferSubData.?(@intFromEnum(b.target), @intCast(offset), info.size, info.ptr); + glad.context.BufferSubData.?( + @intFromEnum(b.target), + @intCast(offset), + info.size, + info.ptr, + ); try errors.getError(); } @@ -66,7 +72,12 @@ pub const Binding = struct { comptime T: type, usage: Usage, ) !void { - glad.context.BufferData.?(@intFromEnum(b.target), @sizeOf(T), null, @intFromEnum(usage)); + glad.context.BufferData.?( + @intFromEnum(b.target), + @sizeOf(T), + null, + @intFromEnum(usage), + ); try errors.getError(); } @@ -76,7 +87,12 @@ pub const Binding = struct { size: usize, usage: Usage, ) !void { - glad.context.BufferData.?(@intFromEnum(b.target), @intCast(size), null, @intFromEnum(usage)); + glad.context.BufferData.?( + @intFromEnum(b.target), + @intCast(size), + null, + @intFromEnum(usage), + ); try errors.getError(); } @@ -199,19 +215,24 @@ pub const Binding = struct { } }; -/// Create a single buffer. -pub fn create() !Buffer { - var vbo: c.GLuint = undefined; - glad.context.GenBuffers.?(1, &vbo); - return Buffer{ .id = vbo }; -} +/// Enum for possible binding targets. +pub const Target = enum(c_uint) { + array = c.GL_ARRAY_BUFFER, + element_array = c.GL_ELEMENT_ARRAY_BUFFER, + uniform = c.GL_UNIFORM_BUFFER, + _, +}; -/// glBindBuffer -pub fn bind(v: Buffer, target: Target) !Binding { - glad.context.BindBuffer.?(@intFromEnum(target), v.id); - return Binding{ .target = target }; -} - -pub fn destroy(v: Buffer) void { - glad.context.DeleteBuffers.?(1, &v.id); -} +/// Enum for possible buffer usages. +pub const Usage = enum(c_uint) { + stream_draw = c.GL_STREAM_DRAW, + stream_read = c.GL_STREAM_READ, + stream_copy = c.GL_STREAM_COPY, + static_draw = c.GL_STATIC_DRAW, + static_read = c.GL_STATIC_READ, + static_copy = c.GL_STATIC_COPY, + dynamic_draw = c.GL_DYNAMIC_DRAW, + dynamic_read = c.GL_DYNAMIC_READ, + dynamic_copy = c.GL_DYNAMIC_COPY, + _, +}; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index dd8e42dda..ffaabebd9 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1480,7 +1480,7 @@ fn drawCells( try binding.setDataNullManual( @sizeOf(CellProgram.Cell) * cells.capacity, - .StaticDraw, + .static_draw, ); self.gl_cells_size = cells.capacity; diff --git a/src/renderer/opengl/CellProgram.zig b/src/renderer/opengl/CellProgram.zig index aa308d44a..c72959ef8 100644 --- a/src/renderer/opengl/CellProgram.zig +++ b/src/renderer/opengl/CellProgram.zig @@ -88,17 +88,17 @@ pub fn init() !CellProgram { // Element buffer (EBO) const ebo = try gl.Buffer.create(); errdefer ebo.destroy(); - var ebobind = try ebo.bind(.ElementArrayBuffer); + var ebobind = try ebo.bind(.element_array); defer ebobind.unbind(); try ebobind.setData([6]u8{ 0, 1, 3, // Top-left triangle 1, 2, 3, // Bottom-right triangle - }, .StaticDraw); + }, .static_draw); // Vertex buffer (VBO) const vbo = try gl.Buffer.create(); errdefer vbo.destroy(); - var vbobind = try vbo.bind(.ArrayBuffer); + var vbobind = try vbo.bind(.array); defer vbobind.unbind(); var offset: usize = 0; try vbobind.attributeAdvanced(0, 2, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(Cell), offset); @@ -148,10 +148,10 @@ pub fn bind(self: CellProgram) !Binding { const vao = try self.vao.bind(); errdefer vao.unbind(); - const ebo = try self.ebo.bind(.ElementArrayBuffer); + const ebo = try self.ebo.bind(.element_array); errdefer ebo.unbind(); - const vbo = try self.vbo.bind(.ArrayBuffer); + const vbo = try self.vbo.bind(.array); errdefer vbo.unbind(); return .{ diff --git a/src/renderer/opengl/CustomProgram.zig b/src/renderer/opengl/CustomProgram.zig index 97340dcc6..f486cfd3e 100644 --- a/src/renderer/opengl/CustomProgram.zig +++ b/src/renderer/opengl/CustomProgram.zig @@ -7,6 +7,19 @@ const gl = @import("opengl"); program: gl.Program, +pub const Uniforms = extern struct { + resolution: [3]f32 align(16), + time: f32 align(4), + time_delta: f32 align(4), + frame_rate: f32 align(4), + frame: i32 align(4), + channel_time: [4][4]f32 align(16), + channel_resolution: [4][4]f32 align(16), + mouse: [4]f32 align(16), + date: [4]f32 align(16), + sample_rate: f32 align(4), +}; + pub fn createList(alloc: Allocator, srcs: []const [:0]const u8) ![]const CustomProgram { var programs = std.ArrayList(CustomProgram).init(alloc); defer programs.deinit(); @@ -26,6 +39,13 @@ pub fn init(src: [:0]const u8) !CustomProgram { ); errdefer program.destroy(); + // Create our uniform buffer that is shared across all custom shaders + const ubo = try gl.Buffer.create(); + errdefer ubo.destroy(); + var ubobind = try ubo.bind(.uniform); + defer ubobind.unbind(); + try ubobind.setDataNull(Uniforms, .static_draw); + return .{ .program = program, }; From aff5090362de714ab51a031d00f5dbc581adf43b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 09:43:49 -0800 Subject: [PATCH 37/55] renderer/opengl: simplifying the custom shader to get things working --- src/renderer/opengl/CustomProgram.zig | 38 ++++++++++++++++++++++++--- src/renderer/shaders/custom.v.glsl | 7 ++--- src/renderer/shaders/temp.f.glsl | 8 ++++++ 3 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 src/renderer/shaders/temp.f.glsl diff --git a/src/renderer/opengl/CustomProgram.zig b/src/renderer/opengl/CustomProgram.zig index f486cfd3e..72e1f006b 100644 --- a/src/renderer/opengl/CustomProgram.zig +++ b/src/renderer/opengl/CustomProgram.zig @@ -7,6 +7,12 @@ const gl = @import("opengl"); program: gl.Program, +/// This VAO is used for all custom shaders. It contains a single quad +/// by using an EBO. The vertex ID (gl_VertexID) can be used to determine the +/// position of the vertex. +vao: gl.VertexArray, +ebo: gl.Buffer, + pub const Uniforms = extern struct { resolution: [3]f32 align(16), time: f32 align(4), @@ -33,24 +39,48 @@ pub fn createList(alloc: Allocator, srcs: []const [:0]const u8) ![]const CustomP } pub fn init(src: [:0]const u8) !CustomProgram { + _ = src; const program = try gl.Program.createVF( @embedFile("../shaders/custom.v.glsl"), - src, + //src, + @embedFile("../shaders/temp.f.glsl"), ); errdefer program.destroy(); // Create our uniform buffer that is shared across all custom shaders const ubo = try gl.Buffer.create(); errdefer ubo.destroy(); - var ubobind = try ubo.bind(.uniform); - defer ubobind.unbind(); - try ubobind.setDataNull(Uniforms, .static_draw); + { + var ubobind = try ubo.bind(.uniform); + defer ubobind.unbind(); + try ubobind.setDataNull(Uniforms, .static_draw); + } + + // Setup our VAO for the custom shader. + const vao = try gl.VertexArray.create(); + errdefer vao.destroy(); + const vaobind = try vao.bind(); + defer vaobind.unbind(); + + // Element buffer (EBO) + const ebo = try gl.Buffer.create(); + errdefer ebo.destroy(); + var ebobind = try ebo.bind(.element_array); + defer ebobind.unbind(); + try ebobind.setData([6]u8{ + 0, 1, 3, // Top-left triangle + 1, 2, 3, // Bottom-right triangle + }, .static_draw); return .{ .program = program, + .vao = vao, + .ebo = ebo, }; } pub fn deinit(self: CustomProgram) void { + self.ebo.destroy(); + self.vao.destroy(); self.program.destroy(); } diff --git a/src/renderer/shaders/custom.v.glsl b/src/renderer/shaders/custom.v.glsl index ff6f60541..fc6366845 100644 --- a/src/renderer/shaders/custom.v.glsl +++ b/src/renderer/shaders/custom.v.glsl @@ -1,7 +1,8 @@ #version 330 core -layout (location = 0) in vec2 position; - void main(){ - gl_Position = vec4(position.x, position.y, 0.0f, 1.0f); + vec2 position; + position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.; + position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.; + gl_Position = vec4(position.xy, 0.0f, 1.0f); } diff --git a/src/renderer/shaders/temp.f.glsl b/src/renderer/shaders/temp.f.glsl new file mode 100644 index 000000000..cbc9de339 --- /dev/null +++ b/src/renderer/shaders/temp.f.glsl @@ -0,0 +1,8 @@ +#version 330 core + +layout(location = 0) out vec4 out_FragColor; + +void main() { + // read + out_FragColor = vec4(1.0, 0.0, 0.0, 1.0); +} From da600fee8f473ff2e5821c56dd55fef54e41adc6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 09:47:52 -0800 Subject: [PATCH 38/55] renderer/opengl: pull out cell program drawing to dedicated func --- src/renderer/OpenGL.zig | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index ffaabebd9..c0514ef30 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1407,6 +1407,22 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { defer if (single_threaded_draw) self.draw_mutex.unlock(); const gl_state = self.gl_state orelse return; + // Draw our terminal cells + try self.drawCellProgram(&gl_state); + + // Swap our window buffers + switch (apprt.runtime) { + apprt.glfw => surface.window.swapBuffers(), + apprt.gtk => {}, + else => @compileError("unsupported runtime"), + } +} + +/// Runs the cell program (shaders) to draw the terminal grid. +fn drawCellProgram( + self: *OpenGL, + gl_state: *const GLState, +) !void { // Try to flush our atlas, this will only do something if there // are changes to the atlas. try self.flushAtlas(); @@ -1443,15 +1459,9 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { self.deferred_font_size = null; } + // Draw our background, then draw the fg on top of it. try self.drawCells(bind.vbo, self.cells_bg); try self.drawCells(bind.vbo, self.cells); - - // Swap our window buffers - switch (apprt.runtime) { - apprt.glfw => surface.window.swapBuffers(), - apprt.gtk => {}, - else => @compileError("unsupported runtime"), - } } /// Loads some set of cell data into our buffer and issues a draw call. From 5fc91401f2a795b3223eb908360bc54aea53c9f6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 09:58:28 -0800 Subject: [PATCH 39/55] renderer/opengl: draw custom shaders, simplified --- src/renderer/OpenGL.zig | 17 ++++++++++++++++ src/renderer/opengl/CustomProgram.zig | 29 +++++++++++++++++++++++++++ src/renderer/shaders/custom.v.glsl | 4 ++-- src/renderer/shaders/temp.f.glsl | 8 ++++++-- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index c0514ef30..4a71c7b41 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1410,6 +1410,9 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { // Draw our terminal cells try self.drawCellProgram(&gl_state); + // Draw our custom shaders + try self.drawCustomPrograms(&gl_state); + // Swap our window buffers switch (apprt.runtime) { apprt.glfw => surface.window.swapBuffers(), @@ -1418,6 +1421,20 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { } } +fn drawCustomPrograms( + self: *OpenGL, + gl_state: *const GLState, +) !void { + _ = self; + + for (gl_state.custom_programs) |program| { + // Bind our cell program state, buffers + const bind = try program.bind(); + defer bind.unbind(); + try gl.drawElementsInstanced(gl.c.GL_TRIANGLES, 6, gl.c.GL_UNSIGNED_BYTE, 1); + } +} + /// Runs the cell program (shaders) to draw the terminal grid. fn drawCellProgram( self: *OpenGL, diff --git a/src/renderer/opengl/CustomProgram.zig b/src/renderer/opengl/CustomProgram.zig index 72e1f006b..e783463bf 100644 --- a/src/renderer/opengl/CustomProgram.zig +++ b/src/renderer/opengl/CustomProgram.zig @@ -84,3 +84,32 @@ pub fn deinit(self: CustomProgram) void { self.vao.destroy(); self.program.destroy(); } + +pub fn bind(self: CustomProgram) !Binding { + const program = try self.program.use(); + errdefer program.unbind(); + + const vao = try self.vao.bind(); + errdefer vao.unbind(); + + const ebo = try self.ebo.bind(.element_array); + errdefer ebo.unbind(); + + return .{ + .program = program, + .vao = vao, + .ebo = ebo, + }; +} + +pub const Binding = struct { + program: gl.Program.Binding, + vao: gl.VertexArray.Binding, + ebo: gl.Buffer.Binding, + + pub fn unbind(self: Binding) void { + self.ebo.unbind(); + self.vao.unbind(); + self.program.unbind(); + } +}; diff --git a/src/renderer/shaders/custom.v.glsl b/src/renderer/shaders/custom.v.glsl index fc6366845..653e1800e 100644 --- a/src/renderer/shaders/custom.v.glsl +++ b/src/renderer/shaders/custom.v.glsl @@ -2,7 +2,7 @@ void main(){ vec2 position; - position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.; - position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.; + position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? -1. : 1.; + position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 1. : -1.; gl_Position = vec4(position.xy, 0.0f, 1.0f); } diff --git a/src/renderer/shaders/temp.f.glsl b/src/renderer/shaders/temp.f.glsl index cbc9de339..632a2f73b 100644 --- a/src/renderer/shaders/temp.f.glsl +++ b/src/renderer/shaders/temp.f.glsl @@ -3,6 +3,10 @@ layout(location = 0) out vec4 out_FragColor; void main() { - // read - out_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + // red + //out_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + + // maze + vec4 I = gl_FragCoord; + out_FragColor = vec4(3)*modf(I*.1,I)[int(length(I)*1e4)&1]; } From 47971e7663567c98b622d7bf128ab9956f3b8402 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 15:20:13 -0800 Subject: [PATCH 40/55] renderer/opengl: setup uniform buffer objects for custom shaders --- pkg/opengl/Buffer.zig | 30 ++++++++---- pkg/opengl/Program.zig | 9 ++++ src/renderer/OpenGL.zig | 5 ++ src/renderer/opengl/CustomProgram.zig | 57 +++++++++++++++++----- src/renderer/shaders/shadertoy_prefix.glsl | 10 ++-- src/renderer/shaders/temp.f.glsl | 26 ++++++++-- 6 files changed, 105 insertions(+), 32 deletions(-) diff --git a/pkg/opengl/Buffer.zig b/pkg/opengl/Buffer.zig index fca94e0fc..a8ba099d3 100644 --- a/pkg/opengl/Buffer.zig +++ b/pkg/opengl/Buffer.zig @@ -15,21 +15,35 @@ pub fn create() !Buffer { } /// glBindBuffer -pub fn bind(v: Buffer, target: Target) !Binding { - glad.context.BindBuffer.?(@intFromEnum(target), v.id); - return Binding{ .target = target }; +pub fn bind(self: Buffer, target: Target) !Binding { + glad.context.BindBuffer.?(@intFromEnum(target), self.id); + return Binding{ .id = self.id, .target = target }; } -pub fn destroy(v: Buffer) void { - glad.context.DeleteBuffers.?(1, &v.id); +pub fn destroy(self: Buffer) void { + glad.context.DeleteBuffers.?(1, &self.id); +} + +pub fn bindBase(self: Buffer, target: Target, idx: c.GLuint) !void { + glad.context.BindBufferBase.?( + @intFromEnum(target), + idx, + self.id, + ); + try errors.getError(); } /// Binding is a bound buffer. By using this for functions that operate /// on bound buffers, you can easily defer unbinding and in safety-enabled /// modes verify that unbound buffers are never accessed. pub const Binding = struct { + id: c.GLuint, target: Target, + pub fn unbind(b: Binding) void { + glad.context.BindBuffer.?(@intFromEnum(b.target), 0); + } + /// Sets the data of this bound buffer. The data can be any array-like /// type. The size of the data is automatically determined based on the type. pub fn setData( @@ -103,7 +117,7 @@ pub const Binding = struct { return switch (@typeInfo(@TypeOf(data))) { .Pointer => |ptr| switch (ptr.size) { .One => .{ - .size = @sizeOf(ptr.child) * data.len, + .size = @sizeOf(ptr.child), .ptr = data, }, .Slice => .{ @@ -209,10 +223,6 @@ pub const Binding = struct { glad.context.VertexAttribIPointer.?(idx, size, typ, stride, offsetPtr); try errors.getError(); } - - pub fn unbind(b: Binding) void { - glad.context.BindBuffer.?(@intFromEnum(b.target), 0); - } }; /// Enum for possible binding targets. diff --git a/pkg/opengl/Program.zig b/pkg/opengl/Program.zig index e8a691f17..3a2f2036a 100644 --- a/pkg/opengl/Program.zig +++ b/pkg/opengl/Program.zig @@ -78,6 +78,15 @@ pub fn use(p: Program) !Binding { return .{}; } +pub fn uniformBlockBinding( + self: Program, + index: c.GLuint, + binding: c.GLuint, +) !void { + glad.context.UniformBlockBinding.?(self.id, index, binding); + try errors.getError(); +} + /// Requires the program is currently in use. pub fn setUniform( p: Program, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 4a71c7b41..c71508597 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1431,6 +1431,11 @@ fn drawCustomPrograms( // Bind our cell program state, buffers const bind = try program.bind(); defer bind.unbind(); + + // Sync the uniform data. + // TODO: only do this when the data has changed + try program.syncUniforms(); + try gl.drawElementsInstanced(gl.c.GL_TRIANGLES, 6, gl.c.GL_UNSIGNED_BYTE, 1); } } diff --git a/src/renderer/opengl/CustomProgram.zig b/src/renderer/opengl/CustomProgram.zig index e783463bf..2013e44fd 100644 --- a/src/renderer/opengl/CustomProgram.zig +++ b/src/renderer/opengl/CustomProgram.zig @@ -5,8 +5,22 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const gl = @import("opengl"); +/// The "INDEX" is the index into the global GL state and the +/// "BINDING" is the binding location in the shader. +const UNIFORM_INDEX: gl.c.GLuint = 0; +const UNIFORM_BINDING: gl.c.GLuint = 0; + +/// The uniform state. Whenever this is modified this should be +/// synced to the buffer. The draw/bind calls don't automatically +/// sync this so this should be done whenever the state is modified. +uniforms: Uniforms = .{}, + +/// The actual shader program. program: gl.Program, +/// The uniform buffer that is updated with our uniform data. +ubo: gl.Buffer, + /// This VAO is used for all custom shaders. It contains a single quad /// by using an EBO. The vertex ID (gl_VertexID) can be used to determine the /// position of the vertex. @@ -14,16 +28,16 @@ vao: gl.VertexArray, ebo: gl.Buffer, pub const Uniforms = extern struct { - resolution: [3]f32 align(16), - time: f32 align(4), - time_delta: f32 align(4), - frame_rate: f32 align(4), - frame: i32 align(4), - channel_time: [4][4]f32 align(16), - channel_resolution: [4][4]f32 align(16), - mouse: [4]f32 align(16), - date: [4]f32 align(16), - sample_rate: f32 align(4), + resolution: [3]f32 align(16) = .{ 0, 0, 0 }, + time: f32 align(4) = 1, + time_delta: f32 align(4) = 1, + frame_rate: f32 align(4) = 1, + frame: i32 align(4) = 1, + channel_time: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + channel_resolution: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + mouse: [4]f32 align(16) = .{ 0, 0, 0, 0 }, + date: [4]f32 align(16) = .{ 0, 0, 0, 0 }, + sample_rate: f32 align(4) = 1, }; pub fn createList(alloc: Allocator, srcs: []const [:0]const u8) ![]const CustomProgram { @@ -39,14 +53,16 @@ pub fn createList(alloc: Allocator, srcs: []const [:0]const u8) ![]const CustomP } pub fn init(src: [:0]const u8) !CustomProgram { - _ = src; const program = try gl.Program.createVF( @embedFile("../shaders/custom.v.glsl"), - //src, - @embedFile("../shaders/temp.f.glsl"), + src, + //@embedFile("../shaders/temp.f.glsl"), ); errdefer program.destroy(); + // Map our uniform buffer to the global GL state + try program.uniformBlockBinding(UNIFORM_INDEX, UNIFORM_BINDING); + // Create our uniform buffer that is shared across all custom shaders const ubo = try gl.Buffer.create(); errdefer ubo.destroy(); @@ -74,6 +90,7 @@ pub fn init(src: [:0]const u8) !CustomProgram { return .{ .program = program, + .ubo = ubo, .vao = vao, .ebo = ebo, }; @@ -85,7 +102,21 @@ pub fn deinit(self: CustomProgram) void { self.program.destroy(); } +pub fn syncUniforms(self: CustomProgram) !void { + var ubobind = try self.ubo.bind(.uniform); + defer ubobind.unbind(); + try ubobind.setData(self.uniforms, .static_draw); +} + pub fn bind(self: CustomProgram) !Binding { + // Move our uniform buffer into proper global index. Note that + // in theory we can do this globally once and never worry about + // it again. I don't think we're high-performance enough at all + // to worry about that and this makes it so you can just move + // around CustomProgram usage without worrying about clobbering + // the global state. + try self.ubo.bindBase(.uniform, UNIFORM_INDEX); + const program = try self.program.use(); errdefer program.unbind(); diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl index ff3749ff2..d62e19605 100644 --- a/src/renderer/shaders/shadertoy_prefix.glsl +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -13,10 +13,12 @@ layout(binding = 0) uniform Globals { uniform float iSampleRate; }; -layout(binding = 0) uniform sampler2D iChannel0; -layout(binding = 1) uniform sampler2D iChannel1; -layout(binding = 2) uniform sampler2D iChannel2; -layout(binding = 3) uniform sampler2D iChannel3; +layout(binding = 1) uniform sampler2D iChannel0; + +// These are unused currently by Ghostty: +// layout(binding = 1) uniform sampler2D iChannel1; +// layout(binding = 2) uniform sampler2D iChannel2; +// layout(binding = 3) uniform sampler2D iChannel3; layout(location = 0) in vec4 gl_FragCoord; layout(location = 0) out vec4 _fragColor; diff --git a/src/renderer/shaders/temp.f.glsl b/src/renderer/shaders/temp.f.glsl index 632a2f73b..b97b34549 100644 --- a/src/renderer/shaders/temp.f.glsl +++ b/src/renderer/shaders/temp.f.glsl @@ -1,12 +1,28 @@ -#version 330 core +#version 430 core -layout(location = 0) out vec4 out_FragColor; +layout(binding = 0, std140) uniform Globals +{ + vec3 iResolution; + float iTime; + float iTimeDelta; + float iFrameRate; + int iFrame; + float iChannelTime[4]; + vec3 iChannelResolution[4]; + vec4 iMouse; + vec4 iDate; + float iSampleRate; +} _89; + +layout(binding = 1) uniform sampler2D iChannel0; + +layout(location = 0) out vec4 _fragColor; void main() { // red - //out_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + _fragColor = vec4(_89.iSampleRate, 0.0, 0.0, 1.0); // maze - vec4 I = gl_FragCoord; - out_FragColor = vec4(3)*modf(I*.1,I)[int(length(I)*1e4)&1]; + //vec4 I = gl_FragCoord; + //_fragColor = vec4(3)*modf(I*.1,I)[int(length(I)*1e4)&1]; } From e0afa442c49013f26afd0b0470d42811d0be2944 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 16:48:34 -0800 Subject: [PATCH 41/55] renderer/opengl: better organization of custom shader state --- src/renderer/OpenGL.zig | 66 ++++++---- src/renderer/opengl/CustomProgram.zig | 146 ---------------------- src/renderer/opengl/custom.zig | 172 ++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 171 deletions(-) delete mode 100644 src/renderer/opengl/CustomProgram.zig create mode 100644 src/renderer/opengl/custom.zig diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index c71508597..076798630 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -22,7 +22,7 @@ const math = @import("../math.zig"); const Surface = @import("../Surface.zig"); const CellProgram = @import("opengl/CellProgram.zig"); -const CustomProgram = @import("opengl/CustomProgram.zig"); +const custom = @import("opengl/custom.zig"); const log = std.log.scoped(.grid); @@ -1427,16 +1427,29 @@ fn drawCustomPrograms( ) !void { _ = self; - for (gl_state.custom_programs) |program| { + // If we have no custom shaders then we do nothing. + const custom_state = gl_state.custom orelse return; + + // Bind our state that is global to all custom shaders + const custom_bind = try custom_state.bind(); + defer custom_bind.unbind(); + + // Sync the uniform data. + // TODO: only do this when the data has changed + try custom_state.syncUniforms(); + + // Go through each custom shader and draw it. + for (custom_state.programs) |program| { // Bind our cell program state, buffers const bind = try program.bind(); defer bind.unbind(); - // Sync the uniform data. - // TODO: only do this when the data has changed - try program.syncUniforms(); - - try gl.drawElementsInstanced(gl.c.GL_TRIANGLES, 6, gl.c.GL_UNSIGNED_BYTE, 1); + try gl.drawElementsInstanced( + gl.c.GL_TRIANGLES, + 6, + gl.c.GL_UNSIGNED_BYTE, + 1, + ); } } @@ -1545,7 +1558,7 @@ const GLState = struct { cell_program: CellProgram, texture: gl.Texture, texture_color: gl.Texture, - custom_programs: []const CustomProgram, + custom: ?custom.State, pub fn init( alloc: Allocator, @@ -1557,21 +1570,25 @@ const GLState = struct { const arena_alloc = arena.allocator(); // Load our custom shaders - const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( - arena_alloc, - config.custom_shaders.items, - .glsl, - ) catch |err| err: { - log.warn("error loading custom shaders err={}", .{err}); - break :err &.{}; - }; + const custom_state: ?custom.State = custom: { + const shaders: []const [:0]const u8 = shadertoy.loadFromFiles( + arena_alloc, + config.custom_shaders.items, + .glsl, + ) catch |err| err: { + log.warn("error loading custom shaders err={}", .{err}); + break :err &.{}; + }; + if (shaders.len == 0) break :custom null; - // Create our custom programs - const custom_programs = try CustomProgram.createList(alloc, custom_shaders); - errdefer { - for (custom_programs) |p| p.deinit(); - alloc.free(custom_programs); - } + break :custom custom.State.init( + alloc, + shaders, + ) catch |err| err: { + log.warn("error initializing custom shaders err={}", .{err}); + break :err null; + }; + }; // Blending for text. We use GL_ONE here because we should be using // premultiplied alpha for all our colors in our fragment shaders. @@ -1628,15 +1645,14 @@ const GLState = struct { return .{ .cell_program = cell_program, - .custom_programs = custom_programs, .texture = tex, .texture_color = tex_color, + .custom = custom_state, }; } pub fn deinit(self: *GLState, alloc: Allocator) void { - for (self.custom_programs) |p| p.deinit(); - alloc.free(self.custom_programs); + if (self.custom) |v| v.deinit(alloc); self.texture.destroy(); self.texture_color.destroy(); self.cell_program.deinit(); diff --git a/src/renderer/opengl/CustomProgram.zig b/src/renderer/opengl/CustomProgram.zig deleted file mode 100644 index 2013e44fd..000000000 --- a/src/renderer/opengl/CustomProgram.zig +++ /dev/null @@ -1,146 +0,0 @@ -/// The OpenGL program for custom shaders. -const CustomProgram = @This(); - -const std = @import("std"); -const Allocator = std.mem.Allocator; -const gl = @import("opengl"); - -/// The "INDEX" is the index into the global GL state and the -/// "BINDING" is the binding location in the shader. -const UNIFORM_INDEX: gl.c.GLuint = 0; -const UNIFORM_BINDING: gl.c.GLuint = 0; - -/// The uniform state. Whenever this is modified this should be -/// synced to the buffer. The draw/bind calls don't automatically -/// sync this so this should be done whenever the state is modified. -uniforms: Uniforms = .{}, - -/// The actual shader program. -program: gl.Program, - -/// The uniform buffer that is updated with our uniform data. -ubo: gl.Buffer, - -/// This VAO is used for all custom shaders. It contains a single quad -/// by using an EBO. The vertex ID (gl_VertexID) can be used to determine the -/// position of the vertex. -vao: gl.VertexArray, -ebo: gl.Buffer, - -pub const Uniforms = extern struct { - resolution: [3]f32 align(16) = .{ 0, 0, 0 }, - time: f32 align(4) = 1, - time_delta: f32 align(4) = 1, - frame_rate: f32 align(4) = 1, - frame: i32 align(4) = 1, - channel_time: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, - channel_resolution: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, - mouse: [4]f32 align(16) = .{ 0, 0, 0, 0 }, - date: [4]f32 align(16) = .{ 0, 0, 0, 0 }, - sample_rate: f32 align(4) = 1, -}; - -pub fn createList(alloc: Allocator, srcs: []const [:0]const u8) ![]const CustomProgram { - var programs = std.ArrayList(CustomProgram).init(alloc); - defer programs.deinit(); - errdefer for (programs.items) |program| program.deinit(); - - for (srcs) |src| { - try programs.append(try CustomProgram.init(src)); - } - - return try programs.toOwnedSlice(); -} - -pub fn init(src: [:0]const u8) !CustomProgram { - const program = try gl.Program.createVF( - @embedFile("../shaders/custom.v.glsl"), - src, - //@embedFile("../shaders/temp.f.glsl"), - ); - errdefer program.destroy(); - - // Map our uniform buffer to the global GL state - try program.uniformBlockBinding(UNIFORM_INDEX, UNIFORM_BINDING); - - // Create our uniform buffer that is shared across all custom shaders - const ubo = try gl.Buffer.create(); - errdefer ubo.destroy(); - { - var ubobind = try ubo.bind(.uniform); - defer ubobind.unbind(); - try ubobind.setDataNull(Uniforms, .static_draw); - } - - // Setup our VAO for the custom shader. - const vao = try gl.VertexArray.create(); - errdefer vao.destroy(); - const vaobind = try vao.bind(); - defer vaobind.unbind(); - - // Element buffer (EBO) - const ebo = try gl.Buffer.create(); - errdefer ebo.destroy(); - var ebobind = try ebo.bind(.element_array); - defer ebobind.unbind(); - try ebobind.setData([6]u8{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }, .static_draw); - - return .{ - .program = program, - .ubo = ubo, - .vao = vao, - .ebo = ebo, - }; -} - -pub fn deinit(self: CustomProgram) void { - self.ebo.destroy(); - self.vao.destroy(); - self.program.destroy(); -} - -pub fn syncUniforms(self: CustomProgram) !void { - var ubobind = try self.ubo.bind(.uniform); - defer ubobind.unbind(); - try ubobind.setData(self.uniforms, .static_draw); -} - -pub fn bind(self: CustomProgram) !Binding { - // Move our uniform buffer into proper global index. Note that - // in theory we can do this globally once and never worry about - // it again. I don't think we're high-performance enough at all - // to worry about that and this makes it so you can just move - // around CustomProgram usage without worrying about clobbering - // the global state. - try self.ubo.bindBase(.uniform, UNIFORM_INDEX); - - const program = try self.program.use(); - errdefer program.unbind(); - - const vao = try self.vao.bind(); - errdefer vao.unbind(); - - const ebo = try self.ebo.bind(.element_array); - errdefer ebo.unbind(); - - return .{ - .program = program, - .vao = vao, - .ebo = ebo, - }; -} - -pub const Binding = struct { - program: gl.Program.Binding, - vao: gl.VertexArray.Binding, - ebo: gl.Buffer.Binding, - - pub fn unbind(self: Binding) void { - self.ebo.unbind(); - self.vao.unbind(); - self.program.unbind(); - } -}; diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig new file mode 100644 index 000000000..7f1deb5b2 --- /dev/null +++ b/src/renderer/opengl/custom.zig @@ -0,0 +1,172 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const gl = @import("opengl"); + +/// The "INDEX" is the index into the global GL state and the +/// "BINDING" is the binding location in the shader. +const UNIFORM_INDEX: gl.c.GLuint = 0; +const UNIFORM_BINDING: gl.c.GLuint = 0; + +pub const Uniforms = extern struct { + resolution: [3]f32 align(16) = .{ 0, 0, 0 }, + time: f32 align(4) = 1, + time_delta: f32 align(4) = 1, + frame_rate: f32 align(4) = 1, + frame: i32 align(4) = 1, + channel_time: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + channel_resolution: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + mouse: [4]f32 align(16) = .{ 0, 0, 0, 0 }, + date: [4]f32 align(16) = .{ 0, 0, 0, 0 }, + sample_rate: f32 align(4) = 1, +}; + +/// The state associated with custom shaders. +pub const State = struct { + /// The uniform data + uniforms: Uniforms, + + /// The OpenGL buffers + ubo: gl.Buffer, + vao: gl.VertexArray, + ebo: gl.Buffer, + + /// The set of programs for the custom shaders. + programs: []const Program, + + /// The last time the frame was drawn. This is used to update + /// the time uniform. + last_frame_time: std.time.Instant, + + pub fn init( + alloc: Allocator, + srcs: []const [:0]const u8, + ) !State { + // Create our programs + var programs = std.ArrayList(Program).init(alloc); + defer programs.deinit(); + errdefer for (programs.items) |p| p.deinit(); + for (srcs) |src| { + try programs.append(try Program.init(src)); + } + + // Create our uniform buffer that is shared across all + // custom shaders + const ubo = try gl.Buffer.create(); + errdefer ubo.destroy(); + { + var ubobind = try ubo.bind(.uniform); + defer ubobind.unbind(); + try ubobind.setDataNull(Uniforms, .static_draw); + } + + // Setup our VAO for the custom shader. + const vao = try gl.VertexArray.create(); + errdefer vao.destroy(); + const vaobind = try vao.bind(); + defer vaobind.unbind(); + + // Element buffer (EBO) + const ebo = try gl.Buffer.create(); + errdefer ebo.destroy(); + var ebobind = try ebo.bind(.element_array); + defer ebobind.unbind(); + try ebobind.setData([6]u8{ + 0, 1, 3, // Top-left triangle + 1, 2, 3, // Bottom-right triangle + }, .static_draw); + + return .{ + .programs = try programs.toOwnedSlice(), + .uniforms = .{}, + .ubo = ubo, + .vao = vao, + .ebo = ebo, + .last_frame_time = try std.time.Instant.now(), + }; + } + + pub fn deinit(self: *const State, alloc: Allocator) void { + for (self.programs) |p| p.deinit(); + alloc.free(self.programs); + self.ubo.destroy(); + self.ebo.destroy(); + self.vao.destroy(); + } + + pub fn syncUniforms(self: *const State) !void { + var ubobind = try self.ubo.bind(.uniform); + defer ubobind.unbind(); + try ubobind.setData(self.uniforms, .static_draw); + } + + pub fn bind(self: *const State) !Binding { + // Move our uniform buffer into proper global index. Note that + // in theory we can do this globally once and never worry about + // it again. I don't think we're high-performance enough at all + // to worry about that and this makes it so you can just move + // around CustomProgram usage without worrying about clobbering + // the global state. + try self.ubo.bindBase(.uniform, UNIFORM_INDEX); + + const vao = try self.vao.bind(); + errdefer vao.unbind(); + + const ebo = try self.ebo.bind(.element_array); + errdefer ebo.unbind(); + + return .{ + .vao = vao, + .ebo = ebo, + }; + } + + pub const Binding = struct { + vao: gl.VertexArray.Binding, + ebo: gl.Buffer.Binding, + + pub fn unbind(self: Binding) void { + self.ebo.unbind(); + self.vao.unbind(); + } + }; +}; + +/// A single OpenGL program (combined shaders) for custom shaders. +pub const Program = struct { + program: gl.Program, + + pub fn init(src: [:0]const u8) !Program { + const program = try gl.Program.createVF( + @embedFile("../shaders/custom.v.glsl"), + src, + //@embedFile("../shaders/temp.f.glsl"), + ); + errdefer program.destroy(); + + // Map our uniform buffer to the global GL state + try program.uniformBlockBinding(UNIFORM_INDEX, UNIFORM_BINDING); + + return .{ .program = program }; + } + + pub fn deinit(self: *const Program) void { + self.program.destroy(); + } + + pub fn bind(self: *const Program) !Binding { + const program = try self.program.use(); + errdefer program.unbind(); + + return .{ + .program = program, + }; + } + + pub const Binding = struct { + program: gl.Program.Binding, + + pub fn unbind(self: Binding) void { + self.program.unbind(); + } + }; +}; From 2559d6b3678379c16e4df5957b546279e09990e3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 16:53:26 -0800 Subject: [PATCH 42/55] renderer/opengl: increment time uniform --- src/renderer/OpenGL.zig | 15 +++++++-------- src/renderer/opengl/custom.zig | 13 ++++++++++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 076798630..8306843b5 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1405,13 +1405,15 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { // If we're in single-threaded more we grab a lock since we use shared data. if (single_threaded_draw) self.draw_mutex.lock(); defer if (single_threaded_draw) self.draw_mutex.unlock(); - const gl_state = self.gl_state orelse return; + const gl_state: *GLState = if (self.gl_state) |*v| v else return; // Draw our terminal cells - try self.drawCellProgram(&gl_state); + try self.drawCellProgram(gl_state); // Draw our custom shaders - try self.drawCustomPrograms(&gl_state); + if (gl_state.custom) |*custom_state| { + try self.drawCustomPrograms(custom_state); + } // Swap our window buffers switch (apprt.runtime) { @@ -1423,20 +1425,17 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { fn drawCustomPrograms( self: *OpenGL, - gl_state: *const GLState, + custom_state: *custom.State, ) !void { _ = self; - // If we have no custom shaders then we do nothing. - const custom_state = gl_state.custom orelse return; - // Bind our state that is global to all custom shaders const custom_bind = try custom_state.bind(); defer custom_bind.unbind(); // Sync the uniform data. // TODO: only do this when the data has changed - try custom_state.syncUniforms(); + try custom_state.newFrame(); // Go through each custom shader and draw it. for (custom_state.programs) |program| { diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig index 7f1deb5b2..b316c3635 100644 --- a/src/renderer/opengl/custom.zig +++ b/src/renderer/opengl/custom.zig @@ -93,7 +93,18 @@ pub const State = struct { self.vao.destroy(); } - pub fn syncUniforms(self: *const State) !void { + /// Call this prior to drawing a frame to update the time + /// and synchronize the uniforms. This synchronizes uniforms + /// so you should make changes to uniforms prior to calling + /// this. + pub fn newFrame(self: *State) !void { + // Update our frame time + const now = std.time.Instant.now() catch self.last_frame_time; + const since_ns: f32 = @floatFromInt(now.since(self.last_frame_time)); + self.uniforms.time = since_ns / std.time.ns_per_s; + self.uniforms.time_delta = since_ns / std.time.ns_per_s; + + // Sync our uniform changes var ubobind = try self.ubo.bind(.uniform); defer ubobind.unbind(); try ubobind.setData(self.uniforms, .static_draw); From fbc13d08b028c82ba6ef66371c820fb8f47a1531 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 16:58:36 -0800 Subject: [PATCH 43/55] renderer/opengl: set resolution uniform on screen size change --- src/renderer/OpenGL.zig | 12 ++++++++++-- src/renderer/opengl/custom.zig | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 8306843b5..f5609fd13 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -107,8 +107,11 @@ draw_background: terminal.color.RGB, const SetScreenSize = struct { size: renderer.ScreenSize, - fn apply(self: SetScreenSize, r: *const OpenGL) !void { - const gl_state = r.gl_state orelse return error.OpenGLUninitialized; + fn apply(self: SetScreenSize, r: *OpenGL) !void { + const gl_state: *GLState = if (r.gl_state) |*v| + v + else + return error.OpenGLUninitialized; // Apply our padding const padding = if (r.padding.balance) @@ -146,6 +149,11 @@ const SetScreenSize = struct { -1 * @as(f32, @floatFromInt(padding.top)), ), ); + + // Update our custom shader resolution + if (gl_state.custom) |*custom_state| { + try custom_state.setScreenSize(self.size); + } } }; diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig index b316c3635..96169be30 100644 --- a/src/renderer/opengl/custom.zig +++ b/src/renderer/opengl/custom.zig @@ -1,6 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const gl = @import("opengl"); +const ScreenSize = @import("../size.zig").ScreenSize; /// The "INDEX" is the index into the global GL state and the /// "BINDING" is the binding location in the shader. @@ -93,6 +94,16 @@ pub const State = struct { self.vao.destroy(); } + pub fn setScreenSize(self: *State, size: ScreenSize) !void { + // Update our uniforms + self.uniforms.resolution = .{ + @floatFromInt(size.width), + @floatFromInt(size.height), + 1, + }; + try self.syncUniforms(); + } + /// Call this prior to drawing a frame to update the time /// and synchronize the uniforms. This synchronizes uniforms /// so you should make changes to uniforms prior to calling @@ -105,11 +116,18 @@ pub const State = struct { self.uniforms.time_delta = since_ns / std.time.ns_per_s; // Sync our uniform changes + try self.syncUniforms(); + } + + fn syncUniforms(self: *State) !void { var ubobind = try self.ubo.bind(.uniform); defer ubobind.unbind(); try ubobind.setData(self.uniforms, .static_draw); } + /// Call this to bind all the necessary OpenGL resources for + /// all custom shaders. Each individual shader needs to be bound + /// one at a time too. pub fn bind(self: *const State) !Binding { // Move our uniform buffer into proper global index. Note that // in theory we can do this globally once and never worry about From db244da1011e13d075e31600a15445ff8bef9522 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 17:02:37 -0800 Subject: [PATCH 44/55] config: note custom-shader requires opengl 4.2 on linux --- src/config/Config.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 563b9627f..0deada505 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -606,6 +606,9 @@ keybind: Keybinds = .{}, /// causing the window to be completely black. If this happens, you can /// unset this configuration to disable the shader. /// +/// On Linux, this requires OpenGL 4.2. Ghostty typically only requires +/// OpenGL 3.3, but custom shaders push that requirement up to 4.2. +/// /// The shader API is identical to the ShaderToy API: you specify a `mainImage` /// function and the available uniforms match ShaderToy. The iChannel0 uniform /// is a texture containing the rendered terminal screen. From c8a51a2158a8c46b38cf430c5d4ab4753769a6b7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 20:39:20 -0800 Subject: [PATCH 45/55] renderer/opengl: create the screen texture --- pkg/opengl/Framebuffer.zig | 15 ++++++++-- pkg/opengl/Texture.zig | 51 ++++++++++++++++---------------- src/renderer/OpenGL.zig | 31 +++++++++++-------- src/renderer/opengl/custom.zig | 54 ++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 40 deletions(-) diff --git a/pkg/opengl/Framebuffer.zig b/pkg/opengl/Framebuffer.zig index 6335fa538..8ab07a238 100644 --- a/pkg/opengl/Framebuffer.zig +++ b/pkg/opengl/Framebuffer.zig @@ -20,8 +20,13 @@ pub fn destroy(v: Framebuffer) void { } pub fn bind(v: Framebuffer, target: Target) !Binding { + // The default framebuffer is documented as being zero but + // on multiple OpenGL drivers its not zero, so we grab it + // at runtime. + var current: c.GLint = undefined; + glad.context.GetIntegerv.?(c.GL_FRAMEBUFFER_BINDING, ¤t); glad.context.BindFramebuffer.?(@intFromEnum(target), v.id); - return .{ .target = target }; + return .{ .target = target, .previous = @intCast(current) }; } /// Enum for possible binding targets. @@ -55,9 +60,13 @@ pub const Status = enum(c_uint) { pub const Binding = struct { target: Target, + previous: c.GLuint, pub fn unbind(self: Binding) void { - glad.context.BindFramebuffer.?(@intFromEnum(self.target), 0); + glad.context.BindFramebuffer.?( + @intFromEnum(self.target), + self.previous, + ); } pub fn texture2D( @@ -78,6 +87,6 @@ pub const Binding = struct { } pub fn checkStatus(self: Binding) Status { - return @enumFromInt(glad.context.CheckFramebufferStatus.?(self.target)); + return @enumFromInt(glad.context.CheckFramebufferStatus.?(@intFromEnum(self.target))); } }; diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig index 91a65b565..afa22e926 100644 --- a/pkg/opengl/Texture.zig +++ b/pkg/opengl/Texture.zig @@ -7,11 +7,29 @@ const glad = @import("glad.zig"); id: c.GLuint, -pub inline fn active(target: c.GLenum) !void { +pub fn active(target: c.GLenum) !void { glad.context.ActiveTexture.?(target); try errors.getError(); } +/// Create a single texture. +pub fn create() !Texture { + var id: c.GLuint = undefined; + glad.context.GenTextures.?(1, &id); + return .{ .id = id }; +} + +/// glBindTexture +pub fn bind(v: Texture, target: Target) !Binding { + glad.context.BindTexture.?(@intFromEnum(target), v.id); + try errors.getError(); + return .{ .target = target }; +} + +pub fn destroy(v: Texture) void { + glad.context.DeleteTextures.?(1, &v.id); +} + /// Enun for possible texture binding targets. pub const Target = enum(c_uint) { @"1D" = c.GL_TEXTURE_1D, @@ -48,8 +66,9 @@ pub const Parameter = enum(c_uint) { /// Internal format enum for texture images. pub const InternalFormat = enum(c_int) { - Red = c.GL_RED, - RGBA = c.GL_RGBA, + red = c.GL_RED, + rgb = c.GL_RGB, + rgba = c.GL_RGBA, // There are so many more that I haven't filled in. _, @@ -57,8 +76,9 @@ pub const InternalFormat = enum(c_int) { /// Format for texture images pub const Format = enum(c_uint) { - Red = c.GL_RED, - BGRA = c.GL_BGRA, + red = c.GL_RED, + rgb = c.GL_RGB, + bgra = c.GL_BGRA, // There are so many more that I haven't filled in. _, @@ -75,9 +95,8 @@ pub const DataType = enum(c_uint) { pub const Binding = struct { target: Target, - pub inline fn unbind(b: *Binding) void { + pub fn unbind(b: *const Binding) void { glad.context.BindTexture.?(@intFromEnum(b.target), 0); - b.* = undefined; } pub fn generateMipmap(b: Binding) void { @@ -143,21 +162,3 @@ pub const Binding = struct { ); } }; - -/// Create a single texture. -pub inline fn create() !Texture { - var id: c.GLuint = undefined; - glad.context.GenTextures.?(1, &id); - return Texture{ .id = id }; -} - -/// glBindTexture -pub inline fn bind(v: Texture, target: Target) !Binding { - glad.context.BindTexture.?(@intFromEnum(target), v.id); - try errors.getError(); - return Binding{ .target = target }; -} - -pub inline fn destroy(v: Texture) void { - glad.context.DeleteTextures.?(1, &v.id); -} diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index f5609fd13..3d3f05d29 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1346,11 +1346,11 @@ fn flushAtlas(self: *OpenGL) !void { atlas.resized = false; try texbind.image2D( 0, - .Red, + .red, @intCast(atlas.size), @intCast(atlas.size), 0, - .Red, + .red, .UnsignedByte, atlas.data.ptr, ); @@ -1361,7 +1361,7 @@ fn flushAtlas(self: *OpenGL) !void { 0, @intCast(atlas.size), @intCast(atlas.size), - .Red, + .red, .UnsignedByte, atlas.data.ptr, ); @@ -1380,11 +1380,11 @@ fn flushAtlas(self: *OpenGL) !void { atlas.resized = false; try texbind.image2D( 0, - .RGBA, + .rgba, @intCast(atlas.size), @intCast(atlas.size), 0, - .BGRA, + .bgra, .UnsignedByte, atlas.data.ptr, ); @@ -1395,7 +1395,7 @@ fn flushAtlas(self: *OpenGL) !void { 0, @intCast(atlas.size), @intCast(atlas.size), - .BGRA, + .bgra, .UnsignedByte, atlas.data.ptr, ); @@ -1441,8 +1441,7 @@ fn drawCustomPrograms( const custom_bind = try custom_state.bind(); defer custom_bind.unbind(); - // Sync the uniform data. - // TODO: only do this when the data has changed + // Setup the new frame try custom_state.newFrame(); // Go through each custom shader and draw it. @@ -1469,6 +1468,14 @@ fn drawCellProgram( // are changes to the atlas. try self.flushAtlas(); + // If we have custom shaders, then we draw to the custom + // shader framebuffer. + const fbobind: ?gl.Framebuffer.Binding = fbobind: { + const state = gl_state.custom orelse break :fbobind null; + break :fbobind try state.fbo.bind(.framebuffer); + }; + defer if (fbobind) |v| v.unbind(); + // Clear the surface gl.clearColor( @as(f32, @floatFromInt(self.draw_background.r)) / 255, @@ -1615,11 +1622,11 @@ const GLState = struct { try texbind.parameter(.MagFilter, gl.c.GL_LINEAR); try texbind.image2D( 0, - .Red, + .red, @intCast(font_group.atlas_greyscale.size), @intCast(font_group.atlas_greyscale.size), 0, - .Red, + .red, .UnsignedByte, font_group.atlas_greyscale.data.ptr, ); @@ -1636,11 +1643,11 @@ const GLState = struct { try texbind.parameter(.MagFilter, gl.c.GL_LINEAR); try texbind.image2D( 0, - .RGBA, + .rgba, @intCast(font_group.atlas_color.size), @intCast(font_group.atlas_color.size), 0, - .BGRA, + .bgra, .UnsignedByte, font_group.atlas_color.data.ptr, ); diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig index 96169be30..cb75cfd54 100644 --- a/src/renderer/opengl/custom.zig +++ b/src/renderer/opengl/custom.zig @@ -3,6 +3,8 @@ const Allocator = std.mem.Allocator; const gl = @import("opengl"); const ScreenSize = @import("../size.zig").ScreenSize; +const log = std.log.scoped(.opengl_custom); + /// The "INDEX" is the index into the global GL state and the /// "BINDING" is the binding location in the shader. const UNIFORM_INDEX: gl.c.GLuint = 0; @@ -27,9 +29,11 @@ pub const State = struct { uniforms: Uniforms, /// The OpenGL buffers + fbo: gl.Framebuffer, ubo: gl.Buffer, vao: gl.VertexArray, ebo: gl.Buffer, + fb_texture: gl.Texture, /// The set of programs for the custom shaders. programs: []const Program, @@ -50,6 +54,44 @@ pub const State = struct { try programs.append(try Program.init(src)); } + // Create the texture for the framebuffer + const fb_tex = try gl.Texture.create(); + errdefer fb_tex.destroy(); + { + const texbind = try fb_tex.bind(.@"2D"); + try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE); + try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE); + try texbind.parameter(.MinFilter, gl.c.GL_LINEAR); + try texbind.parameter(.MagFilter, gl.c.GL_LINEAR); + try texbind.image2D( + 0, + .rgb, + 1, + 1, + 0, + .rgb, + .UnsignedByte, + null, + ); + } + + // Create our framebuffer for rendering off screen. + // The shader prior to custom shaders should use this + // framebuffer. + const fbo = try gl.Framebuffer.create(); + errdefer fbo.destroy(); + const fbbind = try fbo.bind(.framebuffer); + defer fbbind.unbind(); + try fbbind.texture2D(.color0, .@"2D", fb_tex, 0); + const fbstatus = fbbind.checkStatus(); + if (fbstatus != .complete) { + log.warn( + "framebuffer is not complete state={}", + .{fbstatus}, + ); + return error.InvalidFramebuffer; + } + // Create our uniform buffer that is shared across all // custom shaders const ubo = try gl.Buffer.create(); @@ -79,9 +121,11 @@ pub const State = struct { return .{ .programs = try programs.toOwnedSlice(), .uniforms = .{}, + .fbo = fbo, .ubo = ubo, .vao = vao, .ebo = ebo, + .fb_texture = fb_tex, .last_frame_time = try std.time.Instant.now(), }; } @@ -92,6 +136,8 @@ pub const State = struct { self.ubo.destroy(); self.ebo.destroy(); self.vao.destroy(); + self.fb_texture.destroy(); + self.fbo.destroy(); } pub fn setScreenSize(self: *State, size: ScreenSize) !void { @@ -137,6 +183,11 @@ pub const State = struct { // the global state. try self.ubo.bindBase(.uniform, UNIFORM_INDEX); + // Bind our texture that is shared amongst all + try gl.Texture.active(gl.c.GL_TEXTURE0); + var texbind = try self.fb_texture.bind(.@"2D"); + errdefer texbind.unbind(); + const vao = try self.vao.bind(); errdefer vao.unbind(); @@ -146,16 +197,19 @@ pub const State = struct { return .{ .vao = vao, .ebo = ebo, + .fb_texture = texbind, }; } pub const Binding = struct { vao: gl.VertexArray.Binding, ebo: gl.Buffer.Binding, + fb_texture: gl.Texture.Binding, pub fn unbind(self: Binding) void { self.ebo.unbind(); self.vao.unbind(); + self.fb_texture.unbind(); } }; }; From 5c7bad2f7d64b7fdfd5e225b20009e6f2cc036dc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 20:46:23 -0800 Subject: [PATCH 46/55] renderer/opengl: resize the screen texture --- src/renderer/opengl/custom.zig | 13 +++++++++++++ src/renderer/shaders/shadertoy_prefix.glsl | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig index cb75cfd54..6e0dd8d2a 100644 --- a/src/renderer/opengl/custom.zig +++ b/src/renderer/opengl/custom.zig @@ -148,6 +148,19 @@ pub const State = struct { 1, }; try self.syncUniforms(); + + // Update our texture + const texbind = try self.fb_texture.bind(.@"2D"); + try texbind.image2D( + 0, + .rgb, + @intCast(size.width), + @intCast(size.height), + 0, + .rgb, + .UnsignedByte, + null, + ); } /// Call this prior to drawing a frame to update the time diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl index d62e19605..a1a220bd4 100644 --- a/src/renderer/shaders/shadertoy_prefix.glsl +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -13,7 +13,7 @@ layout(binding = 0) uniform Globals { uniform float iSampleRate; }; -layout(binding = 1) uniform sampler2D iChannel0; +layout(binding = 0) uniform sampler2D iChannel0; // These are unused currently by Ghostty: // layout(binding = 1) uniform sampler2D iChannel1; From 39e70558533270ef367367fbd41ad449014427b9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 20:48:20 -0800 Subject: [PATCH 47/55] renderer/opengl: enable animations --- src/renderer/OpenGL.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 3d3f05d29..909388a78 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -475,6 +475,13 @@ pub fn threadExit(self: *const OpenGL) void { } } +/// True if our renderer has animations so that a higher frequency +/// timer is used. +pub fn hasAnimations(self: *const OpenGL) bool { + const state = self.gl_state orelse return false; + return state.custom != null; +} + /// Callback when the focus changes for the terminal this is rendering. /// /// Must be called on the render thread. From cc389c81c2c7acc66422d7262775db3307f00dde Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 20:49:41 -0800 Subject: [PATCH 48/55] quiet tests --- src/renderer/shadertoy.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 5f7dc402e..6f7937102 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -358,7 +358,7 @@ test "shadertoy to glsl" { const glsl = try glslFromSpv(alloc, spvlist.items); defer alloc.free(glsl); - log.warn("glsl={s}", .{glsl}); + // log.warn("glsl={s}", .{glsl}); } const test_crt = @embedFile("shaders/test_shadertoy_crt.glsl"); From 5d7c47a4696f69e49a3085f9ea70397c47c0db4a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 20:55:36 -0800 Subject: [PATCH 49/55] renderer/metal: clean up some memory management --- src/App.zig | 35 ---------------------------------- src/renderer/metal/shaders.zig | 9 +++++++++ 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/src/App.zig b/src/App.zig index 4536a2f69..a09860a86 100644 --- a/src/App.zig +++ b/src/App.zig @@ -73,41 +73,6 @@ pub fn create( }; errdefer app.surfaces.deinit(alloc); - // TODO: remove this temporary crap - // const glslang = @import("glslang"); - // try glslang.init(); - // { - // const c = glslang.c; - // const glsl_input: c.glslang_input_t = .{ - // .language = c.GLSLANG_SOURCE_GLSL, - // .stage = c.GLSLANG_STAGE_FRAGMENT, - // .client = c.GLSLANG_CLIENT_VULKAN, - // .client_version = c.GLSLANG_TARGET_VULKAN_1_2, - // .target_language = c.GLSLANG_TARGET_SPV, - // .target_language_version = c.GLSLANG_TARGET_SPV_1_5, - // .code = @embedFile("temp.frag"), - // .default_version = 100, - // .default_profile = c.GLSLANG_NO_PROFILE, - // .force_default_version_and_profile = 0, - // .forward_compatible = 0, - // .messages = c.GLSLANG_MSG_DEFAULT_BIT, - // .resource = c.glslang_default_resource(), - // }; - // - // const shader = try glslang.Shader.create(&glsl_input); - // defer shader.delete(); - // try shader.preprocess(&glsl_input); - // try shader.parse(&glsl_input); - // - // const program = try glslang.Program.create(); - // defer program.delete(); - // program.addShader(shader); - // try program.link(c.GLSLANG_MSG_SPV_RULES_BIT | c.GLSLANG_MSG_VULKAN_RULES_BIT); - // program.spirvGenerate(c.GLSLANG_STAGE_FRAGMENT); - // const size = program.spirvGetSize(); - // log.warn("SPIRV PROGRAM size={d}", .{size}); - // } - return app; } diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 32f775a50..030ae2b6c 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -254,6 +254,8 @@ fn initPostPipeline( const ptr = post_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); break :func_frag objc.Object.fromId(ptr.?); }; + defer func_vert.msgSend(void, objc.sel("release"), .{}); + defer func_frag.msgSend(void, objc.sel("release"), .{}); // Create our descriptor const desc = init: { @@ -262,6 +264,7 @@ fn initPostPipeline( const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; }; + defer desc.msgSend(void, objc.sel("release"), .{}); desc.setProperty("vertexFunction", func_vert); desc.setProperty("fragmentFunction", func_frag); @@ -315,6 +318,8 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); break :func_frag objc.Object.fromId(ptr.?); }; + defer func_vert.msgSend(void, objc.sel("release"), .{}); + defer func_frag.msgSend(void, objc.sel("release"), .{}); // Create the vertex descriptor. The vertex descriptor describes the // data layout of the vertex inputs. We use indexed (or "instanced") @@ -501,6 +506,8 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); break :func_frag objc.Object.fromId(ptr.?); }; + defer func_vert.msgSend(void, objc.sel("release"), .{}); + defer func_frag.msgSend(void, objc.sel("release"), .{}); // Create the vertex descriptor. The vertex descriptor describes the // data layout of the vertex inputs. We use indexed (or "instanced") @@ -577,6 +584,7 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { break :vertex_desc desc; }; + defer vertex_desc.msgSend(void, objc.sel("release"), .{}); // Create our descriptor const desc = init: { @@ -585,6 +593,7 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; }; + defer desc.msgSend(void, objc.sel("release"), .{}); // Set our properties desc.setProperty("vertexFunction", func_vert); From a64d12d3cbe8e96df09ee013d125f9cde24948b9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 20:56:31 -0800 Subject: [PATCH 50/55] renderer: animations should stop if config changes them --- src/renderer/Thread.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index e6d0809a5..156d6cd6d 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -310,6 +310,11 @@ fn drainMailbox(self: *Thread) !void { .change_config => |config| { defer config.alloc.destroy(config.ptr); try self.renderer.changeConfig(config.ptr); + + // Stop and start the draw timer to capture the new + // hasAnimations value. + self.stopDrawTimer(); + self.startDrawTimer(); }, .inspector => |v| self.flags.has_inspector = v, From 2db36646ac62ab269a9881e377a157922505ab77 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 20:59:20 -0800 Subject: [PATCH 51/55] renderer/opengl: some comments --- src/renderer/OpenGL.zig | 9 ++------- src/renderer/opengl/custom.zig | 31 ++++++++++++++++++++++++++----- src/renderer/shaders/temp.f.glsl | 28 ---------------------------- 3 files changed, 28 insertions(+), 40 deletions(-) delete mode 100644 src/renderer/shaders/temp.f.glsl diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 909388a78..99b89c527 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1438,6 +1438,7 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { } } +/// Draw the custom shaders. fn drawCustomPrograms( self: *OpenGL, custom_state: *custom.State, @@ -1456,13 +1457,7 @@ fn drawCustomPrograms( // Bind our cell program state, buffers const bind = try program.bind(); defer bind.unbind(); - - try gl.drawElementsInstanced( - gl.c.GL_TRIANGLES, - 6, - gl.c.GL_UNSIGNED_BYTE, - 1, - ); + try bind.draw(); } } diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig index 6e0dd8d2a..c14ba3c5c 100644 --- a/src/renderer/opengl/custom.zig +++ b/src/renderer/opengl/custom.zig @@ -10,6 +10,7 @@ const log = std.log.scoped(.opengl_custom); const UNIFORM_INDEX: gl.c.GLuint = 0; const UNIFORM_BINDING: gl.c.GLuint = 0; +/// Global uniforms for custom shaders. pub const Uniforms = extern struct { resolution: [3]f32 align(16) = .{ 0, 0, 0 }, time: f32 align(4) = 1, @@ -23,7 +24,13 @@ pub const Uniforms = extern struct { sample_rate: f32 align(4) = 1, }; -/// The state associated with custom shaders. +/// The state associated with custom shaders. This should only be initialized +/// if there is at least one custom shader. +/// +/// To use this, the main terminal shader should render to the framebuffer +/// specified by "fbo". The resulting "fb_texture" will contain the color +/// attachment. This is then used as the iChannel0 input to the custom +/// shader. pub const State = struct { /// The uniform data uniforms: Uniforms, @@ -40,12 +47,14 @@ pub const State = struct { /// The last time the frame was drawn. This is used to update /// the time uniform. - last_frame_time: std.time.Instant, + first_frame_time: std.time.Instant, pub fn init( alloc: Allocator, srcs: []const [:0]const u8, ) !State { + if (srcs.len == 0) return error.OneCustomShaderRequired; + // Create our programs var programs = std.ArrayList(Program).init(alloc); defer programs.deinit(); @@ -126,7 +135,7 @@ pub const State = struct { .vao = vao, .ebo = ebo, .fb_texture = fb_tex, - .last_frame_time = try std.time.Instant.now(), + .first_frame_time = try std.time.Instant.now(), }; } @@ -169,8 +178,8 @@ pub const State = struct { /// this. pub fn newFrame(self: *State) !void { // Update our frame time - const now = std.time.Instant.now() catch self.last_frame_time; - const since_ns: f32 = @floatFromInt(now.since(self.last_frame_time)); + const now = std.time.Instant.now() catch self.first_frame_time; + const since_ns: f32 = @floatFromInt(now.since(self.first_frame_time)); self.uniforms.time = since_ns / std.time.ns_per_s; self.uniforms.time_delta = since_ns / std.time.ns_per_s; @@ -249,6 +258,8 @@ pub const Program = struct { self.program.destroy(); } + /// Bind the program for use. This should be called so that draw can + /// be called. pub fn bind(self: *const Program) !Binding { const program = try self.program.use(); errdefer program.unbind(); @@ -264,5 +275,15 @@ pub const Program = struct { pub fn unbind(self: Binding) void { self.program.unbind(); } + + pub fn draw(self: Binding) !void { + _ = self; + try gl.drawElementsInstanced( + gl.c.GL_TRIANGLES, + 6, + gl.c.GL_UNSIGNED_BYTE, + 1, + ); + } }; }; diff --git a/src/renderer/shaders/temp.f.glsl b/src/renderer/shaders/temp.f.glsl deleted file mode 100644 index b97b34549..000000000 --- a/src/renderer/shaders/temp.f.glsl +++ /dev/null @@ -1,28 +0,0 @@ -#version 430 core - -layout(binding = 0, std140) uniform Globals -{ - vec3 iResolution; - float iTime; - float iTimeDelta; - float iFrameRate; - int iFrame; - float iChannelTime[4]; - vec3 iChannelResolution[4]; - vec4 iMouse; - vec4 iDate; - float iSampleRate; -} _89; - -layout(binding = 1) uniform sampler2D iChannel0; - -layout(location = 0) out vec4 _fragColor; - -void main() { - // red - _fragColor = vec4(_89.iSampleRate, 0.0, 0.0, 1.0); - - // maze - //vec4 I = gl_FragCoord; - //_fragColor = vec4(3)*modf(I*.1,I)[int(length(I)*1e4)&1]; -} From 8253fc1f3140672b927700ab64365f54f0f8aaf8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 21:07:47 -0800 Subject: [PATCH 52/55] renderer/opengl: shaders only need one color input --- src/renderer/OpenGL.zig | 76 +++++++++++------------------ src/renderer/opengl/CellProgram.zig | 24 +++------ src/renderer/shaders/cell.v.glsl | 18 +++---- 3 files changed, 43 insertions(+), 75 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 99b89c527..2359b3cff 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -833,15 +833,15 @@ pub fn rebuildCells( if (cursor_cell) |*cell| { if (cell.mode == .fg) { if (self.config.cursor_text) |txt| { - cell.fg_r = txt.r; - cell.fg_g = txt.g; - cell.fg_b = txt.b; - cell.fg_a = 255; + cell.r = txt.r; + cell.g = txt.g; + cell.b = txt.b; + cell.a = 255; } else { - cell.fg_r = 0; - cell.fg_g = 0; - cell.fg_b = 0; - cell.fg_a = 255; + cell.r = 0; + cell.g = 0; + cell.b = 0; + cell.a = 255; } } self.cells.appendAssumeCapacity(cell.*); @@ -992,14 +992,10 @@ fn addCursor( .grid_col = @intCast(x), .grid_row = @intCast(screen.cursor.y), .grid_width = if (wide) 2 else 1, - .fg_r = color.r, - .fg_g = color.g, - .fg_b = color.b, - .fg_a = alpha, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, + .r = color.r, + .g = color.g, + .b = color.b, + .a = alpha, .glyph_x = glyph.atlas_x, .glyph_y = glyph.atlas_y, .glyph_width = glyph.width, @@ -1147,14 +1143,10 @@ pub fn updateCell( .glyph_height = 0, .glyph_offset_x = 0, .glyph_offset_y = 0, - .fg_r = 0, - .fg_g = 0, - .fg_b = 0, - .fg_a = 0, - .bg_r = rgb.r, - .bg_g = rgb.g, - .bg_b = rgb.b, - .bg_a = bg_alpha, + .r = rgb.r, + .g = rgb.g, + .b = rgb.b, + .a = bg_alpha, }); } @@ -1189,14 +1181,10 @@ pub fn updateCell( .glyph_height = glyph.height, .glyph_offset_x = glyph.offset_x, .glyph_offset_y = glyph.offset_y, - .fg_r = colors.fg.r, - .fg_g = colors.fg.g, - .fg_b = colors.fg.b, - .fg_a = alpha, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, + .r = colors.fg.r, + .g = colors.fg.g, + .b = colors.fg.b, + .a = alpha, }); } @@ -1230,14 +1218,10 @@ pub fn updateCell( .glyph_height = underline_glyph.height, .glyph_offset_x = underline_glyph.offset_x, .glyph_offset_y = underline_glyph.offset_y, - .fg_r = color.r, - .fg_g = color.g, - .fg_b = color.b, - .fg_a = alpha, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, + .r = color.r, + .g = color.g, + .b = color.b, + .a = alpha, }); } @@ -1253,14 +1237,10 @@ pub fn updateCell( .glyph_height = 0, .glyph_offset_x = 0, .glyph_offset_y = 0, - .fg_r = colors.fg.r, - .fg_g = colors.fg.g, - .fg_b = colors.fg.b, - .fg_a = alpha, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, + .r = colors.fg.r, + .g = colors.fg.g, + .b = colors.fg.b, + .a = alpha, }); } diff --git a/src/renderer/opengl/CellProgram.zig b/src/renderer/opengl/CellProgram.zig index c72959ef8..d1ea969fe 100644 --- a/src/renderer/opengl/CellProgram.zig +++ b/src/renderer/opengl/CellProgram.zig @@ -25,21 +25,15 @@ pub const Cell = extern struct { glyph_width: u32 = 0, glyph_height: u32 = 0, - /// vec2 glyph_size + /// vec2 glyph_offset glyph_offset_x: i32 = 0, glyph_offset_y: i32 = 0, /// vec4 fg_color_in - fg_r: u8, - fg_g: u8, - fg_b: u8, - fg_a: u8, - - /// vec4 bg_color_in - bg_r: u8, - bg_g: u8, - bg_b: u8, - bg_a: u8, + r: u8, + g: u8, + b: u8, + a: u8, /// uint mode mode: CellMode, @@ -111,11 +105,9 @@ pub fn init() !CellProgram { offset += 2 * @sizeOf(i32); try vbobind.attributeAdvanced(4, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Cell), offset); offset += 4 * @sizeOf(u8); - try vbobind.attributeAdvanced(5, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Cell), offset); - offset += 4 * @sizeOf(u8); - try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset); + try vbobind.attributeIAdvanced(5, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset); offset += 1 * @sizeOf(u8); - try vbobind.attributeIAdvanced(7, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset); + try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset); try vbobind.enableAttribArray(0); try vbobind.enableAttribArray(1); try vbobind.enableAttribArray(2); @@ -123,7 +115,6 @@ pub fn init() !CellProgram { try vbobind.enableAttribArray(4); try vbobind.enableAttribArray(5); try vbobind.enableAttribArray(6); - try vbobind.enableAttribArray(7); try vbobind.attributeDivisor(0, 1); try vbobind.attributeDivisor(1, 1); try vbobind.attributeDivisor(2, 1); @@ -131,7 +122,6 @@ pub fn init() !CellProgram { try vbobind.attributeDivisor(4, 1); try vbobind.attributeDivisor(5, 1); try vbobind.attributeDivisor(6, 1); - try vbobind.attributeDivisor(7, 1); return .{ .program = program, diff --git a/src/renderer/shaders/cell.v.glsl b/src/renderer/shaders/cell.v.glsl index a339358c5..ccca33982 100644 --- a/src/renderer/shaders/cell.v.glsl +++ b/src/renderer/shaders/cell.v.glsl @@ -21,20 +21,18 @@ layout (location = 2) in vec2 glyph_size; // Offset of the top-left corner of the glyph when rendered in a rect. layout (location = 3) in vec2 glyph_offset; -// The background color for this cell in RGBA (0 to 1.0) -layout (location = 4) in vec4 fg_color_in; - -// The background color for this cell in RGBA (0 to 1.0) -layout (location = 5) in vec4 bg_color_in; +// The color for this cell in RGBA (0 to 1.0). Background or foreground +// depends on mode. +layout (location = 4) in vec4 color_in; // The mode of this shader. The mode determines what fields are used, // what the output will be, etc. This shader is capable of executing in // multiple "modes" so that we can share some logic and so that we can draw // the entire terminal grid in a single GPU pass. -layout (location = 6) in uint mode_in; +layout (location = 5) in uint mode_in; // The width in cells of this item. -layout (location = 7) in uint grid_width; +layout (location = 6) in uint grid_width; // The background or foreground color for the fragment, depending on // whether this is a background or foreground pass. @@ -117,7 +115,7 @@ void main() { cell_pos = cell_pos + cell_size_scaled * position; gl_Position = projection * vec4(cell_pos, cell_z, 1.0); - color = bg_color_in / 255.0; + color = color_in / 255.0; break; case MODE_FG: @@ -150,7 +148,7 @@ void main() { glyph_tex_coords = glyph_tex_pos + glyph_tex_size * position; // Set our foreground color output - color = fg_color_in / 255.; + color = color_in / 255.; break; case MODE_STRIKETHROUGH: @@ -166,7 +164,7 @@ void main() { cell_pos = cell_pos + strikethrough_offset - (strikethrough_size * position); gl_Position = projection * vec4(cell_pos, cell_z, 1.0); - color = fg_color_in / 255.0; + color = color_in / 255.0; break; } } From 61f10dc583f267ba992b2f220ee4493ae566ffd4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 21:11:24 -0800 Subject: [PATCH 53/55] renderer/opengl: new gpucell --- src/renderer/OpenGL.zig | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 2359b3cff..62232a2de 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -905,14 +905,10 @@ fn addPreeditCell( .glyph_height = 0, .glyph_offset_x = 0, .glyph_offset_y = 0, - .fg_r = 0, - .fg_g = 0, - .fg_b = 0, - .fg_a = 0, - .bg_r = bg.r, - .bg_g = bg.g, - .bg_b = bg.b, - .bg_a = 255, + .r = bg.r, + .g = bg.g, + .b = bg.b, + .a = 255, }); // Add our text @@ -927,14 +923,10 @@ fn addPreeditCell( .glyph_height = glyph.height, .glyph_offset_x = glyph.offset_x, .glyph_offset_y = glyph.offset_y, - .fg_r = fg.r, - .fg_g = fg.g, - .fg_b = fg.b, - .fg_a = 255, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, + .r = fg.r, + .g = fg.g, + .b = fg.b, + .a = 255, }); } From e55cb274bab1f48918bff280e3b232351b9834d5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 21:30:51 -0800 Subject: [PATCH 54/55] config: custom-shader-animation --- src/config/Config.zig | 20 ++++++++++++++------ src/renderer/Metal.zig | 5 ++++- src/renderer/OpenGL.zig | 4 +++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 0deada505..543792225 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -609,22 +609,30 @@ keybind: Keybinds = .{}, /// On Linux, this requires OpenGL 4.2. Ghostty typically only requires /// OpenGL 3.3, but custom shaders push that requirement up to 4.2. /// -/// The shader API is identical to the ShaderToy API: you specify a `mainImage` -/// function and the available uniforms match ShaderToy. The iChannel0 uniform +/// The shader API is identical to the Shadertoy API: you specify a `mainImage` +/// function and the available uniforms match Shadertoy. The iChannel0 uniform /// is a texture containing the rendered terminal screen. /// /// If the shader fails to compile, the shader will be ignored. Any errors /// related to shader compilation will not show up as configuration errors /// and only show up in the log, since shader compilation happens after -/// configuration loading on the dedicated render thread. If your shader is -/// not working, another way to debug is to run the `ghostty -/// +custom-shader-compile` command which will compile the shader and show any -/// errors. For interactive development, use ShaderToy.com. +/// configuration loading on the dedicated render thread. For interactive +/// development, use Shadertoy.com. /// /// This can be repeated multiple times to load multiple shaders. The shaders /// will be run in the order they are specified. @"custom-shader": RepeatablePath = .{}, +/// If true (default), the focused terminal surface will run an animation +/// loop when custom shaders are used. This uses slightly more CPU (generally +/// less than 10%) but allows the shader to animate. This only runs if there +/// are custom shaders. +/// +/// If this is set to false, the terminal and custom shader will only render +/// when the terminal is updated. This is more efficient but the shader will +/// not animate. +@"custom-shader-animation": bool = true, + /// If anything other than false, fullscreen mode on macOS will not use the /// native fullscreen, but make the window fullscreen without animations and /// using a new space. It's faster than the native fullscreen mode since it diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index e26019a9a..1012d6bea 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -152,6 +152,7 @@ pub const DerivedConfig = struct { selection_foreground: ?terminal.color.RGB, invert_selection_fg_bg: bool, custom_shaders: std.ArrayListUnmanaged([]const u8), + custom_shader_animation: bool, pub fn init( alloc_gpa: Allocator, @@ -206,6 +207,7 @@ pub const DerivedConfig = struct { null, .custom_shaders = custom_shaders, + .custom_shader_animation = config.@"custom-shader-animation", .arena = arena, }; @@ -471,7 +473,8 @@ pub fn threadExit(self: *const Metal) void { /// True if our renderer has animations so that a higher frequency /// timer is used. pub fn hasAnimations(self: *const Metal) bool { - return self.custom_shader_state != null; + return self.custom_shader_state != null and + self.config.custom_shader_animation; } /// Returns the grid size for a given screen size. This is safe to call diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 62232a2de..3d7031c8d 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -200,6 +200,7 @@ pub const DerivedConfig = struct { selection_foreground: ?terminal.color.RGB, invert_selection_fg_bg: bool, custom_shaders: std.ArrayListUnmanaged([]const u8), + custom_shader_animation: bool, pub fn init( alloc_gpa: Allocator, @@ -254,6 +255,7 @@ pub const DerivedConfig = struct { null, .custom_shaders = custom_shaders, + .custom_shader_animation = config.@"custom-shader-animation", .arena = arena, }; @@ -479,7 +481,7 @@ pub fn threadExit(self: *const OpenGL) void { /// timer is used. pub fn hasAnimations(self: *const OpenGL) bool { const state = self.gl_state orelse return false; - return state.custom != null; + return state.custom != null and self.config.custom_shader_animation; } /// Callback when the focus changes for the terminal this is rendering. From eaf847381ae748b2abb072022815f6d3150f7dba Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Nov 2023 22:00:33 -0800 Subject: [PATCH 55/55] config: clarify runtime reloading of shader stuff --- src/config/Config.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 543792225..42af30825 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -621,6 +621,9 @@ keybind: Keybinds = .{}, /// /// This can be repeated multiple times to load multiple shaders. The shaders /// will be run in the order they are specified. +/// +/// Changing this value at runtime and reloading the configuration will only +/// affect new windows, tabs, and splits. @"custom-shader": RepeatablePath = .{}, /// If true (default), the focused terminal surface will run an animation @@ -631,6 +634,9 @@ keybind: Keybinds = .{}, /// If this is set to false, the terminal and custom shader will only render /// when the terminal is updated. This is more efficient but the shader will /// not animate. +/// +/// This value can be changed at runtime and will affect all currently +/// open terminals. @"custom-shader-animation": bool = true, /// If anything other than false, fullscreen mode on macOS will not use the