The Big Renderer Rework (#7620)

It's here, the long-foretold and long-procrastinated renderer rework!
Hopefully this makes it easier to adapt and modify the renderer in the
future and ensures feature parity between Metal and OpenGL. Despite
having been a lot of work to write initially, with the abstraction layer
in place I feel like working on the renderer will be a much more
pleasant experience going forward.

## Key points
- CPU-side renderer logic is now mostly unified via a generic
`Renderer`.
- A graphics API abstraction layer over OpenGL and Metal has been
introduced.
- Minimum OpenGL version bumped to `4.3`, so can no longer be run on
macOS; I used the nix VM stuff for my testing during development. (Edit
by @mitchellh: Note for readers that Ghostty still works on macOS, but
the OpenGL backend doesn't, only the Metal one)
- The OpenGL backend now supports linear blending! Woohoo! The default
`alpha-blending` has been updated to `linear-corrected` since it's
essentially a strict improvement over `native`. The default on macOS is
still `native` though to match other mac apps in appearance, since macOS
users are more sensitive to text appearance.
- Custom shaders can now be hot reloaded.
- The background color is once again drawn by us, so custom shaders can
interact with it properly. In general, custom shaders should be a little
more robust.

## The abstraction layer
The general hierarchy of the abstraction layer is as such:
```
 [ GraphicsAPI ] - Responsible for configuring the runtime surface
    |     |        and providing render `Target`s that draw to it,
    |     |        as well as `Frame`s and `Pipeline`s.
    |     V
    | [ Target ] - Represents an abstract target for rendering, which
    |              could be a surface directly but is also used as an
    |              abstraction for off-screen frame buffers.
    V
 [ Frame ] - Represents the context for drawing a given frame,
    |        provides `RenderPass`es for issuing draw commands
    |        to, and reports the frame health when complete.
    V
 [ RenderPass ] - Represents a render pass in a frame, consisting of
   :              one or more `Step`s applied to the same target(s),
 [ Step ] - - - - each describing the input buffers and textures and
   :              the vertex/fragment functions and geometry to use.
   :_ _ _ _ _ _ _ _ _ _/
   v
 [ Pipeline ] - Describes a vertex and fragment function to be used
                for a `Step`; the `GraphicsAPI` is responsible for
                these and they should be constructed and cached
                ahead of time.

 [ Buffer ] - An abstraction over a GPU buffer.

 [ Texture ] - An abstraction over a GPU texture.
```
More specific documentation can be found on the relevant structures.

## Miscellany
Renderers (which effectively just means the generic renderer) are now
expected to only touch GPU resources in `init`, certain lifecycle
functions such as the `displayRealized`/`displayUnrealized` callbacks
from GTK-- and `drawFrame`; and are also expected to be thread-safe.
This allows the renderer thread to build the CPU-side buffers
(`updateFrame`) even if we can only *draw* from the app thread.

Because of this change, we can draw synchronously from the main thread
on macOS when necessary to always have a frame of the correct size
during a resize animation. This was necessary to allow the background to
be drawn by our GPU code (instead of setting a background color on the
layer) while still avoiding holes during resize.

The OpenGL backend now theoretically has access to multi-buffering, but
it's disabled (by setting the buffer count to 1) because it
synchronously waits for frames to complete anyway which means that the
extra buffers were just a waste of memory.

## Validation
To validate that there are no significant or obvious problems, I
exercised both backends with a variety of configurations, and visually
inspected the results. Everything looks to be in order.

The images are available in a gist here:
https://gist.github.com/qwerasd205/c1bd3e4c694d888e41612e53c0560179

## Memory
Here's a comparison of memory usage for ReleaseFast builds on macOS,
between `main` and this branch.
Memory figures given are values from Activity Monitor measuring windows
of the same size, with two tabs with 3 splits each.

||Before|After|
|-:|-|-|
|**Memory**|247.9 MB|224.2 MB|
|**Real Memory**|174.4 MB|172.5 MB|

Happily, the rework has slightly *reduced* the memory footprint- likely
due to removing the overhead of `CAMetalLayer`. (The footprint could be
reduced much further if we got rid of multi-buffering and satisfied
ourselves with blocking for each frame, but that's a discussion for
another day.)

If someone could do a similar comparison for Linux, that'd be much
appreciated!

## Notes / future work
- There are a couple structures that *can* be unified using the
abstraction layer, but I haven't gotten around to unifying yet.
Specifically, in `renderer/(opengl|metal)/`, there's `cell.zig` and
`image.zig`, both of which are substantially identical between the two
backends. `shaders.zig` may also be a candidate for unification, but
that might be *overly* DRY.
- ~~I did not double-check the documentation for config options, which
may mention whether certain options can be hot-reloaded; if it does then
that will need to be updated.~~ Fixed: be5908f
- The `fragCoord` for custom shaders originates at the top left for
Metal, but *bottom* left for OpenGL; fixing this will be a bit annoying,
since the screen texture is likewise vertically flipped between the two.
Some shaders rely on the fragcoord for things like falling particles, so
this does need to be fixed.
- `Target` should be improved to support multiple types of targets right
now it only represents a framebuffer or iosurface, but it should also be
able to represent a texture; right now a kind of messy tagged union is
used so that steps can accept both.
- Custom shader cursor uniforms (#6912) and terminal background images
(#4226, #5233) should be much more straightforward to implement on top
of this rework, and I plan to make follow-up PRs for them once this is
merged.
- I *do* want to do a rework of the pipelines themselves, since the way
we're rendering stuff is a bit messy currently, but this is already a
huge enough PR as it is- so for now the renderer still uses the same
rendering passes that Metal did before.
- We should probably add a system requirements section to the README
where we can note the minimum required OpenGL version of `4.3`, any even
slightly modern Linux system will support this, but it would be good to
document it somewhere user-facing anyway.

# TODO BEFORE MERGE
- [x] Have multiple people test this on both macOS and linux.
- [ ] ~~Have someone with a better dev setup on linux check for memory
leaks and other problems.~~ (Skipped, will merge and let tip users
figure this out, someone should *specifically* look for memory leaks
before the next versioned release though.)
- [x] Address any code review feedback.
This commit is contained in:
Qwerasd
2025-06-21 22:00:01 -06:00
committed by GitHub
76 changed files with 8567 additions and 7569 deletions

View File

@@ -158,10 +158,6 @@ extension Ghostty {
// We need to support being a first responder so that we can get input events
override var acceptsFirstResponder: Bool { return true }
// I don't think we need this but this lets us know we should redraw our layer
// so we'll use that to tell ghostty to refresh.
override var wantsUpdateLayer: Bool { return true }
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
self.markedText = NSMutableAttributedString()
self.uuid = uuid ?? .init()
@@ -732,11 +728,6 @@ extension Ghostty {
setSurfaceSize(width: UInt32(fbFrame.size.width), height: UInt32(fbFrame.size.height))
}
override func updateLayer() {
guard let surface = self.surface else { return }
ghostty_surface_draw(surface);
}
override func mouseDown(with event: NSEvent) {
guard let surface = self.surface else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags)

View File

@@ -2,6 +2,8 @@ pub const c = @import("animation/c.zig").c;
/// https://developer.apple.com/documentation/quartzcore/calayer/contents_gravity_values?language=objc
pub extern "c" const kCAGravityTopLeft: *anyopaque;
pub extern "c" const kCAGravityBottomLeft: *anyopaque;
pub extern "c" const kCAGravityCenter: *anyopaque;
test {
@import("std").testing.refAllDecls(@This());

View File

@@ -33,6 +33,7 @@ pub fn build(b: *std.Build) !void {
lib.linkFramework("CoreText");
lib.linkFramework("CoreVideo");
lib.linkFramework("QuartzCore");
lib.linkFramework("IOSurface");
if (target.result.os.tag == .macos) {
lib.linkFramework("Carbon");
module.linkFramework("Carbon", .{});
@@ -44,6 +45,7 @@ pub fn build(b: *std.Build) !void {
module.linkFramework("CoreText", .{});
module.linkFramework("CoreVideo", .{});
module.linkFramework("QuartzCore", .{});
module.linkFramework("IOSurface", .{});
try apple_sdk.addPaths(b, lib);
}

View File

@@ -3,6 +3,16 @@ pub const data = @import("dispatch/data.zig");
pub const queue = @import("dispatch/queue.zig");
pub const Data = data.Data;
pub extern "c" fn dispatch_sync(
queue: *anyopaque,
block: *anyopaque,
) void;
pub extern "c" fn dispatch_async(
queue: *anyopaque,
block: *anyopaque,
) void;
test {
@import("std").testing.refAllDecls(@This());
}

View File

@@ -30,6 +30,7 @@ pub const stringGetSurrogatePairForLongCharacter = string.stringGetSurrogatePair
pub const URL = url.URL;
pub const URLPathStyle = url.URLPathStyle;
pub const CFRelease = typepkg.CFRelease;
pub const CFRetain = typepkg.CFRetain;
test {
@import("std").testing.refAllDecls(@This());

View File

@@ -1 +1,2 @@
pub extern "c" fn CFRelease(*anyopaque) void;
pub extern "c" fn CFRetain(*anyopaque) void;

8
pkg/macos/iosurface.zig Normal file
View File

@@ -0,0 +1,8 @@
const iosurface = @import("iosurface/iosurface.zig");
pub const c = @import("iosurface/c.zig").c;
pub const IOSurface = iosurface.IOSurface;
test {
@import("std").testing.refAllDecls(@This());
}

View File

@@ -0,0 +1 @@
pub const c = @import("../main.zig").c;

View File

@@ -0,0 +1,136 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const c = @import("c.zig").c;
const foundation = @import("../foundation.zig");
const graphics = @import("../graphics.zig");
const video = @import("../video.zig");
pub const IOSurface = opaque {
pub const Error = error{
InvalidOperation,
};
pub const Properties = struct {
width: c_int,
height: c_int,
pixel_format: video.PixelFormat,
bytes_per_element: c_int,
colorspace: ?*graphics.ColorSpace,
};
pub fn init(properties: Properties) Allocator.Error!*IOSurface {
var w = try foundation.Number.create(.int, &properties.width);
defer w.release();
var h = try foundation.Number.create(.int, &properties.height);
defer h.release();
var pf = try foundation.Number.create(.int, &@as(c_int, @intFromEnum(properties.pixel_format)));
defer pf.release();
var bpe = try foundation.Number.create(.int, &properties.bytes_per_element);
defer bpe.release();
var properties_dict = try foundation.Dictionary.create(
&[_]?*const anyopaque{
c.kIOSurfaceWidth,
c.kIOSurfaceHeight,
c.kIOSurfacePixelFormat,
c.kIOSurfaceBytesPerElement,
},
&[_]?*const anyopaque{ w, h, pf, bpe },
);
defer properties_dict.release();
var surface = @as(?*IOSurface, @ptrFromInt(@intFromPtr(
c.IOSurfaceCreate(@ptrCast(properties_dict)),
))) orelse return error.OutOfMemory;
if (properties.colorspace) |space| {
surface.setColorSpace(space);
}
return surface;
}
pub fn deinit(self: *IOSurface) void {
// We mark it purgeable so that it is immediately unloaded, so that we
// don't have to wait for CoreFoundation garbage collection to trigger.
_ = c.IOSurfaceSetPurgeable(
@ptrCast(self),
c.kIOSurfacePurgeableEmpty,
null,
);
foundation.CFRelease(self);
}
pub fn retain(self: *IOSurface) void {
foundation.CFRetain(self);
}
pub fn release(self: *IOSurface) void {
foundation.CFRelease(self);
}
pub fn setColorSpace(self: *IOSurface, colorspace: *graphics.ColorSpace) void {
const serialized_colorspace = graphics.c.CGColorSpaceCopyPropertyList(
@ptrCast(colorspace),
).?;
defer foundation.CFRelease(@constCast(serialized_colorspace));
c.IOSurfaceSetValue(
@ptrCast(self),
c.kIOSurfaceColorSpace,
@ptrCast(serialized_colorspace),
);
}
pub inline fn lock(self: *IOSurface) void {
c.IOSurfaceLock(
@ptrCast(self),
0,
null,
);
}
pub inline fn unlock(self: *IOSurface) void {
c.IOSurfaceUnlock(
@ptrCast(self),
0,
null,
);
}
pub inline fn getAllocSize(self: *IOSurface) usize {
return c.IOSurfaceGetAllocSize(@ptrCast(self));
}
pub inline fn getWidth(self: *IOSurface) usize {
return c.IOSurfaceGetWidth(@ptrCast(self));
}
pub inline fn getHeight(self: *IOSurface) usize {
return c.IOSurfaceGetHeight(@ptrCast(self));
}
pub inline fn getBytesPerElement(self: *IOSurface) usize {
return c.IOSurfaceGetBytesPerElement(@ptrCast(self));
}
pub inline fn getBytesPerRow(self: *IOSurface) usize {
return c.IOSurfaceGetBytesPerRow(@ptrCast(self));
}
pub inline fn getBaseAddress(self: *IOSurface) ?[*]u8 {
return @ptrCast(c.IOSurfaceGetBaseAddress(@ptrCast(self)));
}
pub inline fn getElementWidth(self: *IOSurface) usize {
return c.IOSurfaceGetElementWidth(@ptrCast(self));
}
pub inline fn getElementHeight(self: *IOSurface) usize {
return c.IOSurfaceGetElementHeight(@ptrCast(self));
}
pub inline fn getPixelFormat(self: *IOSurface) video.PixelFormat {
return @enumFromInt(c.IOSurfaceGetPixelFormat(@ptrCast(self)));
}
};

View File

@@ -8,6 +8,7 @@ pub const graphics = @import("graphics.zig");
pub const os = @import("os.zig");
pub const text = @import("text.zig");
pub const video = @import("video.zig");
pub const iosurface = @import("iosurface.zig");
// All of our C imports consolidated into one place. We used to
// import them one by one in each package but Zig 0.14 has some
@@ -17,7 +18,9 @@ pub const c = @cImport({
@cInclude("CoreGraphics/CoreGraphics.h");
@cInclude("CoreText/CoreText.h");
@cInclude("CoreVideo/CoreVideo.h");
@cInclude("CoreVideo/CVPixelBuffer.h");
@cInclude("QuartzCore/CALayer.h");
@cInclude("IOSurface/IOSurfaceRef.h");
@cInclude("dispatch/dispatch.h");
@cInclude("os/log.h");

View File

@@ -1,7 +1,9 @@
const display_link = @import("video/display_link.zig");
const pixel_format = @import("video/pixel_format.zig");
pub const c = @import("video/c.zig").c;
pub const DisplayLink = display_link.DisplayLink;
pub const PixelFormat = pixel_format.PixelFormat;
test {
@import("std").testing.refAllDecls(@This());

View File

@@ -0,0 +1,171 @@
const c = @import("c.zig").c;
pub const PixelFormat = enum(c_int) {
/// 1 bit indexed
@"1Monochrome" = c.kCVPixelFormatType_1Monochrome,
/// 2 bit indexed
@"2Indexed" = c.kCVPixelFormatType_2Indexed,
/// 4 bit indexed
@"4Indexed" = c.kCVPixelFormatType_4Indexed,
/// 8 bit indexed
@"8Indexed" = c.kCVPixelFormatType_8Indexed,
/// 1 bit indexed gray, white is zero
@"1IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_1IndexedGray_WhiteIsZero,
/// 2 bit indexed gray, white is zero
@"2IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_2IndexedGray_WhiteIsZero,
/// 4 bit indexed gray, white is zero
@"4IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_4IndexedGray_WhiteIsZero,
/// 8 bit indexed gray, white is zero
@"8IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_8IndexedGray_WhiteIsZero,
/// 16 bit BE RGB 555
@"16BE555" = c.kCVPixelFormatType_16BE555,
/// 16 bit LE RGB 555
@"16LE555" = c.kCVPixelFormatType_16LE555,
/// 16 bit LE RGB 5551
@"16LE5551" = c.kCVPixelFormatType_16LE5551,
/// 16 bit BE RGB 565
@"16BE565" = c.kCVPixelFormatType_16BE565,
/// 16 bit LE RGB 565
@"16LE565" = c.kCVPixelFormatType_16LE565,
/// 24 bit RGB
@"24RGB" = c.kCVPixelFormatType_24RGB,
/// 24 bit BGR
@"24BGR" = c.kCVPixelFormatType_24BGR,
/// 32 bit ARGB
@"32ARGB" = c.kCVPixelFormatType_32ARGB,
/// 32 bit BGRA
@"32BGRA" = c.kCVPixelFormatType_32BGRA,
/// 32 bit ABGR
@"32ABGR" = c.kCVPixelFormatType_32ABGR,
/// 32 bit RGBA
@"32RGBA" = c.kCVPixelFormatType_32RGBA,
/// 64 bit ARGB, 16-bit big-endian samples
@"64ARGB" = c.kCVPixelFormatType_64ARGB,
/// 64 bit RGBA, 16-bit little-endian full-range (0-65535) samples
@"64RGBALE" = c.kCVPixelFormatType_64RGBALE,
/// 48 bit RGB, 16-bit big-endian samples
@"48RGB" = c.kCVPixelFormatType_48RGB,
/// 32 bit AlphaGray, 16-bit big-endian samples, black is zero
@"32AlphaGray" = c.kCVPixelFormatType_32AlphaGray,
/// 16 bit Grayscale, 16-bit big-endian samples, black is zero
@"16Gray" = c.kCVPixelFormatType_16Gray,
/// 30 bit RGB, 10-bit big-endian samples, 2 unused padding bits (at least significant end).
@"30RGB" = c.kCVPixelFormatType_30RGB,
/// 30 bit RGB, 10-bit big-endian samples, 2 unused padding bits (at most significant end), video-range (64-940).
@"30RGB_r210" = c.kCVPixelFormatType_30RGB_r210,
/// Component Y'CbCr 8-bit 4:2:2, ordered Cb Y'0 Cr Y'1
@"422YpCbCr8" = c.kCVPixelFormatType_422YpCbCr8,
/// Component Y'CbCrA 8-bit 4:4:4:4, ordered Cb Y' Cr A
@"4444YpCbCrA8" = c.kCVPixelFormatType_4444YpCbCrA8,
/// Component Y'CbCrA 8-bit 4:4:4:4, rendering format. full range alpha, zero biased YUV, ordered A Y' Cb Cr
@"4444YpCbCrA8R" = c.kCVPixelFormatType_4444YpCbCrA8R,
/// Component Y'CbCrA 8-bit 4:4:4:4, ordered A Y' Cb Cr, full range alpha, video range Y'CbCr.
@"4444AYpCbCr8" = c.kCVPixelFormatType_4444AYpCbCr8,
/// Component Y'CbCrA 16-bit 4:4:4:4, ordered A Y' Cb Cr, full range alpha, video range Y'CbCr, 16-bit little-endian samples.
@"4444AYpCbCr16" = c.kCVPixelFormatType_4444AYpCbCr16,
/// Component AY'CbCr single precision floating-point 4:4:4:4
@"4444AYpCbCrFloat" = c.kCVPixelFormatType_4444AYpCbCrFloat,
/// Component Y'CbCr 8-bit 4:4:4, ordered Cr Y' Cb, video range Y'CbCr
@"444YpCbCr8" = c.kCVPixelFormatType_444YpCbCr8,
/// Component Y'CbCr 10,12,14,16-bit 4:2:2
@"422YpCbCr16" = c.kCVPixelFormatType_422YpCbCr16,
/// Component Y'CbCr 10-bit 4:2:2
@"422YpCbCr10" = c.kCVPixelFormatType_422YpCbCr10,
/// Component Y'CbCr 10-bit 4:4:4
@"444YpCbCr10" = c.kCVPixelFormatType_444YpCbCr10,
/// Planar Component Y'CbCr 8-bit 4:2:0. baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrPlanar struct
@"420YpCbCr8Planar" = c.kCVPixelFormatType_420YpCbCr8Planar,
/// Planar Component Y'CbCr 8-bit 4:2:0, full range. baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrPlanar struct
@"420YpCbCr8PlanarFullRange" = c.kCVPixelFormatType_420YpCbCr8PlanarFullRange,
/// First plane: Video-range Component Y'CbCr 8-bit 4:2:2, ordered Cb Y'0 Cr Y'1; second plane: alpha 8-bit 0-255
@"422YpCbCr_4A_8BiPlanar" = c.kCVPixelFormatType_422YpCbCr_4A_8BiPlanar,
/// Bi-Planar Component Y'CbCr 8-bit 4:2:0, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
@"420YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
/// Bi-Planar Component Y'CbCr 8-bit 4:2:0, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
@"420YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
/// Bi-Planar Component Y'CbCr 8-bit 4:2:2, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
@"422YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange,
/// Bi-Planar Component Y'CbCr 8-bit 4:2:2, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
@"422YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_422YpCbCr8BiPlanarFullRange,
/// Bi-Planar Component Y'CbCr 8-bit 4:4:4, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
@"444YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange,
/// Bi-Planar Component Y'CbCr 8-bit 4:4:4, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
@"444YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_444YpCbCr8BiPlanarFullRange,
/// Component Y'CbCr 8-bit 4:2:2, ordered Y'0 Cb Y'1 Cr
@"422YpCbCr8_yuvs" = c.kCVPixelFormatType_422YpCbCr8_yuvs,
/// Component Y'CbCr 8-bit 4:2:2, full range, ordered Y'0 Cb Y'1 Cr
@"422YpCbCr8FullRange" = c.kCVPixelFormatType_422YpCbCr8FullRange,
/// 8 bit one component, black is zero
OneComponent8 = c.kCVPixelFormatType_OneComponent8,
/// 8 bit two component, black is zero
TwoComponent8 = c.kCVPixelFormatType_TwoComponent8,
/// little-endian RGB101010, 2 MSB are ignored, wide-gamut (384-895)
@"30RGBLEPackedWideGamut" = c.kCVPixelFormatType_30RGBLEPackedWideGamut,
/// little-endian ARGB2101010 full-range ARGB
ARGB2101010LEPacked = c.kCVPixelFormatType_ARGB2101010LEPacked,
/// little-endian ARGB10101010, each 10 bits in the MSBs of 16bits, wide-gamut (384-895, including alpha)
@"40ARGBLEWideGamut" = c.kCVPixelFormatType_40ARGBLEWideGamut,
/// little-endian ARGB10101010, each 10 bits in the MSBs of 16bits, wide-gamut (384-895, including alpha). Alpha premultiplied
@"40ARGBLEWideGamutPremultiplied" = c.kCVPixelFormatType_40ARGBLEWideGamutPremultiplied,
/// 10 bit little-endian one component, stored as 10 MSBs of 16 bits, black is zero
OneComponent10 = c.kCVPixelFormatType_OneComponent10,
/// 12 bit little-endian one component, stored as 12 MSBs of 16 bits, black is zero
OneComponent12 = c.kCVPixelFormatType_OneComponent12,
/// 16 bit little-endian one component, black is zero
OneComponent16 = c.kCVPixelFormatType_OneComponent16,
/// 16 bit little-endian two component, black is zero
TwoComponent16 = c.kCVPixelFormatType_TwoComponent16,
/// 16 bit one component IEEE half-precision float, 16-bit little-endian samples
OneComponent16Half = c.kCVPixelFormatType_OneComponent16Half,
/// 32 bit one component IEEE float, 32-bit little-endian samples
OneComponent32Float = c.kCVPixelFormatType_OneComponent32Float,
/// 16 bit two component IEEE half-precision float, 16-bit little-endian samples
TwoComponent16Half = c.kCVPixelFormatType_TwoComponent16Half,
/// 32 bit two component IEEE float, 32-bit little-endian samples
TwoComponent32Float = c.kCVPixelFormatType_TwoComponent32Float,
/// 64 bit RGBA IEEE half-precision float, 16-bit little-endian samples
@"64RGBAHalf" = c.kCVPixelFormatType_64RGBAHalf,
/// 128 bit RGBA IEEE float, 32-bit little-endian samples
@"128RGBAFloat" = c.kCVPixelFormatType_128RGBAFloat,
/// Bayer 14-bit Little-Endian, packed in 16-bits, ordered G R G R... alternating with B G B G...
@"14Bayer_GRBG" = c.kCVPixelFormatType_14Bayer_GRBG,
/// Bayer 14-bit Little-Endian, packed in 16-bits, ordered R G R G... alternating with G B G B...
@"14Bayer_RGGB" = c.kCVPixelFormatType_14Bayer_RGGB,
/// Bayer 14-bit Little-Endian, packed in 16-bits, ordered B G B G... alternating with G R G R...
@"14Bayer_BGGR" = c.kCVPixelFormatType_14Bayer_BGGR,
/// Bayer 14-bit Little-Endian, packed in 16-bits, ordered G B G B... alternating with R G R G...
@"14Bayer_GBRG" = c.kCVPixelFormatType_14Bayer_GBRG,
/// IEEE754-2008 binary16 (half float), describing the normalized shift when comparing two images. Units are 1/meters: ( pixelShift / (pixelFocalLength * baselineInMeters) )
DisparityFloat16 = c.kCVPixelFormatType_DisparityFloat16,
/// IEEE754-2008 binary32 float, describing the normalized shift when comparing two images. Units are 1/meters: ( pixelShift / (pixelFocalLength * baselineInMeters) )
DisparityFloat32 = c.kCVPixelFormatType_DisparityFloat32,
/// IEEE754-2008 binary16 (half float), describing the depth (distance to an object) in meters
DepthFloat16 = c.kCVPixelFormatType_DepthFloat16,
/// IEEE754-2008 binary32 float, describing the depth (distance to an object) in meters
DepthFloat32 = c.kCVPixelFormatType_DepthFloat32,
/// 2 plane YCbCr10 4:2:0, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960])
@"420YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange,
/// 2 plane YCbCr10 4:2:2, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960])
@"422YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange,
/// 2 plane YCbCr10 4:4:4, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960])
@"444YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange,
/// 2 plane YCbCr10 4:2:0, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023)
@"420YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_420YpCbCr10BiPlanarFullRange,
/// 2 plane YCbCr10 4:2:2, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023)
@"422YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_422YpCbCr10BiPlanarFullRange,
/// 2 plane YCbCr10 4:4:4, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023)
@"444YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_444YpCbCr10BiPlanarFullRange,
/// first and second planes as per 420YpCbCr8BiPlanarVideoRange (420v), alpha 8 bits in third plane full-range. No CVPlanarPixelBufferInfo struct.
@"420YpCbCr8VideoRange_8A_TriPlanar" = c.kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar,
/// Single plane Bayer 16-bit little-endian sensor element ("sensel".*) samples from full-size decoding of ProRes RAW images; Bayer pattern (sensel ordering) and other raw conversion information is described via buffer attachments
@"16VersatileBayer" = c.kCVPixelFormatType_16VersatileBayer,
/// Single plane 64-bit RGBA (16-bit little-endian samples) from downscaled decoding of ProRes RAW images; components--which may not be co-sited with one another--are sensel values and require raw conversion, information for which is described via buffer attachments
@"64RGBA_DownscaledProResRAW" = c.kCVPixelFormatType_64RGBA_DownscaledProResRAW,
/// 2 plane YCbCr16 4:2:2, video-range (luma=[4096,60160] chroma=[4096,61440])
@"422YpCbCr16BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr16BiPlanarVideoRange,
/// 2 plane YCbCr16 4:4:4, video-range (luma=[4096,60160] chroma=[4096,61440])
@"444YpCbCr16BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr16BiPlanarVideoRange,
/// 3 plane video-range YCbCr16 4:4:4 with 16-bit full-range alpha (luma=[4096,60160] chroma=[4096,61440] alpha=[0,65535]). No CVPlanarPixelBufferInfo struct.
@"444YpCbCr16VideoRange_16A_TriPlanar" = c.kCVPixelFormatType_444YpCbCr16VideoRange_16A_TriPlanar,
_,
};

View File

@@ -51,7 +51,7 @@ pub const Binding = struct {
data: anytype,
usage: Usage,
) !void {
const info = dataInfo(&data);
const info = dataInfo(data);
glad.context.BufferData.?(
@intFromEnum(b.target),
info.size,
@@ -136,10 +136,6 @@ pub const Binding = struct {
};
}
pub 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.
@@ -230,6 +226,7 @@ pub const Target = enum(c_uint) {
array = c.GL_ARRAY_BUFFER,
element_array = c.GL_ELEMENT_ARRAY_BUFFER,
uniform = c.GL_UNIFORM_BUFFER,
storage = c.GL_SHADER_STORAGE_BUFFER,
_,
};

View File

@@ -5,6 +5,7 @@ const c = @import("c.zig").c;
const errors = @import("errors.zig");
const glad = @import("glad.zig");
const Texture = @import("Texture.zig");
const Renderbuffer = @import("Renderbuffer.zig");
id: c.GLuint,
@@ -86,6 +87,29 @@ pub const Binding = struct {
try errors.getError();
}
pub fn renderbuffer(
self: Binding,
attachment: Attachment,
buffer: Renderbuffer,
) !void {
glad.context.FramebufferRenderbuffer.?(
@intFromEnum(self.target),
@intFromEnum(attachment),
c.GL_RENDERBUFFER,
buffer.id,
);
try errors.getError();
}
pub fn drawBuffers(
self: Binding,
bufs: []Attachment,
) !void {
_ = self;
glad.context.DrawBuffers.?(@intCast(bufs.len), bufs.ptr);
try errors.getError();
}
pub fn checkStatus(self: Binding) Status {
return @enumFromInt(glad.context.CheckFramebufferStatus.?(@intFromEnum(self.target)));
}

View File

@@ -0,0 +1,56 @@
const Renderbuffer = @This();
const std = @import("std");
const c = @import("c.zig").c;
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() !Renderbuffer {
var rbo: c.GLuint = undefined;
glad.context.GenRenderbuffers.?(1, &rbo);
return .{ .id = rbo };
}
pub fn destroy(v: Renderbuffer) void {
glad.context.DeleteRenderbuffers.?(1, &v.id);
}
pub fn bind(v: Renderbuffer) !Binding {
// Keep track of the previous binding so we can restore it in unbind.
var current: c.GLint = undefined;
glad.context.GetIntegerv.?(c.GL_RENDERBUFFER_BINDING, &current);
glad.context.BindRenderbuffer.?(c.GL_RENDERBUFFER, v.id);
return .{ .previous = @intCast(current) };
}
pub const Binding = struct {
previous: c.GLuint,
pub fn unbind(self: Binding) void {
glad.context.BindRenderbuffer.?(
c.GL_RENDERBUFFER,
self.previous,
);
}
pub fn storage(
self: Binding,
format: Texture.InternalFormat,
width: c.GLsizei,
height: c.GLsizei,
) !void {
_ = self;
glad.context.RenderbufferStorage.?(
c.GL_RENDERBUFFER,
@intCast(@intFromEnum(format)),
width,
height,
);
try errors.getError();
}
};

View File

@@ -7,15 +7,16 @@ const glad = @import("glad.zig");
id: c.GLuint,
pub fn active(target: c.GLenum) !void {
glad.context.ActiveTexture.?(target);
pub fn active(index: c_uint) errors.Error!void {
glad.context.ActiveTexture.?(index + c.GL_TEXTURE0);
try errors.getError();
}
/// Create a single texture.
pub fn create() !Texture {
pub fn create() errors.Error!Texture {
var id: c.GLuint = undefined;
glad.context.GenTextures.?(1, &id);
try errors.getError();
return .{ .id = id };
}
@@ -30,7 +31,7 @@ pub fn destroy(v: Texture) void {
glad.context.DeleteTextures.?(1, &v.id);
}
/// Enun for possible texture binding targets.
/// Enum for possible texture binding targets.
pub const Target = enum(c_uint) {
@"1D" = c.GL_TEXTURE_1D,
@"2D" = c.GL_TEXTURE_2D,
@@ -67,11 +68,11 @@ pub const Parameter = enum(c_uint) {
/// Internal format enum for texture images.
pub const InternalFormat = enum(c_int) {
red = c.GL_RED,
rgb = c.GL_RGB,
rgba = c.GL_RGBA,
rgb = c.GL_RGB8,
rgba = c.GL_RGBA8,
srgb = c.GL_SRGB,
srgba = c.GL_SRGB_ALPHA,
srgb = c.GL_SRGB8,
srgba = c.GL_SRGB8_ALPHA8,
// There are so many more that I haven't filled in.
_,
@@ -107,7 +108,7 @@ pub const Binding = struct {
glad.context.GenerateMipmap.?(@intFromEnum(b.target));
}
pub fn parameter(b: Binding, name: Parameter, value: anytype) !void {
pub fn parameter(b: Binding, name: Parameter, value: anytype) errors.Error!void {
switch (@TypeOf(value)) {
c.GLint => glad.context.TexParameteri.?(
@intFromEnum(b.target),
@@ -116,6 +117,7 @@ pub const Binding = struct {
),
else => unreachable,
}
try errors.getError();
}
pub fn image2D(
@@ -128,7 +130,7 @@ pub const Binding = struct {
format: Format,
typ: DataType,
data: ?*const anyopaque,
) !void {
) errors.Error!void {
glad.context.TexImage2D.?(
@intFromEnum(b.target),
level,
@@ -140,6 +142,7 @@ pub const Binding = struct {
@intFromEnum(typ),
data,
);
try errors.getError();
}
pub fn subImage2D(
@@ -152,7 +155,7 @@ pub const Binding = struct {
format: Format,
typ: DataType,
data: ?*const anyopaque,
) !void {
) errors.Error!void {
glad.context.TexSubImage2D.?(
@intFromEnum(b.target),
level,
@@ -164,6 +167,7 @@ pub const Binding = struct {
@intFromEnum(typ),
data,
);
try errors.getError();
}
pub fn copySubImage2D(
@@ -175,7 +179,17 @@ pub const Binding = struct {
y: c.GLint,
width: c.GLsizei,
height: c.GLsizei,
) !void {
glad.context.CopyTexSubImage2D.?(@intFromEnum(b.target), level, xoffset, yoffset, x, y, width, height);
) errors.Error!void {
glad.context.CopyTexSubImage2D.?(
@intFromEnum(b.target),
level,
xoffset,
yoffset,
x,
y,
width,
height,
);
try errors.getError();
}
};

View File

@@ -29,4 +29,88 @@ pub const Binding = struct {
_ = self;
glad.context.BindVertexArray.?(0);
}
pub fn enableAttribArray(_: Binding, idx: c.GLuint) !void {
glad.context.EnableVertexAttribArray.?(idx);
try errors.getError();
}
pub fn bindingDivisor(_: Binding, idx: c.GLuint, divisor: c.GLuint) !void {
glad.context.VertexBindingDivisor.?(idx, divisor);
try errors.getError();
}
pub fn attributeBinding(
_: Binding,
attrib_idx: c.GLuint,
binding_idx: c.GLuint,
) !void {
glad.context.VertexAttribBinding.?(attrib_idx, binding_idx);
try errors.getError();
}
pub fn attributeFormat(
_: Binding,
idx: c.GLuint,
size: c.GLint,
typ: c.GLenum,
normalized: bool,
offset: c.GLuint,
) !void {
glad.context.VertexAttribFormat.?(
idx,
size,
typ,
@intCast(@intFromBool(normalized)),
offset,
);
try errors.getError();
}
pub fn attributeIFormat(
_: Binding,
idx: c.GLuint,
size: c.GLint,
typ: c.GLenum,
offset: c.GLuint,
) !void {
glad.context.VertexAttribIFormat.?(
idx,
size,
typ,
offset,
);
try errors.getError();
}
pub fn attributeLFormat(
_: Binding,
idx: c.GLuint,
size: c.GLint,
offset: c.GLuint,
) !void {
glad.context.VertexAttribLFormat.?(
idx,
size,
c.GL_DOUBLE,
offset,
);
try errors.getError();
}
pub fn bindVertexBuffer(
_: Binding,
idx: c.GLuint,
buffer: c.GLuint,
offset: c.GLintptr,
stride: c.GLsizei,
) !void {
glad.context.BindVertexBuffer.?(
idx,
buffer,
offset,
stride,
);
try errors.getError();
}
};

View File

@@ -1,6 +1,7 @@
const c = @import("c.zig").c;
const errors = @import("errors.zig");
const glad = @import("glad.zig");
const Primitive = @import("primitives.zig").Primitive;
pub fn clearColor(r: f32, g: f32, b: f32, a: f32) void {
glad.context.ClearColor.?(r, g, b, a);
@@ -15,6 +16,21 @@ pub fn drawArrays(mode: c.GLenum, first: c.GLint, count: c.GLsizei) !void {
try errors.getError();
}
pub fn drawArraysInstanced(
mode: Primitive,
first: c.GLint,
count: c.GLsizei,
primcount: c.GLsizei,
) !void {
glad.context.DrawArraysInstanced.?(
@intCast(@intFromEnum(mode)),
first,
count,
primcount,
);
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);
@@ -25,9 +41,15 @@ pub fn drawElementsInstanced(
mode: c.GLenum,
count: c.GLsizei,
typ: c.GLenum,
primcount: usize,
primcount: c.GLsizei,
) !void {
glad.context.DrawElementsInstanced.?(mode, count, typ, null, @intCast(primcount));
glad.context.DrawElementsInstanced.?(
mode,
count,
typ,
null,
primcount,
);
try errors.getError();
}
@@ -36,6 +58,11 @@ pub fn enable(cap: c.GLenum) !void {
try errors.getError();
}
pub fn disable(cap: c.GLenum) !void {
glad.context.Disable.?(cap);
try errors.getError();
}
pub fn frontFace(mode: c.GLenum) !void {
glad.context.FrontFace.?(mode);
try errors.getError();
@@ -57,3 +84,11 @@ pub fn pixelStore(mode: c.GLenum, value: anytype) !void {
}
try errors.getError();
}
pub fn finish() void {
glad.context.Finish.?();
}
pub fn flush() void {
glad.context.Flush.?();
}

View File

@@ -16,20 +16,29 @@ pub const glad = @import("glad.zig");
pub const ext = @import("extensions.zig");
pub const Buffer = @import("Buffer.zig");
pub const Framebuffer = @import("Framebuffer.zig");
pub const Renderbuffer = @import("Renderbuffer.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");
pub const errors = @import("errors.zig");
pub const Primitive = @import("primitives.zig").Primitive;
const draw = @import("draw.zig");
pub const blendFunc = draw.blendFunc;
pub const clear = draw.clear;
pub const clearColor = draw.clearColor;
pub const drawArrays = draw.drawArrays;
pub const drawArraysInstanced = draw.drawArraysInstanced;
pub const drawElements = draw.drawElements;
pub const drawElementsInstanced = draw.drawElementsInstanced;
pub const enable = draw.enable;
pub const disable = draw.disable;
pub const frontFace = draw.frontFace;
pub const pixelStore = draw.pixelStore;
pub const viewport = draw.viewport;
pub const flush = draw.flush;
pub const finish = draw.finish;

18
pkg/opengl/primitives.zig Normal file
View File

@@ -0,0 +1,18 @@
pub const c = @import("c.zig").c;
pub const Primitive = enum(c_int) {
point = c.GL_POINTS,
line = c.GL_LINES,
line_strip = c.GL_LINE_STRIP,
triangle = c.GL_TRIANGLES,
triangle_strip = c.GL_TRIANGLE_STRIP,
// Commented out primitive types are excluded for parity with Metal.
//
// line_loop = c.GL_LINE_LOOP,
// line_adjacency = c.GL_LINES_ADJACENCY,
// line_strip_adjacency = c.GL_LINE_STRIP_ADJACENCY,
// triangle_fan = c.GL_TRIANGLE_FAN,
// triangle_adjacency = c.GL_TRIANGLES_ADJACENCY,
// triangle_strip_adjacency = c.GL_TRIANGLE_STRIP_ADJACENCY,
};

View File

@@ -468,6 +468,7 @@ pub fn init(
.size = size,
.surface_mailbox = .{ .surface = self, .app = app_mailbox },
.rt_surface = rt_surface,
.thread = &self.renderer_thread,
});
errdefer renderer_impl.deinit();
@@ -726,7 +727,9 @@ pub fn close(self: *Surface) void {
/// is in the middle of animation (such as a resize, etc.) or when
/// the render timer is managed manually by the apprt.
pub fn draw(self: *Surface) !void {
try self.renderer_thread.draw_now.notify();
// Renderers are required to support `drawFrame` being called from
// the main thread, so that they can update contents during resize.
try self.renderer.drawFrame(true);
}
/// Activate the inspector. This will begin collecting inspection data.

View File

@@ -55,6 +55,11 @@ pub const c = @cImport({
const log = std.log.scoped(.gtk);
/// This is detected by the Renderer, in which case it sends a `redraw_surface`
/// message so that we can call `drawFrame` ourselves from the app thread,
/// because GTK's `GLArea` does not support drawing from a different thread.
pub const must_draw_from_app_thread = true;
pub const Options = struct {};
core_app: *CoreApp,

View File

@@ -41,10 +41,6 @@ const adw_version = @import("adw_version.zig");
const log = std.log.scoped(.gtk_surface);
/// This is detected by the OpenGL renderer to move to a single-threaded
/// draw operation. This basically puts locks around our draw path.
pub const opengl_single_threaded_draw = true;
pub const Options = struct {
/// The parent surface to inherit settings such as font size, working
/// directory, etc. from.
@@ -394,7 +390,10 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
// Various other GL properties
gl_area_widget.setCursorFromName("text");
gl_area.setRequiredVersion(3, 3);
gl_area.setRequiredVersion(
renderer.OpenGL.MIN_VERSION_MAJOR,
renderer.OpenGL.MIN_VERSION_MINOR,
);
gl_area.setHasStencilBuffer(0);
gl_area.setHasDepthBuffer(0);
gl_area.setUseEs(0);
@@ -683,12 +682,13 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
fn realize(self: *Surface) !void {
// If this surface has already been realized, then we don't need to
// reinitialize. This can happen if a surface is moved from one GDK surface
// to another (i.e. a tab is pulled out into a window).
// reinitialize. This can happen if a surface is moved from one GDK
// surface to another (i.e. a tab is pulled out into a window).
if (self.realized) {
// If we have no OpenGL state though, we do need to reinitialize.
// We allow the renderer to figure that out
try self.core_surface.renderer.displayRealize();
// We allow the renderer to figure that out, and then queue a draw.
try self.core_surface.renderer.displayRealized();
self.redraw();
return;
}
@@ -794,7 +794,7 @@ pub fn primaryWidget(self: *Surface) *gtk.Widget {
}
fn render(self: *Surface) !void {
try self.core_surface.renderer.drawFrame(self);
try self.core_surface.renderer.drawFrame(true);
}
/// Called by core surface to get the cgroup.

View File

@@ -266,6 +266,9 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// This affects the appearance of text and of any images with transparency.
/// Additionally, custom shaders will receive colors in the configured space.
///
/// On macOS the default is `native`, on all other platforms the default is
/// `linear-corrected`.
///
/// Valid values:
///
/// * `native` - Perform alpha blending in the native color space for the OS.
@@ -276,12 +279,15 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// when certain color combinations are used (e.g. red / green), but makes
/// dark text look much thinner than normal and light text much thicker.
/// This is also sometimes known as "gamma correction".
/// (Currently only supported on macOS. Has no effect on Linux.)
///
/// * `linear-corrected` - Same as `linear`, but with a correction step applied
/// for text that makes it look nearly or completely identical to `native`,
/// but without any of the darkening artifacts.
@"alpha-blending": AlphaBlending = .native,
@"alpha-blending": AlphaBlending =
if (builtin.os.tag == .macos)
.native
else
.@"linear-corrected",
/// All of the configurations behavior adjust various metrics determined by the
/// font. The values can be integers (1, -1, etc.) or a percentage (20%, -15%,
@@ -1961,9 +1967,6 @@ 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.
@@ -1977,8 +1980,7 @@ 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.
/// This can be changed at runtime and will affect all open terminals.
@"custom-shader": RepeatablePath = .{},
/// If `true` (default), the focused terminal surface will run an animation
@@ -1996,8 +1998,7 @@ keybind: Keybinds = .{},
/// will use more CPU per terminal surface and can become quite expensive
/// depending on the shader and your terminal usage.
///
/// This value can be changed at runtime and will affect all currently
/// open terminals.
/// This can be changed at runtime and will affect all open terminals.
@"custom-shader-animation": CustomShaderAnimation = .true,
/// Bell features to enable if bell support is available in your runtime. Not

View File

@@ -81,6 +81,13 @@ pub fn init(gpa: Allocator) !void {
fn initThread(gpa: Allocator) !void {
if (comptime !build_options.sentry) return;
// Right now, on Darwin, `std.Thread.setName` can only name the current
// thread, and we have no way to get the current thread from within it,
// so instead we use this code to name the thread instead.
if (builtin.os.tag.isDarwin()) {
internal_os.macos.pthread_setname_np(&"sentry-init".*);
}
var arena = std.heap.ArenaAllocator.init(gpa);
defer arena.deinit();
const alloc = arena.allocator();

View File

@@ -8,6 +8,7 @@ const std = @import("std");
const builtin = @import("builtin");
const macos = @import("macos");
const internal_os = @import("../os/main.zig");
const xev = @import("../global.zig").xev;
const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue;
@@ -119,6 +120,13 @@ pub fn threadMain(self: *Thread) void {
fn threadMain_(self: *Thread) !void {
defer log.debug("cf release thread exited", .{});
// Right now, on Darwin, `std.Thread.setName` can only name the current
// thread, and we have no way to get the current thread from within it,
// so instead we use this code to name the thread instead.
if (builtin.os.tag.isDarwin()) {
internal_os.macos.pthread_setname_np(&"cf_release".*);
}
// Start the async handlers. We start these first so that they're
// registered even if anything below fails so we can drain the mailbox.
self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback);

View File

@@ -88,6 +88,10 @@ extern "c" fn pthread_set_qos_class_self_np(
relative_priority: c_int,
) c_int;
pub extern "c" fn pthread_setname_np(
name: [*:0]const u8,
) void;
pub const NSOperatingSystemVersion = extern struct {
major: i64,
minor: i64,

View File

@@ -16,6 +16,7 @@ const cursor = @import("renderer/cursor.zig");
const message = @import("renderer/message.zig");
const size = @import("renderer/size.zig");
pub const shadertoy = @import("renderer/shadertoy.zig");
pub const GenericRenderer = @import("renderer/generic.zig").Renderer;
pub const Metal = @import("renderer/Metal.zig");
pub const OpenGL = @import("renderer/OpenGL.zig");
pub const WebGL = @import("renderer/WebGL.zig");
@@ -56,8 +57,8 @@ pub const Impl = enum {
/// The implementation to use for the renderer. This is comptime chosen
/// so that every build has exactly one renderer implementation.
pub const Renderer = switch (build_config.renderer) {
.metal => Metal,
.opengl => OpenGL,
.metal => GenericRenderer(Metal),
.opengl => GenericRenderer(OpenGL),
.webgl => WebGL,
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -20,3 +20,6 @@ surface_mailbox: apprt.surface.Mailbox,
/// The apprt surface.
rt_surface: *apprt.Surface,
/// The renderer thread.
thread: *renderer.Thread,

View File

@@ -20,6 +20,16 @@ const log = std.log.scoped(.renderer_thread);
const DRAW_INTERVAL = 8; // 120 FPS
const CURSOR_BLINK_INTERVAL = 600;
/// Whether calls to `drawFrame` must be done from the app thread.
///
/// If this is `true` then we send a `redraw_surface` message to the apprt
/// whenever we need to draw instead of calling `drawFrame` directly.
const must_draw_from_app_thread =
if (@hasDecl(apprt.App, "must_draw_from_app_thread"))
apprt.App.must_draw_from_app_thread
else
false;
/// The type used for sending messages to the IO thread. For now this is
/// hardcoded with a capacity. We can make this a comptime parameter in
/// the future if we want it configurable.
@@ -198,6 +208,13 @@ pub fn threadMain(self: *Thread) void {
fn threadMain_(self: *Thread) !void {
defer log.debug("renderer thread exited", .{});
// Right now, on Darwin, `std.Thread.setName` can only name the current
// thread, and we have no way to get the current thread from within it,
// so instead we use this code to name the thread instead.
if (builtin.os.tag.isDarwin()) {
internal_os.macos.pthread_setname_np(&"renderer".*);
}
// Setup our crash metadata
crash.sentry.thread_state = .{
.type = .renderer,
@@ -307,6 +324,16 @@ fn stopDrawTimer(self: *Thread) void {
/// Drain the mailbox.
fn drainMailbox(self: *Thread) !void {
// There's probably a more elegant way to do this...
//
// This is effectively an @autoreleasepool{} block, which we need in
// order to ensure that autoreleased objects are properly released.
const pool = if (builtin.os.tag.isDarwin())
@import("objc").AutoreleasePool.init()
else
void;
defer if (builtin.os.tag.isDarwin()) pool.deinit();
while (self.mailbox.pop()) |message| {
log.debug("mailbox message={}", .{message});
switch (message) {
@@ -425,7 +452,7 @@ fn drainMailbox(self: *Thread) !void {
self.renderer.markDirty();
},
.resize => |v| try self.renderer.setScreenSize(v),
.resize => |v| self.renderer.setScreenSize(v),
.change_config => |config| {
defer config.alloc.destroy(config.thread);
@@ -461,20 +488,16 @@ fn drawFrame(self: *Thread, now: bool) void {
if (!self.flags.visible) return;
// If the renderer is managing a vsync on its own, we only draw
// when we're forced to via now.
// when we're forced to via `now`.
if (!now and self.renderer.hasVsync()) return;
// If we're doing single-threaded GPU calls then we just wake up the
// app thread to redraw at this point.
if (rendererpkg.Renderer == rendererpkg.OpenGL and
rendererpkg.OpenGL.single_threaded_draw)
{
if (must_draw_from_app_thread) {
_ = self.app_mailbox.push(
.{ .redraw_surface = self.surface },
.{ .instant = {} },
);
} else {
self.renderer.drawFrame(self.surface) catch |err|
self.renderer.drawFrame(false) catch |err|
log.warn("error drawing err={}", .{err});
}
}
@@ -582,7 +605,6 @@ fn renderCallback(
// Update our frame data
t.renderer.updateFrame(
t.surface,
t.state,
t.flags.cursor_blink_visible,
) catch |err|

2868
src/renderer/generic.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,137 @@
//! Wrapper for handling render passes.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const objc = @import("objc");
const mtl = @import("api.zig");
const Renderer = @import("../generic.zig").Renderer(Metal);
const Metal = @import("../Metal.zig");
const Target = @import("Target.zig");
const Pipeline = @import("Pipeline.zig");
const RenderPass = @import("RenderPass.zig");
const Buffer = @import("buffer.zig").Buffer;
const Health = @import("../../renderer.zig").Health;
const log = std.log.scoped(.metal);
/// Options for beginning a frame.
pub const Options = struct {
/// MTLCommandQueue
queue: objc.Object,
};
/// MTLCommandBuffer
buffer: objc.Object,
block: CompletionBlock,
/// Begin encoding a frame.
pub fn begin(
opts: Options,
/// Once the frame has been completed, the `frameCompleted` method
/// on the renderer is called with the health status of the frame.
renderer: *Renderer,
/// The target is presented via the provided renderer's API when completed.
target: *Target,
) !Self {
const buffer = opts.queue.msgSend(
objc.Object,
objc.sel("commandBuffer"),
.{},
);
// Create our block to register for completion updates.
// The block is deallocated by the objC runtime on success.
const block = try CompletionBlock.init(
.{
.renderer = renderer,
.target = target,
.sync = false,
},
&bufferCompleted,
);
errdefer block.deinit();
return .{ .buffer = buffer, .block = block };
}
/// This is the block type used for the addCompletedHandler callback.
const CompletionBlock = objc.Block(struct {
renderer: *Renderer,
target: *Target,
sync: bool,
}, .{
objc.c.id, // MTLCommandBuffer
}, void);
fn bufferCompleted(
block: *const CompletionBlock.Context,
buffer_id: objc.c.id,
) callconv(.c) void {
const buffer = objc.Object.fromId(buffer_id);
// Get our command buffer status to pass back to the generic renderer.
const status = buffer.getProperty(mtl.MTLCommandBufferStatus, "status");
const health: Health = switch (status) {
.@"error" => .unhealthy,
else => .healthy,
};
// If the frame is healthy, present it.
if (health == .healthy) {
block.renderer.api.present(
block.target.*,
block.sync,
) catch |err| {
log.err("Failed to present render target: err={}", .{err});
};
}
block.renderer.frameCompleted(health);
}
/// Add a render pass to this frame with the provided attachments.
/// Returns a RenderPass which allows render steps to be added.
pub inline fn renderPass(
self: *const Self,
attachments: []const RenderPass.Options.Attachment,
) RenderPass {
return RenderPass.begin(.{
.attachments = attachments,
.command_buffer = self.buffer,
});
}
/// Complete this frame and present the target.
///
/// If `sync` is true, this will block until the frame is presented.
pub inline fn complete(self: *Self, sync: bool) void {
// If we don't need to complete synchronously,
// we add our block as a completion handler.
//
// It will be deallocated by the objc runtime on success.
if (!sync) {
self.buffer.msgSend(
void,
objc.sel("addCompletedHandler:"),
.{self.block.context},
);
}
self.buffer.msgSend(void, objc.sel("commit"), .{});
// If we need to complete synchronously, we wait until
// the buffer is completed and call the callback directly,
// deiniting the block after we're done.
if (sync) {
self.buffer.msgSend(void, "waitUntilCompleted", .{});
self.block.context.sync = true;
bufferCompleted(self.block.context, self.buffer.value);
self.block.deinit();
}
}

View File

@@ -0,0 +1,190 @@
//! A wrapper around a CALayer with a utility method
//! for settings its `contents` to an IOSurface.
const IOSurfaceLayer = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const objc = @import("objc");
const macos = @import("macos");
const IOSurface = macos.iosurface.IOSurface;
const log = std.log.scoped(.IOSurfaceLayer);
/// We subclass CALayer with a custom display handler, we only need
/// to make the subclass once, and then we can use it as a singleton.
var Subclass: ?objc.Class = null;
/// The underlying CALayer
layer: objc.Object,
pub fn init() !IOSurfaceLayer {
// The layer returned by `[CALayer layer]` is autoreleased, which means
// that at the end of the current autorelease pool it will be deallocated
// if it isn't retained, so we retain it here manually an extra time.
const layer = (try getSubclass()).msgSend(
objc.Object,
objc.sel("layer"),
.{},
).retain();
errdefer layer.release();
// The layer gravity is set to top-left so that the contents aren't
// stretched during resize operations before a new frame has been drawn.
layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft);
layer.setInstanceVariable("display_cb", .{ .value = null });
layer.setInstanceVariable("display_ctx", .{ .value = null });
return .{ .layer = layer };
}
pub fn release(self: *IOSurfaceLayer) void {
self.layer.release();
}
/// Sets the layer's `contents` to the provided IOSurface.
///
/// Makes sure to do so on the main thread to avoid visual artifacts.
pub inline fn setSurface(self: *IOSurfaceLayer, surface: *IOSurface) !void {
// We retain the surface to make sure it's not GC'd
// before we can set it as the contents of the layer.
//
// We release in the callback after setting the contents.
surface.retain();
// We also need to retain the layer itself to make sure it
// isn't destroyed before the callback completes, since if
// that happens it will try to interact with a deallocated
// object.
_ = self.layer.retain();
var block = try SetSurfaceBlock.init(.{
.layer = self.layer.value,
.surface = surface,
}, &setSurfaceCallback);
// We check if we're on the main thread and run the block directly if so.
const NSThread = objc.getClass("NSThread").?;
if (NSThread.msgSend(bool, "isMainThread", .{})) {
setSurfaceCallback(block.context);
block.deinit();
} else {
// NOTE: The block will automatically be deallocated by the objc
// runtime once it's executed, so there's no need to deinit it.
macos.dispatch.dispatch_async(
@ptrCast(macos.dispatch.queue.getMain()),
@ptrCast(block.context),
);
}
}
/// Sets the layer's `contents` to the provided IOSurface.
///
/// Does not ensure this happens on the main thread.
pub inline fn setSurfaceSync(self: *IOSurfaceLayer, surface: *IOSurface) void {
self.layer.setProperty("contents", surface);
}
const SetSurfaceBlock = objc.Block(struct {
layer: objc.c.id,
surface: *IOSurface,
}, .{}, void);
fn setSurfaceCallback(
block: *const SetSurfaceBlock.Context,
) callconv(.c) void {
const layer = objc.Object.fromId(block.layer);
const surface: *IOSurface = block.surface;
// See explanation of why we retain and release in `setSurface`.
defer {
surface.release();
layer.release();
}
// We check to see if the surface is the appropriate size for
// the layer, if it's not then we discard it. This is because
// asynchronously drawn frames can sometimes finish just after
// a synchronously drawn frame during a resize, and if we don't
// discard the improperly sized surface it creates jank.
const bounds = layer.getProperty(macos.graphics.Rect, "bounds");
const scale = layer.getProperty(f64, "contentsScale");
const width: usize = @intFromFloat(bounds.size.width * scale);
const height: usize = @intFromFloat(bounds.size.height * scale);
if (width != surface.getWidth() or height != surface.getHeight()) {
log.debug(
"setSurfaceCallback(): surface is wrong size for layer, discarding. surface = {d}x{d}, layer = {d}x{d}",
.{ surface.getWidth(), surface.getHeight(), width, height },
);
return;
}
layer.setProperty("contents", surface);
}
pub const DisplayCallback = ?*align(8) const fn (?*anyopaque) void;
pub fn setDisplayCallback(
self: *IOSurfaceLayer,
display_cb: DisplayCallback,
display_ctx: ?*anyopaque,
) void {
self.layer.setInstanceVariable(
"display_cb",
objc.Object.fromId(@constCast(display_cb)),
);
self.layer.setInstanceVariable(
"display_ctx",
objc.Object.fromId(display_ctx),
);
}
fn getSubclass() error{ObjCFailed}!objc.Class {
if (Subclass) |c| return c;
const CALayer =
objc.getClass("CALayer") orelse return error.ObjCFailed;
var subclass =
objc.allocateClassPair(CALayer, "IOSurfaceLayer") orelse return error.ObjCFailed;
errdefer objc.disposeClassPair(subclass);
if (!subclass.addIvar("display_cb")) return error.ObjCFailed;
if (!subclass.addIvar("display_ctx")) return error.ObjCFailed;
subclass.replaceMethod("display", struct {
fn display(target: objc.c.id, sel: objc.c.SEL) callconv(.c) void {
_ = sel;
const self = objc.Object.fromId(target);
const display_cb: DisplayCallback = @ptrFromInt(@intFromPtr(
self.getInstanceVariable("display_cb").value,
));
if (display_cb) |cb| cb(
@ptrCast(self.getInstanceVariable("display_ctx").value),
);
}
}.display);
// Disable all animations for this layer by returning null for all actions.
subclass.replaceMethod("actionForKey:", struct {
fn actionForKey(
target: objc.c.id,
sel: objc.c.SEL,
key: objc.c.id,
) callconv(.c) objc.c.id {
_ = target;
_ = sel;
_ = key;
return objc.getClass("NSNull").?.msgSend(objc.c.id, "null", .{});
}
}.actionForKey);
objc.registerClassPair(subclass);
Subclass = subclass;
return subclass;
}

View File

@@ -0,0 +1,203 @@
//! Wrapper for handling render pipelines.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const macos = @import("macos");
const objc = @import("objc");
const mtl = @import("api.zig");
const Texture = @import("Texture.zig");
const Metal = @import("../Metal.zig");
const log = std.log.scoped(.metal);
/// Options for initializing a render pipeline.
pub const Options = struct {
/// MTLDevice
device: objc.Object,
/// Name of the vertex function
vertex_fn: []const u8,
/// Name of the fragment function
fragment_fn: []const u8,
/// MTLLibrary to get the vertex function from
vertex_library: objc.Object,
/// MTLLibrary to get the fragment function from
fragment_library: objc.Object,
/// Vertex step function
step_fn: mtl.MTLVertexStepFunction = .per_vertex,
/// Info about the color attachments used by this render pipeline.
attachments: []const Attachment,
/// Describes a color attachment.
pub const Attachment = struct {
pixel_format: mtl.MTLPixelFormat,
blending_enabled: bool = true,
};
};
/// MTLRenderPipelineState
state: objc.Object,
pub fn init(comptime VertexAttributes: ?type, opts: Options) !Self {
// 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;
};
defer desc.msgSend(void, objc.sel("release"), .{});
// Get our vertex and fragment functions and add them to the descriptor.
{
const str = try macos.foundation.String.createWithBytes(
opts.vertex_fn,
.utf8,
false,
);
defer str.release();
const ptr = opts.vertex_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
const func_vert = objc.Object.fromId(ptr.?);
defer func_vert.msgSend(void, objc.sel("release"), .{});
desc.setProperty("vertexFunction", func_vert);
}
{
const str = try macos.foundation.String.createWithBytes(
opts.fragment_fn,
.utf8,
false,
);
defer str.release();
const ptr = opts.fragment_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
const func_frag = objc.Object.fromId(ptr.?);
defer func_frag.msgSend(void, objc.sel("release"), .{});
desc.setProperty("fragmentFunction", func_frag);
}
// If we have vertex attributes, create and add a vertex descriptor.
if (VertexAttributes) |V| {
const vertex_desc = init: {
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;
};
defer vertex_desc.msgSend(void, objc.sel("release"), .{});
// Our attributes are the fields of the input
const attrs = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "attributes"));
autoAttribute(V, attrs);
// The layout describes how and when we fetch the next vertex input.
const layouts = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "layouts"));
{
const layout = layouts.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 0)},
);
layout.setProperty("stepFunction", @intFromEnum(opts.step_fn));
layout.setProperty("stride", @as(c_ulong, @sizeOf(V)));
}
desc.setProperty("vertexDescriptor", vertex_desc);
}
// Set our color attachment
const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
for (opts.attachments, 0..) |at, i| {
const attachment = attachments.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, i)},
);
attachment.setProperty("pixelFormat", @intFromEnum(at.pixel_format));
attachment.setProperty("blendingEnabled", at.blending_enabled);
// We always use premultiplied alpha blending for now.
if (at.blending_enabled) {
attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
}
}
// Make our state
var err: ?*anyopaque = null;
const pipeline_state = opts.device.msgSend(
objc.Object,
objc.sel("newRenderPipelineStateWithDescriptor:error:"),
.{ desc, &err },
);
try checkError(err);
errdefer pipeline_state.release();
return .{ .state = pipeline_state };
}
pub fn deinit(self: *const Self) void {
self.state.release();
}
fn autoAttribute(T: type, attrs: objc.Object) void {
inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
const offset = @offsetOf(T, field.name);
const FT = switch (@typeInfo(field.type)) {
.@"enum" => |e| e.tag_type,
else => field.type,
};
// Very incomplete list, expand as necessary.
const format = switch (FT) {
[4]u8 => mtl.MTLVertexFormat.uchar4,
[2]u16 => mtl.MTLVertexFormat.ushort2,
[2]i16 => mtl.MTLVertexFormat.short2,
[2]f32 => mtl.MTLVertexFormat.float2,
[4]f32 => mtl.MTLVertexFormat.float4,
[2]i32 => mtl.MTLVertexFormat.int2,
u32 => mtl.MTLVertexFormat.uint,
[2]u32 => mtl.MTLVertexFormat.uint2,
[4]u32 => mtl.MTLVertexFormat.uint4,
u8 => mtl.MTLVertexFormat.uchar,
else => comptime unreachable,
};
const attr = attrs.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, i)},
);
attr.setProperty("format", @intFromEnum(format));
attr.setProperty("offset", @as(c_ulong, offset));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
}
fn checkError(err_: ?*anyopaque) !void {
const nserr = objc.Object.fromId(err_ orelse return);
const str = @as(
*macos.foundation.String,
@ptrCast(nserr.getProperty(?*anyopaque, "localizedDescription").?),
);
log.err("metal error={s}", .{str.cstringPtr(.ascii).?});
return error.MetalFailed;
}

View File

@@ -0,0 +1,220 @@
//! Wrapper for handling render passes.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const objc = @import("objc");
const mtl = @import("api.zig");
const Pipeline = @import("Pipeline.zig");
const Texture = @import("Texture.zig");
const Target = @import("Target.zig");
const Metal = @import("../Metal.zig");
const Buffer = @import("buffer.zig").Buffer;
const log = std.log.scoped(.metal);
/// Options for beginning a render pass.
pub const Options = struct {
/// MTLCommandBuffer
command_buffer: objc.Object,
/// Color attachments for this render pass.
attachments: []const Attachment,
/// Describes a color attachment.
pub const Attachment = struct {
target: union(enum) {
texture: Texture,
target: Target,
},
clear_color: ?[4]f64 = null,
};
};
/// Describes a step in a render pass.
pub const Step = struct {
pipeline: Pipeline,
/// MTLBuffer
uniforms: ?objc.Object = null,
/// MTLBuffer
buffers: []const ?objc.Object = &.{},
textures: []const ?Texture = &.{},
draw: Draw,
/// Describes the draw call for this step.
pub const Draw = struct {
type: mtl.MTLPrimitiveType,
vertex_count: usize,
instance_count: usize = 1,
};
};
/// MTLRenderCommandEncoder
encoder: objc.Object,
/// Begin a render pass.
pub fn begin(
opts: Options,
) Self {
// Create a pass descriptor
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"),
);
for (opts.attachments, 0..) |at, i| {
const attachment = attachments.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, i)},
);
attachment.setProperty(
"loadAction",
@intFromEnum(@as(
mtl.MTLLoadAction,
if (at.clear_color != null)
.clear
else
.load,
)),
);
attachment.setProperty(
"storeAction",
@intFromEnum(mtl.MTLStoreAction.store),
);
attachment.setProperty("texture", switch (at.target) {
.texture => |t| t.texture.value,
.target => |t| t.texture.value,
});
if (at.clear_color) |c| attachment.setProperty(
"clearColor",
mtl.MTLClearColor{
.red = c[0],
.green = c[1],
.blue = c[2],
.alpha = c[3],
},
);
}
break :desc desc;
};
// MTLRenderCommandEncoder
const encoder = opts.command_buffer.msgSend(
objc.Object,
objc.sel("renderCommandEncoderWithDescriptor:"),
.{desc.value},
);
return .{ .encoder = encoder };
}
/// Add a step to this render pass.
pub fn step(self: *const Self, s: Step) void {
if (s.draw.instance_count == 0) return;
// Set pipeline state
self.encoder.msgSend(
void,
objc.sel("setRenderPipelineState:"),
.{s.pipeline.state.value},
);
if (s.buffers.len > 0) {
// We reserve index 0 for the vertex buffer, this isn't very
// flexible but it lines up with the API we have for OpenGL.
if (s.buffers[0]) |buf| {
self.encoder.msgSend(
void,
objc.sel("setVertexBuffer:offset:atIndex:"),
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) },
);
self.encoder.msgSend(
void,
objc.sel("setFragmentBuffer:offset:atIndex:"),
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) },
);
}
// Set the rest of the buffers starting at index 2, this is
// so that we can use index 1 for the uniforms if present.
//
// Also, we set buffers (and textures) for both stages.
//
// Again, not very flexible, but it's consistent and predictable,
// and we need to treat the uniforms as special because of OpenGL.
//
// TODO: Maybe in the future add info to the pipeline struct which
// allows it to define a mapping between provided buffers and
// what index they get set at for the vertex / fragment stage.
for (s.buffers[1..], 2..) |b, i| if (b) |buf| {
self.encoder.msgSend(
void,
objc.sel("setVertexBuffer:offset:atIndex:"),
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, i) },
);
self.encoder.msgSend(
void,
objc.sel("setFragmentBuffer:offset:atIndex:"),
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, i) },
);
};
}
// Set the uniforms as buffer index 1 if present.
if (s.uniforms) |buf| {
self.encoder.msgSend(
void,
objc.sel("setVertexBuffer:offset:atIndex:"),
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, 1) },
);
self.encoder.msgSend(
void,
objc.sel("setFragmentBuffer:offset:atIndex:"),
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, 1) },
);
}
// Set textures.
for (s.textures, 0..) |t, i| if (t) |tex| {
self.encoder.msgSend(
void,
objc.sel("setVertexTexture:atIndex:"),
.{ tex.texture.value, @as(c_ulong, i) },
);
self.encoder.msgSend(
void,
objc.sel("setFragmentTexture:atIndex:"),
.{ tex.texture.value, @as(c_ulong, i) },
);
};
// Draw!
self.encoder.msgSend(
void,
objc.sel("drawPrimitives:vertexStart:vertexCount:instanceCount:"),
.{
@intFromEnum(s.draw.type),
@as(c_ulong, 0),
@as(c_ulong, s.draw.vertex_count),
@as(c_ulong, s.draw.instance_count),
},
);
}
/// Complete this render pass.
/// This struct can no longer be used after calling this.
pub fn complete(self: *const Self) void {
self.encoder.msgSend(void, objc.sel("endEncoding"), .{});
}

View File

@@ -0,0 +1,110 @@
//! Represents a render target.
//!
//! In this case, an IOSurface-backed MTLTexture.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const objc = @import("objc");
const macos = @import("macos");
const graphics = macos.graphics;
const IOSurface = macos.iosurface.IOSurface;
const mtl = @import("api.zig");
const log = std.log.scoped(.metal);
/// Options for initializing a Target
pub const Options = struct {
/// MTLDevice
device: objc.Object,
/// Desired width
width: usize,
/// Desired height
height: usize,
/// Pixel format for the MTLTexture
pixel_format: mtl.MTLPixelFormat,
/// Storage mode for the MTLTexture
storage_mode: mtl.MTLResourceOptions.StorageMode,
};
/// The underlying IOSurface.
surface: *IOSurface,
/// The underlying MTLTexture.
texture: objc.Object,
/// Current width of this target.
width: usize,
/// Current height of this target.
height: usize,
pub fn init(opts: Options) !Self {
// We set our surface's color space to Display P3.
// This allows us to have "Apple-style" alpha blending,
// since it seems to be the case that Apple apps like
// Terminal and TextEdit render text in the display's
// color space using converted colors, which reduces,
// but does not fully eliminate blending artifacts.
const colorspace = try graphics.ColorSpace.createNamed(.displayP3);
defer colorspace.release();
const surface = try IOSurface.init(.{
.width = @intCast(opts.width),
.height = @intCast(opts.height),
.pixel_format = .@"32BGRA",
.bytes_per_element = 4,
.colorspace = colorspace,
});
// Create our descriptor
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;
};
errdefer desc.msgSend(void, objc.sel("release"), .{});
// Set our properties
desc.setProperty("width", @as(c_ulong, @intCast(opts.width)));
desc.setProperty("height", @as(c_ulong, @intCast(opts.height)));
desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format));
desc.setProperty("usage", mtl.MTLTextureUsage{ .render_target = true });
desc.setProperty(
"resourceOptions",
mtl.MTLResourceOptions{
// Indicate that the CPU writes to this resource but never reads it.
.cpu_cache_mode = .write_combined,
.storage_mode = opts.storage_mode,
},
);
const id = opts.device.msgSend(
?*anyopaque,
objc.sel("newTextureWithDescriptor:iosurface:plane:"),
.{
desc,
surface,
@as(c_ulong, 0),
},
) orelse return error.MetalFailed;
const texture = objc.Object.fromId(id);
return .{
.surface = surface,
.texture = texture,
.width = opts.width,
.height = opts.height,
};
}
pub fn deinit(self: *Self) void {
self.surface.deinit();
self.texture.release();
}

View File

@@ -0,0 +1,201 @@
//! Wrapper for handling textures.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const objc = @import("objc");
const mtl = @import("api.zig");
const Metal = @import("../Metal.zig");
const log = std.log.scoped(.metal);
/// Options for initializing a texture.
pub const Options = struct {
/// MTLDevice
device: objc.Object,
pixel_format: mtl.MTLPixelFormat,
resource_options: mtl.MTLResourceOptions,
};
/// The underlying MTLTexture Object.
texture: objc.Object,
/// The width of this texture.
width: usize,
/// The height of this texture.
height: usize,
/// Bytes per pixel for this texture.
bpp: usize,
pub const Error = error{
/// A Metal API call failed.
MetalFailed,
};
/// Initialize a texture
pub fn init(
opts: Options,
width: usize,
height: usize,
data: ?[]const u8,
) Error!Self {
// Create our descriptor
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;
};
errdefer desc.msgSend(void, objc.sel("release"), .{});
// Set our properties
desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format));
desc.setProperty("width", @as(c_ulong, width));
desc.setProperty("height", @as(c_ulong, height));
desc.setProperty("resourceOptions", opts.resource_options);
// Initialize
const id = opts.device.msgSend(
?*anyopaque,
objc.sel("newTextureWithDescriptor:"),
.{desc},
) orelse return error.MetalFailed;
const self: Self = .{
.texture = objc.Object.fromId(id),
.width = width,
.height = height,
.bpp = bppOf(opts.pixel_format),
};
// If we have data, we set it here.
if (data) |d| {
assert(d.len == width * height * self.bpp);
try self.replaceRegion(0, 0, width, height, d);
}
return self;
}
pub fn deinit(self: Self) void {
self.texture.release();
}
/// Replace a region of the texture with the provided data.
///
/// Does NOT check the dimensions of the data to ensure correctness.
pub fn replaceRegion(
self: Self,
x: usize,
y: usize,
width: usize,
height: usize,
data: []const u8,
) error{}!void {
self.texture.msgSend(
void,
objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"),
.{
mtl.MTLRegion{
.origin = .{ .x = x, .y = y, .z = 0 },
.size = .{
.width = @intCast(width),
.height = @intCast(height),
.depth = 1,
},
},
@as(c_ulong, 0),
@as(*const anyopaque, data.ptr),
@as(c_ulong, self.bpp * width),
},
);
}
/// Returns the bytes per pixel for the provided pixel format
fn bppOf(pixel_format: mtl.MTLPixelFormat) usize {
return switch (pixel_format) {
// Invalid
.invalid => @panic("invalid pixel format"),
// Weird formats I was too lazy to get the sizes of
else => @panic("pixel format size unknown (unlikely that this format was actually used, could be memory corruption)"),
// 8-bit pixel formats
.a8unorm,
.r8unorm,
.r8unorm_srgb,
.r8snorm,
.r8uint,
.r8sint,
.rg8unorm,
.rg8unorm_srgb,
.rg8snorm,
.rg8uint,
.rg8sint,
.stencil8,
=> 1,
// 16-bit pixel formats
.r16unorm,
.r16snorm,
.r16uint,
.r16sint,
.r16float,
.rg16unorm,
.rg16snorm,
.rg16uint,
.rg16sint,
.rg16float,
.b5g6r5unorm,
.a1bgr5unorm,
.abgr4unorm,
.bgr5a1unorm,
.depth16unorm,
=> 2,
// 32-bit pixel formats
.rgba8unorm,
.rgba8unorm_srgb,
.rgba8snorm,
.rgba8uint,
.rgba8sint,
.bgra8unorm,
.bgra8unorm_srgb,
.rgb10a2unorm,
.rgb10a2uint,
.rg11b10float,
.rgb9e5float,
.bgr10a2unorm,
.bgr10_xr,
.bgr10_xr_srgb,
.r32uint,
.r32sint,
.r32float,
.depth32float,
.depth24unorm_stencil8,
=> 4,
// 64-bit pixel formats
.rg32uint,
.rg32sint,
.rg32float,
.rgba16unorm,
.rgba16snorm,
.rgba16uint,
.rgba16sint,
.rgba16float,
.bgra10_xr,
.bgra10_xr_srgb,
=> 8,
// 128-bit pixel formats,
.rgba32uint,
.rgba32sint,
.rgba32float,
=> 128,
};
}

View File

@@ -1,4 +1,10 @@
//! This file contains the definitions of the Metal API that we use.
//!
//! Because the online Apple developer docs have recently (as of January 2025)
//! been changed to hide enum values, `Metal-cpp` has been used as a reference
//! source instead.
//!
//! Ref: https://developer.apple.com/metal/cpp/
/// https://developer.apple.com/documentation/metal/mtlcommandbufferstatus?language=objc
pub const MTLCommandBufferStatus = enum(c_ulong) {
@@ -22,6 +28,10 @@ pub const MTLLoadAction = enum(c_ulong) {
pub const MTLStoreAction = enum(c_ulong) {
dont_care = 0,
store = 1,
multisample_resolve = 2,
store_and_multisample_resolve = 3,
unknown = 4,
custom_sample_depth_store = 5,
};
/// https://developer.apple.com/documentation/metal/mtlresourceoptions?language=objc
@@ -73,16 +83,60 @@ pub const MTLIndexType = enum(c_ulong) {
/// https://developer.apple.com/documentation/metal/mtlvertexformat?language=objc
pub const MTLVertexFormat = enum(c_ulong) {
invalid = 0,
uchar2 = 1,
uchar3 = 2,
uchar4 = 3,
char2 = 4,
char3 = 5,
char4 = 6,
uchar2normalized = 7,
uchar3normalized = 8,
uchar4normalized = 9,
char2normalized = 10,
char3normalized = 11,
char4normalized = 12,
ushort2 = 13,
ushort3 = 14,
ushort4 = 15,
short2 = 16,
short3 = 17,
short4 = 18,
ushort2normalized = 19,
ushort3normalized = 20,
ushort4normalized = 21,
short2normalized = 22,
short3normalized = 23,
short4normalized = 24,
half2 = 25,
half3 = 26,
half4 = 27,
float = 28,
float2 = 29,
float3 = 30,
float4 = 31,
int = 32,
int2 = 33,
int3 = 34,
int4 = 35,
uint = 36,
uint2 = 37,
uint3 = 38,
uint4 = 39,
int1010102normalized = 40,
uint1010102normalized = 41,
uchar4normalized_bgra = 42,
uchar = 45,
char = 46,
ucharnormalized = 47,
charnormalized = 48,
ushort = 49,
short = 50,
ushortnormalized = 51,
shortnormalized = 52,
half = 53,
floatrg11b10 = 54,
floatrgb9e5 = 55,
};
/// https://developer.apple.com/documentation/metal/mtlvertexstepfunction?language=objc
@@ -90,20 +144,158 @@ pub const MTLVertexStepFunction = enum(c_ulong) {
constant = 0,
per_vertex = 1,
per_instance = 2,
per_patch = 3,
per_patch_control_point = 4,
};
/// https://developer.apple.com/documentation/metal/mtlpixelformat?language=objc
pub const MTLPixelFormat = enum(c_ulong) {
invalid = 0,
a8unorm = 1,
r8unorm = 10,
r8unorm_srgb = 11,
r8snorm = 12,
r8uint = 13,
r8sint = 14,
r16unorm = 20,
r16snorm = 22,
r16uint = 23,
r16sint = 24,
r16float = 25,
rg8unorm = 30,
rg8unorm_srgb = 31,
rg8snorm = 32,
rg8uint = 33,
rg8sint = 34,
b5g6r5unorm = 40,
a1bgr5unorm = 41,
abgr4unorm = 42,
bgr5a1unorm = 43,
r32uint = 53,
r32sint = 54,
r32float = 55,
rg16unorm = 60,
rg16snorm = 62,
rg16uint = 63,
rg16sint = 64,
rg16float = 65,
rgba8unorm = 70,
rgba8unorm_srgb = 71,
rgba8snorm = 72,
rgba8uint = 73,
rgba8sint = 74,
bgra8unorm = 80,
bgra8unorm_srgb = 81,
rgb10a2unorm = 90,
rgb10a2uint = 91,
rg11b10float = 92,
rgb9e5float = 93,
bgr10a2unorm = 94,
bgr10_xr = 554,
bgr10_xr_srgb = 555,
rg32uint = 103,
rg32sint = 104,
rg32float = 105,
rgba16unorm = 110,
rgba16snorm = 112,
rgba16uint = 113,
rgba16sint = 114,
rgba16float = 115,
bgra10_xr = 552,
bgra10_xr_srgb = 553,
rgba32uint = 123,
rgba32sint = 124,
rgba32float = 125,
bc1_rgba = 130,
bc1_rgba_srgb = 131,
bc2_rgba = 132,
bc2_rgba_srgb = 133,
bc3_rgba = 134,
bc3_rgba_srgb = 135,
bc4_runorm = 140,
bc4_rsnorm = 141,
bc5_rgunorm = 142,
bc5_rgsnorm = 143,
bc6h_rgbfloat = 150,
bc6h_rgbufloat = 151,
bc7_rgbaunorm = 152,
bc7_rgbaunorm_srgb = 153,
pvrtc_rgb_2bpp = 160,
pvrtc_rgb_2bpp_srgb = 161,
pvrtc_rgb_4bpp = 162,
pvrtc_rgb_4bpp_srgb = 163,
pvrtc_rgba_2bpp = 164,
pvrtc_rgba_2bpp_srgb = 165,
pvrtc_rgba_4bpp = 166,
pvrtc_rgba_4bpp_srgb = 167,
eac_r11unorm = 170,
eac_r11snorm = 172,
eac_rg11unorm = 174,
eac_rg11snorm = 176,
eac_rgba8 = 178,
eac_rgba8_srgb = 179,
etc2_rgb8 = 180,
etc2_rgb8_srgb = 181,
etc2_rgb8a1 = 182,
etc2_rgb8a1_srgb = 183,
astc_4x4_srgb = 186,
astc_5x4_srgb = 187,
astc_5x5_srgb = 188,
astc_6x5_srgb = 189,
astc_6x6_srgb = 190,
astc_8x5_srgb = 192,
astc_8x6_srgb = 193,
astc_8x8_srgb = 194,
astc_10x5_srgb = 195,
astc_10x6_srgb = 196,
astc_10x8_srgb = 197,
astc_10x10_srgb = 198,
astc_12x10_srgb = 199,
astc_12x12_srgb = 200,
astc_4x4_ldr = 204,
astc_5x4_ldr = 205,
astc_5x5_ldr = 206,
astc_6x5_ldr = 207,
astc_6x6_ldr = 208,
astc_8x5_ldr = 210,
astc_8x6_ldr = 211,
astc_8x8_ldr = 212,
astc_10x5_ldr = 213,
astc_10x6_ldr = 214,
astc_10x8_ldr = 215,
astc_10x10_ldr = 216,
astc_12x10_ldr = 217,
astc_12x12_ldr = 218,
astc_4x4_hdr = 222,
astc_5x4_hdr = 223,
astc_5x5_hdr = 224,
astc_6x5_hdr = 225,
astc_6x6_hdr = 226,
astc_8x5_hdr = 228,
astc_8x6_hdr = 229,
astc_8x8_hdr = 230,
astc_10x5_hdr = 231,
astc_10x6_hdr = 232,
astc_10x8_hdr = 233,
astc_10x10_hdr = 234,
astc_12x10_hdr = 235,
astc_12x12_hdr = 236,
gbgr422 = 240,
bgrg422 = 241,
depth16unorm = 250,
depth32float = 252,
stencil8 = 253,
depth24unorm_stencil8 = 255,
depth32float_stencil8 = 260,
x32_stencil8 = 261,
x24_stencil8 = 262,
};
/// https://developer.apple.com/documentation/metal/mtlpurgeablestate?language=objc
pub const MTLPurgeableState = enum(c_ulong) {
keep_current = 1,
non_volatile = 2,
@"volatile" = 3,
empty = 4,
};
@@ -155,13 +347,48 @@ pub const MTLBlendOperation = enum(c_ulong) {
max = 4,
};
/// https://developer.apple.com/documentation/metal/mtltextureusage?language=objc<D-j>
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/mtltextureusage?language=objc
pub const MTLTextureUsage = packed struct(c_ulong) {
/// https://developer.apple.com/documentation/metal/mtltextureusage/shaderread?language=objc
shader_read: bool = false, // TextureUsageShaderRead = 1,
/// https://developer.apple.com/documentation/metal/mtltextureusage/shaderwrite?language=objc
shader_write: bool = false, // TextureUsageShaderWrite = 2,
/// https://developer.apple.com/documentation/metal/mtltextureusage/rendertarget?language=objc
render_target: bool = false, // TextureUsageRenderTarget = 4,
_reserved: u1 = 0, // The enum skips from 4 to 16, 8 has no documented use.
/// https://developer.apple.com/documentation/metal/mtltextureusage/pixelformatview?language=objc
pixel_format_view: bool = false, // TextureUsagePixelFormatView = 16,
/// https://developer.apple.com/documentation/metal/mtltextureusage/shaderatomic?language=objc
shader_atomic: bool = false, // TextureUsageShaderAtomic = 32,
__reserved: @Type(.{ .int = .{
.signedness = .unsigned,
.bits = @bitSizeOf(c_ulong) - 6,
} }) = 0,
/// https://developer.apple.com/documentation/metal/mtltextureusage/unknown?language=objc
const unknown: MTLTextureUsage = @bitCast(0); // TextureUsageUnknown = 0,
};
/// https://developer.apple.com/documentation/metal/mtlbarrierscope?language=objc
pub const MTLBarrierScope = enum(c_ulong) {
buffers = 1,
textures = 2,
render_targets = 4,
};
/// https://developer.apple.com/documentation/metal/mtlrenderstages?language=objc
pub const MTLRenderStage = enum(c_ulong) {
vertex = 1,
fragment = 2,
tile = 4,
object = 8,
mesh = 16,
};
pub const MTLClearColor = extern struct {

View File

@@ -5,9 +5,17 @@ const objc = @import("objc");
const macos = @import("macos");
const mtl = @import("api.zig");
const Metal = @import("../Metal.zig");
const log = std.log.scoped(.metal);
/// Options for initializing a buffer.
pub const Options = struct {
/// MTLDevice
device: objc.Object,
resource_options: mtl.MTLResourceOptions,
};
/// Metal data storage for a certain set of equal types. This is usually
/// used for vertex buffers, etc. This helpful wrapper makes it easy to
/// prealloc, shrink, grow, sync, buffers with Metal.
@@ -15,74 +23,57 @@ pub fn Buffer(comptime T: type) type {
return struct {
const Self = @This();
/// The resource options for this buffer.
options: mtl.MTLResourceOptions,
/// The options this buffer was initialized with.
opts: Options,
buffer: objc.Object, // MTLBuffer
/// The underlying MTLBuffer object.
buffer: objc.Object,
/// The allocated length of the buffer.
/// Note that this is the number
/// of `T`s not the size in bytes.
len: usize,
/// Initialize a buffer with the given length pre-allocated.
pub fn init(
device: objc.Object,
len: usize,
options: mtl.MTLResourceOptions,
) !Self {
const buffer = device.msgSend(
pub fn init(opts: Options, len: usize) !Self {
const buffer = opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(len * @sizeOf(T))),
options,
opts.resource_options,
},
);
return .{ .buffer = buffer, .options = options };
return .{ .buffer = buffer, .opts = opts, .len = len };
}
/// Init the buffer filled with the given data.
pub fn initFill(
device: objc.Object,
data: []const T,
options: mtl.MTLResourceOptions,
) !Self {
const buffer = device.msgSend(
pub fn initFill(opts: Options, data: []const T) !Self {
const buffer = opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithBytes:length:options:"),
.{
@as(*const anyopaque, @ptrCast(data.ptr)),
@as(c_ulong, @intCast(data.len * @sizeOf(T))),
options,
opts.resource_options,
},
);
return .{ .buffer = buffer, .options = options };
return .{ .buffer = buffer, .opts = opts, .len = data.len };
}
pub fn deinit(self: *Self) void {
pub fn deinit(self: *const Self) void {
self.buffer.msgSend(void, objc.sel("release"), .{});
}
/// Get the buffer contents as a slice of T. The contents are
/// mutable. The contents may or may not be automatically synced
/// depending on the buffer storage mode. See the Metal docs.
pub fn contents(self: *Self) ![]T {
const len_bytes = self.buffer.getProperty(c_ulong, "length");
assert(@mod(len_bytes, @sizeOf(T)) == 0);
const len = @divExact(len_bytes, @sizeOf(T));
const ptr = self.buffer.msgSend(
?[*]T,
objc.sel("contents"),
.{},
).?;
return ptr[0..len];
}
/// Sync new contents to the buffer. The data is expected to be the
/// complete contents of the buffer. If the amount of data is larger
/// than the buffer length, the buffer will be reallocated.
///
/// If the amount of data is smaller than the buffer length, the
/// remaining data in the buffer is left untouched.
pub fn sync(self: *Self, device: objc.Object, data: []const T) !void {
pub fn sync(self: *Self, data: []const T) !void {
// If we need more bytes than our buffer has, we need to reallocate.
const req_bytes = data.len * @sizeOf(T);
const avail_bytes = self.buffer.getProperty(c_ulong, "length");
@@ -92,12 +83,12 @@ pub fn Buffer(comptime T: type) type {
// Allocate a new buffer with enough to hold double what we require.
const size = req_bytes * 2;
self.buffer = device.msgSend(
self.buffer = self.opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(size * @sizeOf(T))),
self.options,
self.opts.resource_options,
},
);
}
@@ -123,7 +114,7 @@ pub fn Buffer(comptime T: type) type {
// we need to signal Metal to synchronize the buffer data.
//
// Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc
if (self.options.storage_mode == .managed) {
if (self.opts.resource_options.storage_mode == .managed) {
self.buffer.msgSend(
void,
"didModifyRange:",
@@ -134,7 +125,7 @@ pub fn Buffer(comptime T: type) type {
/// Like Buffer.sync but takes data from an array of ArrayLists,
/// rather than a single array. Returns the number of items synced.
pub fn syncFromArrayLists(self: *Self, device: objc.Object, lists: []std.ArrayListUnmanaged(T)) !usize {
pub fn syncFromArrayLists(self: *Self, lists: []const std.ArrayListUnmanaged(T)) !usize {
var total_len: usize = 0;
for (lists) |list| {
total_len += list.items.len;
@@ -149,12 +140,12 @@ pub fn Buffer(comptime T: type) type {
// Allocate a new buffer with enough to hold double what we require.
const size = req_bytes * 2;
self.buffer = device.msgSend(
self.buffer = self.opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(size * @sizeOf(T))),
self.options,
self.opts.resource_options,
},
);
}
@@ -181,7 +172,7 @@ pub fn Buffer(comptime T: type) type {
// we need to signal Metal to synchronize the buffer data.
//
// Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc
if (self.options.storage_mode == .managed) {
if (self.opts.resource_options.storage_mode == .managed) {
self.buffer.msgSend(
void,
"didModifyRange:",

View File

@@ -4,6 +4,9 @@ const assert = std.debug.assert;
const objc = @import("objc");
const wuffs = @import("wuffs");
const Metal = @import("../Metal.zig");
const Texture = Metal.Texture;
const mtl = @import("api.zig");
/// Represents a single image placement on the grid. A placement is a
@@ -61,15 +64,15 @@ pub const Image = union(enum) {
replace_rgba: Replace,
/// The image is uploaded and ready to be used.
ready: objc.Object, // MTLTexture
ready: Texture,
/// The image is uploaded but is scheduled to be unloaded.
unload_pending: []u8,
unload_ready: objc.Object, // MTLTexture
unload_replace: struct { []u8, objc.Object },
unload_ready: Texture,
unload_replace: struct { []u8, Texture },
pub const Replace = struct {
texture: objc.Object,
texture: Texture,
pending: Pending,
};
@@ -101,32 +104,32 @@ pub const Image = union(enum) {
.replace_gray => |r| {
alloc.free(r.pending.dataSlice(1));
r.texture.msgSend(void, objc.sel("release"), .{});
r.texture.deinit();
},
.replace_gray_alpha => |r| {
alloc.free(r.pending.dataSlice(2));
r.texture.msgSend(void, objc.sel("release"), .{});
r.texture.deinit();
},
.replace_rgb => |r| {
alloc.free(r.pending.dataSlice(3));
r.texture.msgSend(void, objc.sel("release"), .{});
r.texture.deinit();
},
.replace_rgba => |r| {
alloc.free(r.pending.dataSlice(4));
r.texture.msgSend(void, objc.sel("release"), .{});
r.texture.deinit();
},
.unload_replace => |r| {
alloc.free(r[0]);
r[1].msgSend(void, objc.sel("release"), .{});
r[1].deinit();
},
.ready,
.unload_ready,
=> |obj| obj.msgSend(void, objc.sel("release"), .{}),
=> |t| t.deinit(),
}
}
@@ -170,7 +173,7 @@ pub const Image = union(enum) {
// Get our existing texture. This switch statement will also handle
// scenarios where there is no existing texture and we can modify
// the self pointer directly.
const existing: objc.Object = switch (self.*) {
const existing: Texture = switch (self.*) {
// For pending, we can free the old data and become pending
// ourselves.
.pending_gray => |p| {
@@ -357,10 +360,11 @@ pub const Image = union(enum) {
pub fn upload(
self: *Image,
alloc: Allocator,
device: objc.Object,
/// Storage mode for the MTLTexture object
storage_mode: mtl.MTLResourceOptions.StorageMode,
metal: *const Metal,
) !void {
const device = metal.device;
const storage_mode = metal.default_storage_mode;
// Convert our data if we have to
try self.convert(alloc);
@@ -368,27 +372,19 @@ pub const Image = union(enum) {
const p = self.pending().?;
// Create our texture
const texture = try initTexture(p, device, storage_mode);
errdefer texture.msgSend(void, objc.sel("release"), .{});
// Upload our data
const d = self.depth();
texture.msgSend(
void,
objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"),
const texture = try Texture.init(
.{
mtl.MTLRegion{
.origin = .{ .x = 0, .y = 0, .z = 0 },
.size = .{
.width = @intCast(p.width),
.height = @intCast(p.height),
.depth = 1,
},
.device = device,
.pixel_format = .rgba8unorm_srgb,
.resource_options = .{
// Indicate that the CPU writes to this resource but never reads it.
.cpu_cache_mode = .write_combined,
.storage_mode = storage_mode,
},
@as(c_ulong, 0),
@as(*const anyopaque, p.data),
@as(c_ulong, d * p.width),
},
@intCast(p.width),
@intCast(p.height),
p.data[0 .. p.width * p.height * self.depth()],
);
// Uploaded. We can now clear our data and change our state.
@@ -425,42 +421,4 @@ pub const Image = union(enum) {
else => null,
};
}
fn initTexture(
p: Pending,
device: objc.Object,
/// Storage mode for the MTLTexture object
storage_mode: mtl.MTLResourceOptions.StorageMode,
) !objc.Object {
// Create our descriptor
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.rgba8unorm_srgb));
desc.setProperty("width", @as(c_ulong, @intCast(p.width)));
desc.setProperty("height", @as(c_ulong, @intCast(p.height)));
desc.setProperty(
"resourceOptions",
mtl.MTLResourceOptions{
// Indicate that the CPU writes to this resource but never reads it.
.cpu_cache_mode = .write_combined,
.storage_mode = storage_mode,
},
);
// Initialize
const id = device.msgSend(
?*anyopaque,
objc.sel("newTextureWithDescriptor:"),
.{desc},
) orelse return error.MetalFailed;
return objc.Object.fromId(id);
}
};

View File

@@ -1,38 +0,0 @@
const std = @import("std");
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,
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"), .{});
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,
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"), .{});
}
};

View File

@@ -6,6 +6,7 @@ const objc = @import("objc");
const math = @import("../../math.zig");
const mtl = @import("api.zig");
const Pipeline = @import("Pipeline.zig");
const log = std.log.scoped(.metal);
@@ -14,20 +15,24 @@ pub const Shaders = struct {
library: objc.Object,
/// Renders cell foreground elements (text, decorations).
cell_text_pipeline: objc.Object,
cell_text_pipeline: Pipeline,
/// The cell background shader is the shader used to render the
/// background of terminal cells.
cell_bg_pipeline: objc.Object,
cell_bg_pipeline: Pipeline,
/// The image shader is the shader used to render images for things
/// like the Kitty image protocol.
image_pipeline: objc.Object,
image_pipeline: Pipeline,
/// 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,
post_pipelines: []const Pipeline,
/// Set to true when deinited, if you try to deinit a defunct set
/// of shaders it will just be ignored, to prevent double-free.
defunct: bool = false,
/// Initialize our shader set.
///
@@ -44,15 +49,15 @@ pub const Shaders = struct {
errdefer library.msgSend(void, objc.sel("release"), .{});
const cell_text_pipeline = try initCellTextPipeline(device, library, pixel_format);
errdefer cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
errdefer cell_text_pipeline.deinit();
const cell_bg_pipeline = try initCellBgPipeline(device, library, pixel_format);
errdefer cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
errdefer cell_bg_pipeline.deinit();
const image_pipeline = try initImagePipeline(device, library, pixel_format);
errdefer image_pipeline.msgSend(void, objc.sel("release"), .{});
errdefer image_pipeline.deinit();
const post_pipelines: []const objc.Object = initPostPipelines(
const post_pipelines: []const Pipeline = initPostPipelines(
alloc,
device,
library,
@@ -66,7 +71,7 @@ pub const Shaders = struct {
break :err &.{};
};
errdefer if (post_pipelines.len > 0) {
for (post_pipelines) |pipeline| pipeline.msgSend(void, objc.sel("release"), .{});
for (post_pipelines) |pipeline| pipeline.deinit();
alloc.free(post_pipelines);
};
@@ -80,16 +85,19 @@ pub const Shaders = struct {
}
pub fn deinit(self: *Shaders, alloc: Allocator) void {
if (self.defunct) return;
self.defunct = true;
// Release our primary shaders
self.cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
self.cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
self.image_pipeline.msgSend(void, objc.sel("release"), .{});
self.cell_text_pipeline.deinit();
self.cell_bg_pipeline.deinit();
self.image_pipeline.deinit();
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"), .{});
pipeline.deinit();
}
alloc.free(self.post_pipelines);
}
@@ -106,7 +114,7 @@ pub const Image = extern struct {
/// The uniforms that are passed to the terminal cell shader.
pub const Uniforms = extern struct {
// Note: all of the explicit aligmnments are copied from the
// Note: all of the explicit alignments are copied from the
// MSL developer reference just so that we can be sure that we got
// it all exactly right.
@@ -140,25 +148,30 @@ pub const Uniforms = extern struct {
/// The background color for the whole surface.
bg_color: [4]u8 align(4),
/// Whether the cursor is 2 cells wide.
cursor_wide: bool align(1),
/// Various booleans.
///
/// TODO: Maybe put these in a packed struct, like for OpenGL.
bools: extern struct {
/// Whether the cursor is 2 cells wide.
cursor_wide: bool align(1),
/// Indicates that colors provided to the shader are already in
/// the P3 color space, so they don't need to be converted from
/// sRGB.
use_display_p3: bool align(1),
/// Indicates that colors provided to the shader are already in
/// the P3 color space, so they don't need to be converted from
/// sRGB.
use_display_p3: bool align(1),
/// Indicates that the color attachments for the shaders have
/// an `*_srgb` pixel format, which means the shaders need to
/// output linear RGB colors rather than gamma encoded colors,
/// since blending will be performed in linear space and then
/// Metal itself will re-encode the colors for storage.
use_linear_blending: bool align(1),
/// Indicates that the color attachments for the shaders have
/// an `*_srgb` pixel format, which means the shaders need to
/// output linear RGB colors rather than gamma encoded colors,
/// since blending will be performed in linear space and then
/// Metal itself will re-encode the colors for storage.
use_linear_blending: bool align(1),
/// Enables a weight correction step that makes text rendered
/// with linear alpha blending have a similar apparent weight
/// (thickness) to gamma-incorrect blending.
use_linear_correction: bool align(1) = false,
/// Enables a weight correction step that makes text rendered
/// with linear alpha blending have a similar apparent weight
/// (thickness) to gamma-incorrect blending.
use_linear_correction: bool align(1) = false,
},
const PaddingExtend = packed struct(u8) {
left: bool = false,
@@ -171,7 +184,7 @@ 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
// Note: all of the explicit alignments 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),
@@ -214,15 +227,16 @@ 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.
/// 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,
library: objc.Object,
shaders: []const [:0]const u8,
pixel_format: mtl.MTLPixelFormat,
) ![]const objc.Object {
) ![]const Pipeline {
// If we have no shaders, do nothing.
if (shaders.len == 0) return &.{};
@@ -230,10 +244,10 @@ fn initPostPipelines(
var i: usize = 0;
// Initialize our result set. If any error happens, we undo everything.
var pipelines = try alloc.alloc(objc.Object, shaders.len);
var pipelines = try alloc.alloc(Pipeline, shaders.len);
errdefer {
for (pipelines[0..i]) |pipeline| {
pipeline.msgSend(void, objc.sel("release"), .{});
pipeline.deinit();
}
alloc.free(pipelines);
}
@@ -259,7 +273,7 @@ fn initPostPipeline(
library: objc.Object,
data: [:0]const u8,
pixel_format: mtl.MTLPixelFormat,
) !objc.Object {
) !Pipeline {
// Create our library which has the shader source
const post_library = library: {
const source = try macos.foundation.String.createWithBytes(
@@ -282,65 +296,19 @@ fn initPostPipeline(
};
defer post_library.msgSend(void, objc.sel("release"), .{});
// Get our vertex and fragment functions
const func_vert = func_vert: {
const str = try macos.foundation.String.createWithBytes(
"full_screen_vertex",
.utf8,
false,
);
defer str.release();
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.?);
};
defer func_vert.msgSend(void, objc.sel("release"), .{});
defer func_frag.msgSend(void, objc.sel("release"), .{});
// 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;
};
defer desc.msgSend(void, objc.sel("release"), .{});
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)},
);
attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
}
// 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;
return try Pipeline.init(null, .{
.device = device,
.vertex_fn = "full_screen_vertex",
.fragment_fn = "main0",
.vertex_library = library,
.fragment_library = post_library,
.attachments = &.{
.{
.pixel_format = pixel_format,
.blending_enabled = false,
},
},
});
}
/// This is a single parameter for the terminal cell shader.
@@ -373,114 +341,21 @@ fn initCellTextPipeline(
device: objc.Object,
library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
) !objc.Object {
// Get our vertex and fragment functions
const func_vert = func_vert: {
const str = try macos.foundation.String.createWithBytes(
"cell_text_vertex",
.utf8,
false,
);
defer str.release();
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(
"cell_text_fragment",
.utf8,
false,
);
defer str.release();
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")
// rendering, so this makes it so that each instance gets a single
// Cell as input.
const vertex_desc = vertex_desc: {
const desc = init: {
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;
};
// Our attributes are the fields of the input
const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes"));
autoAttribute(CellText, attrs);
// The layout describes how and when we fetch the next vertex input.
const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts"));
{
const layout = layouts.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 0)},
);
// Access each Cell per instance, not per vertex.
layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance));
layout.setProperty("stride", @as(c_ulong, @sizeOf(CellText)));
}
break :vertex_desc desc;
};
defer vertex_desc.msgSend(void, objc.sel("release"), .{});
// 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;
};
defer desc.msgSend(void, objc.sel("release"), .{});
// Set our properties
desc.setProperty("vertexFunction", func_vert);
desc.setProperty("fragmentFunction", func_frag);
desc.setProperty("vertexDescriptor", vertex_desc);
// 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)},
);
attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
// Blending. This is required so that our text we render on top
// of our drawable properly blends into the bg.
attachment.setProperty("blendingEnabled", true);
attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
}
// Make our state
var err: ?*anyopaque = null;
const pipeline_state = device.msgSend(
objc.Object,
objc.sel("newRenderPipelineStateWithDescriptor:error:"),
.{ desc, &err },
);
try checkError(err);
errdefer pipeline_state.msgSend(void, objc.sel("release"), .{});
return pipeline_state;
) !Pipeline {
return try Pipeline.init(CellText, .{
.device = device,
.vertex_fn = "cell_text_vertex",
.fragment_fn = "cell_text_fragment",
.vertex_library = library,
.fragment_library = library,
.step_fn = .per_instance,
.attachments = &.{
.{
.pixel_format = pixel_format,
.blending_enabled = true,
},
},
});
}
/// This is a single parameter for the cell bg shader.
@@ -491,79 +366,20 @@ fn initCellBgPipeline(
device: objc.Object,
library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
) !objc.Object {
// Get our vertex and fragment functions
const func_vert = func_vert: {
const str = try macos.foundation.String.createWithBytes(
"cell_bg_vertex",
.utf8,
false,
);
defer str.release();
const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
break :func_vert objc.Object.fromId(ptr.?);
};
defer func_vert.msgSend(void, objc.sel("release"), .{});
const func_frag = func_frag: {
const str = try macos.foundation.String.createWithBytes(
"cell_bg_fragment",
.utf8,
false,
);
defer str.release();
const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
break :func_frag objc.Object.fromId(ptr.?);
};
defer func_frag.msgSend(void, objc.sel("release"), .{});
// 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;
};
defer desc.msgSend(void, objc.sel("release"), .{});
// Set our properties
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)},
);
attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
// Blending. This is required so that our text we render on top
// of our drawable properly blends into the bg.
attachment.setProperty("blendingEnabled", true);
attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
}
// Make our state
var err: ?*anyopaque = null;
const pipeline_state = device.msgSend(
objc.Object,
objc.sel("newRenderPipelineStateWithDescriptor:error:"),
.{ desc, &err },
);
try checkError(err);
errdefer pipeline_state.msgSend(void, objc.sel("release"), .{});
return pipeline_state;
) !Pipeline {
return try Pipeline.init(null, .{
.device = device,
.vertex_fn = "cell_bg_vertex",
.fragment_fn = "cell_bg_fragment",
.vertex_library = library,
.fragment_library = library,
.attachments = &.{
.{
.pixel_format = pixel_format,
.blending_enabled = false,
},
},
});
}
/// Initialize the image render pipeline for our shader library.
@@ -571,148 +387,21 @@ fn initImagePipeline(
device: objc.Object,
library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
) !objc.Object {
// Get our vertex and fragment functions
const func_vert = func_vert: {
const str = try macos.foundation.String.createWithBytes(
"image_vertex",
.utf8,
false,
);
defer str.release();
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(
"image_fragment",
.utf8,
false,
);
defer str.release();
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")
// rendering, so this makes it so that each instance gets a single
// Image as input.
const vertex_desc = vertex_desc: {
const desc = init: {
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;
};
// Our attributes are the fields of the input
const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes"));
autoAttribute(Image, attrs);
// The layout describes how and when we fetch the next vertex input.
const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts"));
{
const layout = layouts.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 0)},
);
// Access each Image per instance, not per vertex.
layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance));
layout.setProperty("stride", @as(c_ulong, @sizeOf(Image)));
}
break :vertex_desc desc;
};
defer vertex_desc.msgSend(void, objc.sel("release"), .{});
// 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;
};
defer desc.msgSend(void, objc.sel("release"), .{});
// Set our properties
desc.setProperty("vertexFunction", func_vert);
desc.setProperty("fragmentFunction", func_frag);
desc.setProperty("vertexDescriptor", vertex_desc);
// 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)},
);
attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
// Blending. This is required so that our text we render on top
// of our drawable properly blends into the bg.
attachment.setProperty("blendingEnabled", true);
attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
}
// 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;
}
fn autoAttribute(T: type, attrs: objc.Object) void {
inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
const offset = @offsetOf(T, field.name);
const FT = switch (@typeInfo(field.type)) {
.@"enum" => |e| e.tag_type,
else => field.type,
};
const format = switch (FT) {
[4]u8 => mtl.MTLVertexFormat.uchar4,
[2]u16 => mtl.MTLVertexFormat.ushort2,
[2]i16 => mtl.MTLVertexFormat.short2,
[2]f32 => mtl.MTLVertexFormat.float2,
[4]f32 => mtl.MTLVertexFormat.float4,
[2]i32 => mtl.MTLVertexFormat.int2,
u32 => mtl.MTLVertexFormat.uint,
[2]u32 => mtl.MTLVertexFormat.uint2,
[4]u32 => mtl.MTLVertexFormat.uint4,
u8 => mtl.MTLVertexFormat.uchar,
else => comptime unreachable,
};
const attr = attrs.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, i)},
);
attr.setProperty("format", @intFromEnum(format));
attr.setProperty("offset", @as(c_ulong, offset));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
) !Pipeline {
return try Pipeline.init(Image, .{
.device = device,
.vertex_fn = "image_vertex",
.fragment_fn = "image_fragment",
.vertex_library = library,
.fragment_library = library,
.step_fn = .per_instance,
.attachments = &.{
.{
.pixel_format = pixel_format,
.blending_enabled = true,
},
},
});
}
fn checkError(err_: ?*anyopaque) !void {

View File

@@ -1,196 +0,0 @@
/// 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.
pub 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_offset
glyph_offset_x: i32 = 0,
glyph_offset_y: i32 = 0,
/// vec4 color_in
r: u8,
g: u8,
b: u8,
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,
};
pub const CellMode = enum(u8) {
bg = 1,
fg = 2,
fg_constrained = 3,
fg_color = 7,
fg_powerline = 15,
// 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 isFg(self: CellMode) bool {
// Since we use bit tricks below, we want to ensure the enum
// doesn't change without us looking at this logic again.
comptime {
const info = @typeInfo(CellMode).@"enum";
std.debug.assert(info.fields.len == 5);
}
return @intFromEnum(self) & @intFromEnum(@as(CellMode, .fg)) != 0;
}
};
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"),
);
errdefer program.destroy();
// 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(.element_array);
defer ebobind.unbind();
try ebobind.setData([6]u8{
0, 1, 3, // Top-left triangle
1, 2, 3, // Bottom-right triangle
}, .static_draw);
// Vertex buffer (VBO)
const vbo = try gl.Buffer.create();
errdefer vbo.destroy();
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);
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(.element_array);
errdefer ebo.unbind();
const vbo = try self.vbo.bind(.array);
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();
}
};

View File

@@ -0,0 +1,75 @@
//! Wrapper for handling render passes.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const gl = @import("opengl");
const Renderer = @import("../generic.zig").Renderer(OpenGL);
const OpenGL = @import("../OpenGL.zig");
const Target = @import("Target.zig");
const Pipeline = @import("Pipeline.zig");
const RenderPass = @import("RenderPass.zig");
const Buffer = @import("buffer.zig").Buffer;
const Health = @import("../../renderer.zig").Health;
const log = std.log.scoped(.opengl);
/// Options for beginning a frame.
pub const Options = struct {};
renderer: *Renderer,
target: *Target,
/// Begin encoding a frame.
pub fn begin(
opts: Options,
/// Once the frame has been completed, the `frameCompleted` method
/// on the renderer is called with the health status of the frame.
renderer: *Renderer,
/// The target is presented via the provided renderer's API when completed.
target: *Target,
) !Self {
_ = opts;
return .{
.renderer = renderer,
.target = target,
};
}
/// Add a render pass to this frame with the provided attachments.
/// Returns a RenderPass which allows render steps to be added.
pub inline fn renderPass(
self: *const Self,
attachments: []const RenderPass.Options.Attachment,
) RenderPass {
_ = self;
return RenderPass.begin(.{ .attachments = attachments });
}
/// Complete this frame and present the target.
///
/// If `sync` is true, this will block until the frame is presented.
///
/// NOTE: For OpenGL, `sync` is ignored and we always block.
pub fn complete(self: *const Self, sync: bool) void {
_ = sync;
gl.finish();
// If there are any GL errors, consider the frame unhealthy.
const health: Health = if (gl.errors.getError()) .healthy else |_| .unhealthy;
// If the frame is healthy, present it.
if (health == .healthy) {
self.renderer.api.present(self.target.*) catch |err| {
log.err("Failed to present render target: err={}", .{err});
};
}
// Report the health to the renderer.
self.renderer.frameCompleted(health);
}

View File

@@ -1,134 +0,0 @@
/// The OpenGL program for rendering terminal cells.
const ImageProgram = @This();
const std = @import("std");
const gl = @import("opengl");
program: gl.Program,
vao: gl.VertexArray,
ebo: gl.Buffer,
vbo: gl.Buffer,
pub const Input = extern struct {
/// vec2 grid_coord
grid_col: i32,
grid_row: i32,
/// vec2 cell_offset
cell_offset_x: u32 = 0,
cell_offset_y: u32 = 0,
/// vec4 source_rect
source_x: u32 = 0,
source_y: u32 = 0,
source_width: u32 = 0,
source_height: u32 = 0,
/// vec2 dest_size
dest_width: u32 = 0,
dest_height: u32 = 0,
};
pub fn init() !ImageProgram {
// Load and compile our shaders.
const program = try gl.Program.createVF(
@embedFile("../shaders/image.v.glsl"),
@embedFile("../shaders/image.f.glsl"),
);
errdefer program.destroy();
// Set our program uniforms
const pbind = try program.use();
defer pbind.unbind();
// Set all of our texture indexes
try program.setUniform("image", 0);
// 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(.element_array);
defer ebobind.unbind();
try ebobind.setData([6]u8{
0, 1, 3, // Top-left triangle
1, 2, 3, // Bottom-right triangle
}, .static_draw);
// Vertex buffer (VBO)
const vbo = try gl.Buffer.create();
errdefer vbo.destroy();
var vbobind = try vbo.bind(.array);
defer vbobind.unbind();
var offset: usize = 0;
try vbobind.attributeAdvanced(0, 2, gl.c.GL_INT, false, @sizeOf(Input), offset);
offset += 2 * @sizeOf(i32);
try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset);
offset += 2 * @sizeOf(u32);
try vbobind.attributeAdvanced(2, 4, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset);
offset += 4 * @sizeOf(u32);
try vbobind.attributeAdvanced(3, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset);
offset += 2 * @sizeOf(u32);
try vbobind.enableAttribArray(0);
try vbobind.enableAttribArray(1);
try vbobind.enableAttribArray(2);
try vbobind.enableAttribArray(3);
try vbobind.attributeDivisor(0, 1);
try vbobind.attributeDivisor(1, 1);
try vbobind.attributeDivisor(2, 1);
try vbobind.attributeDivisor(3, 1);
return .{
.program = program,
.vao = vao,
.ebo = ebo,
.vbo = vbo,
};
}
pub fn bind(self: ImageProgram) !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();
const vbo = try self.vbo.bind(.array);
errdefer vbo.unbind();
return .{
.program = program,
.vao = vao,
.ebo = ebo,
.vbo = vbo,
};
}
pub fn deinit(self: ImageProgram) 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();
}
};

View File

@@ -0,0 +1,169 @@
//! Wrapper for handling render pipelines.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const gl = @import("opengl");
const OpenGL = @import("../OpenGL.zig");
const Texture = @import("Texture.zig");
const Buffer = @import("buffer.zig").Buffer;
const log = std.log.scoped(.opengl);
/// Options for initializing a render pipeline.
pub const Options = struct {
/// GLSL source of the vertex function
vertex_fn: [:0]const u8,
/// GLSL source of the fragment function
fragment_fn: [:0]const u8,
/// Vertex step function
step_fn: StepFunction = .per_vertex,
/// Whether to enable blending.
blending_enabled: bool = true,
pub const StepFunction = enum {
constant,
per_vertex,
per_instance,
};
};
program: gl.Program,
fbo: gl.Framebuffer,
vao: gl.VertexArray,
stride: usize,
blending_enabled: bool,
pub fn init(comptime VertexAttributes: ?type, opts: Options) !Self {
// Load and compile our shaders.
const program = try gl.Program.createVF(
opts.vertex_fn,
opts.fragment_fn,
);
errdefer program.destroy();
const pbind = try program.use();
defer pbind.unbind();
const fbo = try gl.Framebuffer.create();
errdefer fbo.destroy();
const fbobind = try fbo.bind(.framebuffer);
defer fbobind.unbind();
const vao = try gl.VertexArray.create();
errdefer vao.destroy();
const vaobind = try vao.bind();
defer vaobind.unbind();
if (VertexAttributes) |VA| try autoAttribute(VA, vaobind, opts.step_fn);
return .{
.program = program,
.fbo = fbo,
.vao = vao,
.stride = if (VertexAttributes) |VA| @sizeOf(VA) else 0,
.blending_enabled = opts.blending_enabled,
};
}
pub fn deinit(self: *const Self) void {
self.program.destroy();
}
fn autoAttribute(
T: type,
vaobind: gl.VertexArray.Binding,
step_fn: Options.StepFunction,
) !void {
const divisor: gl.c.GLuint = switch (step_fn) {
.per_vertex => 0,
.per_instance => 1,
.constant => std.math.maxInt(gl.c.GLuint),
};
inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
try vaobind.enableAttribArray(i);
try vaobind.attributeBinding(i, 0);
try vaobind.bindingDivisor(i, divisor);
const offset = @offsetOf(T, field.name);
const FT = switch (@typeInfo(field.type)) {
.@"enum" => |e| e.tag_type,
else => field.type,
};
const size, const IT = switch (@typeInfo(FT)) {
.array => |a| .{ a.len, a.child },
else => .{ 1, FT },
};
try switch (IT) {
u8 => vaobind.attributeIFormat(
i,
size,
gl.c.GL_UNSIGNED_BYTE,
offset,
),
u16 => vaobind.attributeIFormat(
i,
size,
gl.c.GL_UNSIGNED_SHORT,
offset,
),
u32 => vaobind.attributeIFormat(
i,
size,
gl.c.GL_UNSIGNED_INT,
offset,
),
i8 => vaobind.attributeIFormat(
i,
size,
gl.c.GL_BYTE,
offset,
),
i16 => vaobind.attributeIFormat(
i,
size,
gl.c.GL_SHORT,
offset,
),
i32 => vaobind.attributeIFormat(
i,
size,
gl.c.GL_INT,
offset,
),
f16 => vaobind.attributeFormat(
i,
size,
gl.c.GL_HALF_FLOAT,
false,
offset,
),
f32 => vaobind.attributeFormat(
i,
size,
gl.c.GL_FLOAT,
false,
offset,
),
f64 => vaobind.attributeLFormat(
i,
size,
offset,
),
else => unreachable,
};
}
}

View File

@@ -0,0 +1,141 @@
//! Wrapper for handling render passes.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const gl = @import("opengl");
const OpenGL = @import("../OpenGL.zig");
const Target = @import("Target.zig");
const Texture = @import("Texture.zig");
const Pipeline = @import("Pipeline.zig");
const RenderPass = @import("RenderPass.zig");
const Buffer = @import("buffer.zig").Buffer;
/// Options for beginning a render pass.
pub const Options = struct {
/// Color attachments for this render pass.
attachments: []const Attachment,
/// Describes a color attachment.
pub const Attachment = struct {
target: union(enum) {
texture: Texture,
target: Target,
},
clear_color: ?[4]f32 = null,
};
};
/// Describes a step in a render pass.
pub const Step = struct {
pipeline: Pipeline,
uniforms: ?gl.Buffer = null,
buffers: []const ?gl.Buffer = &.{},
textures: []const ?Texture = &.{},
draw: Draw,
/// Describes the draw call for this step.
pub const Draw = struct {
type: gl.Primitive,
vertex_count: usize,
instance_count: usize = 1,
};
};
attachments: []const Options.Attachment,
step_number: usize = 0,
/// Begin a render pass.
pub fn begin(
opts: Options,
) Self {
return .{
.attachments = opts.attachments,
};
}
/// Add a step to this render pass.
///
/// TODO: Errors are silently ignored in this function, maybe they shouldn't be?
pub fn step(self: *Self, s: Step) void {
if (s.draw.instance_count == 0) return;
const pbind = s.pipeline.program.use() catch return;
defer pbind.unbind();
const vaobind = s.pipeline.vao.bind() catch return;
defer vaobind.unbind();
const fbobind = switch (self.attachments[0].target) {
.target => |t| t.framebuffer.bind(.framebuffer) catch return,
.texture => |t| bind: {
const fbobind = s.pipeline.fbo.bind(.framebuffer) catch return;
fbobind.texture2D(.color0, t.target, t.texture, 0) catch {
fbobind.unbind();
return;
};
break :bind fbobind;
},
};
defer fbobind.unbind();
defer self.step_number += 1;
// If we have a clear color and this is the
// first step in the pass, go ahead and clear.
if (self.step_number == 0) if (self.attachments[0].clear_color) |c| {
gl.clearColor(c[0], c[1], c[2], c[3]);
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
};
// Bind the uniform buffer we bind at index 1 to align with Metal.
if (s.uniforms) |ubo| {
_ = ubo.bindBase(.uniform, 1) catch return;
}
// Bind relevant texture units.
for (s.textures, 0..) |t, i| if (t) |tex| {
gl.Texture.active(@intCast(i)) catch return;
_ = tex.texture.bind(tex.target) catch return;
};
// Bind 0th buffer as the vertex buffer,
// and bind the rest as storage buffers.
if (s.buffers.len > 0) {
if (s.buffers[0]) |vbo| vaobind.bindVertexBuffer(
0,
vbo.id,
0,
@intCast(s.pipeline.stride),
) catch return;
for (s.buffers[1..], 1..) |b, i| if (b) |buf| {
_ = buf.bindBase(.storage, @intCast(i)) catch return;
};
}
if (s.pipeline.blending_enabled) {
gl.enable(gl.c.GL_BLEND) catch return;
gl.blendFunc(gl.c.GL_ONE, gl.c.GL_ONE_MINUS_SRC_ALPHA) catch return;
} else {
gl.disable(gl.c.GL_BLEND) catch return;
}
gl.drawArraysInstanced(
s.draw.type,
0,
@intCast(s.draw.vertex_count),
@intCast(s.draw.instance_count),
) catch return;
}
/// Complete this render pass.
/// This struct can no longer be used after calling this.
pub fn complete(self: *const Self) void {
_ = self;
gl.flush();
}

View File

@@ -0,0 +1,62 @@
//! Represents a render target.
//!
//! In this case, an OpenGL renderbuffer-backed framebuffer.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const gl = @import("opengl");
const log = std.log.scoped(.opengl);
/// Options for initializing a Target
pub const Options = struct {
/// Desired width
width: usize,
/// Desired height
height: usize,
/// Internal format for the renderbuffer.
internal_format: gl.Texture.InternalFormat,
};
/// The underlying `gl.Framebuffer` instance.
framebuffer: gl.Framebuffer,
/// The underlying `gl.Renderbuffer` instance.
renderbuffer: gl.Renderbuffer,
/// Current width of this target.
width: usize,
/// Current height of this target.
height: usize,
pub fn init(opts: Options) !Self {
const rbo = try gl.Renderbuffer.create();
const bound_rbo = try rbo.bind();
defer bound_rbo.unbind();
try bound_rbo.storage(
opts.internal_format,
@intCast(opts.width),
@intCast(opts.height),
);
const fbo = try gl.Framebuffer.create();
const bound_fbo = try fbo.bind(.framebuffer);
defer bound_fbo.unbind();
try bound_fbo.renderbuffer(.color0, rbo);
return .{
.framebuffer = fbo,
.renderbuffer = rbo,
.width = opts.width,
.height = opts.height,
};
}
pub fn deinit(self: *Self) void {
self.framebuffer.destroy();
self.renderbuffer.destroy();
}

View File

@@ -0,0 +1,103 @@
//! Wrapper for handling textures.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const gl = @import("opengl");
const OpenGL = @import("../OpenGL.zig");
const log = std.log.scoped(.opengl);
/// Options for initializing a texture.
pub const Options = struct {
format: gl.Texture.Format,
internal_format: gl.Texture.InternalFormat,
target: gl.Texture.Target,
};
texture: gl.Texture,
/// The width of this texture.
width: usize,
/// The height of this texture.
height: usize,
/// Format for this texture.
format: gl.Texture.Format,
/// Target for this texture.
target: gl.Texture.Target,
pub const Error = error{
/// An OpenGL API call failed.
OpenGLFailed,
};
/// Initialize a texture
pub fn init(
opts: Options,
width: usize,
height: usize,
data: ?[]const u8,
) Error!Self {
const tex = gl.Texture.create() catch return error.OpenGLFailed;
errdefer tex.destroy();
{
const texbind = tex.bind(opts.target) catch return error.OpenGLFailed;
defer texbind.unbind();
texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed;
texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed;
texbind.parameter(.MinFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed;
texbind.parameter(.MagFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed;
texbind.image2D(
0,
opts.internal_format,
@intCast(width),
@intCast(height),
0,
opts.format,
.UnsignedByte,
if (data) |d| @ptrCast(d.ptr) else null,
) catch return error.OpenGLFailed;
}
return .{
.texture = tex,
.width = width,
.height = height,
.format = opts.format,
.target = opts.target,
};
}
pub fn deinit(self: Self) void {
self.texture.destroy();
}
/// Replace a region of the texture with the provided data.
///
/// Does NOT check the dimensions of the data to ensure correctness.
pub fn replaceRegion(
self: Self,
x: usize,
y: usize,
width: usize,
height: usize,
data: []const u8,
) Error!void {
const texbind = self.texture.bind(self.target) catch return error.OpenGLFailed;
defer texbind.unbind();
texbind.subImage2D(
0,
@intCast(x),
@intCast(y),
@intCast(width),
@intCast(height),
self.format,
.UnsignedByte,
data.ptr,
) catch return error.OpenGLFailed;
}

View File

@@ -0,0 +1,127 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const gl = @import("opengl");
const OpenGL = @import("../OpenGL.zig");
const log = std.log.scoped(.opengl);
/// Options for initializing a buffer.
pub const Options = struct {
target: gl.Buffer.Target = .array,
usage: gl.Buffer.Usage = .dynamic_draw,
};
/// OpenGL data storage for a certain set of equal types. This is usually
/// used for vertex buffers, etc. This helpful wrapper makes it easy to
/// prealloc, shrink, grow, sync, buffers with OpenGL.
pub fn Buffer(comptime T: type) type {
return struct {
const Self = @This();
/// Underlying `gl.Buffer` instance.
buffer: gl.Buffer,
/// Options this buffer was allocated with.
opts: Options,
/// Current allocated length of the data store.
/// Note this is the number of `T`s, not the size in bytes.
len: usize,
/// Initialize a buffer with the given length pre-allocated.
pub fn init(opts: Options, len: usize) !Self {
const buffer = try gl.Buffer.create();
errdefer buffer.destroy();
const binding = try buffer.bind(opts.target);
defer binding.unbind();
try binding.setDataNullManual(len * @sizeOf(T), opts.usage);
return .{
.buffer = buffer,
.opts = opts,
.len = len,
};
}
/// Init the buffer filled with the given data.
pub fn initFill(opts: Options, data: []const T) !Self {
const buffer = try gl.Buffer.create();
errdefer buffer.destroy();
const binding = try buffer.bind(opts.target);
defer binding.unbind();
try binding.setData(data, opts.usage);
return .{
.buffer = buffer,
.opts = opts,
.len = data.len * @sizeOf(T),
};
}
pub fn deinit(self: Self) void {
self.buffer.destroy();
}
/// Sync new contents to the buffer. The data is expected to be the
/// complete contents of the buffer. If the amount of data is larger
/// than the buffer length, the buffer will be reallocated.
///
/// If the amount of data is smaller than the buffer length, the
/// remaining data in the buffer is left untouched.
pub fn sync(self: *Self, data: []const T) !void {
const binding = try self.buffer.bind(self.opts.target);
defer binding.unbind();
// If we need more space than our buffer has, we need to reallocate.
if (data.len > self.len) {
// Reallocate the buffer to hold double what we require.
self.len = data.len * 2;
try binding.setDataNullManual(
self.len * @sizeOf(T),
self.opts.usage,
);
}
// We can fit within the buffer so we can just replace bytes.
try binding.setSubData(0, data);
}
/// Like Buffer.sync but takes data from an array of ArrayLists,
/// rather than a single array. Returns the number of items synced.
pub fn syncFromArrayLists(self: *Self, lists: []const std.ArrayListUnmanaged(T)) !usize {
const binding = try self.buffer.bind(self.opts.target);
defer binding.unbind();
var total_len: usize = 0;
for (lists) |list| {
total_len += list.items.len;
}
// If we need more space than our buffer has, we need to reallocate.
if (total_len > self.len) {
// Reallocate the buffer to hold double what we require.
self.len = total_len * 2;
try binding.setDataNullManual(
self.len * @sizeOf(T),
self.opts.usage,
);
}
// We can fit within the buffer so we can just replace bytes.
var i: usize = 0;
for (lists) |list| {
try binding.setSubData(i, list.items);
i += list.items.len * @sizeOf(T);
}
return total_len;
}
};
}

View File

@@ -0,0 +1,220 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const renderer = @import("../../renderer.zig");
const terminal = @import("../../terminal/main.zig");
const shaderpkg = @import("shaders.zig");
/// The possible cell content keys that exist.
pub const Key = enum {
bg,
text,
underline,
strikethrough,
overline,
/// Returns the GPU vertex type for this key.
pub fn CellType(self: Key) type {
return switch (self) {
.bg => shaderpkg.CellBg,
.text,
.underline,
.strikethrough,
.overline,
=> shaderpkg.CellText,
};
}
};
/// A pool of ArrayLists with methods for bulk operations.
fn ArrayListPool(comptime T: type) type {
return struct {
const Self = ArrayListPool(T);
const ArrayListT = std.ArrayListUnmanaged(T);
// An array containing the lists that belong to this pool.
lists: []ArrayListT = &[_]ArrayListT{},
// The pool will be initialized with empty ArrayLists.
pub fn init(alloc: Allocator, list_count: usize, initial_capacity: usize) !Self {
const self: Self = .{
.lists = try alloc.alloc(ArrayListT, list_count),
};
for (self.lists) |*list| {
list.* = try ArrayListT.initCapacity(alloc, initial_capacity);
}
return self;
}
pub fn deinit(self: *Self, alloc: Allocator) void {
for (self.lists) |*list| {
list.deinit(alloc);
}
alloc.free(self.lists);
}
/// Clear all lists in the pool.
pub fn reset(self: *Self) void {
for (self.lists) |*list| {
list.clearRetainingCapacity();
}
}
};
}
/// The contents of all the cells in the terminal.
///
/// The goal of this data structure is to allow for efficient row-wise
/// clearing of data from the GPU buffers, to allow for row-wise dirty
/// tracking to eliminate the overhead of rebuilding the GPU buffers
/// each frame.
///
/// Must be initialized by resizing before calling any operations.
pub const Contents = struct {
size: renderer.GridSize = .{ .rows = 0, .columns = 0 },
/// Flat array containing cell background colors for the terminal grid.
///
/// Indexed as `bg_cells[row * size.columns + col]`.
///
/// Prefer accessing with `Contents.bgCell(row, col).*` instead
/// of directly indexing in order to avoid integer size bugs.
bg_cells: []shaderpkg.CellBg = undefined,
/// The ArrayListPool which holds all of the foreground cells. When sized
/// with Contents.resize the individual ArrayLists are given enough room
/// that they can hold a single row with #cols glyphs, underlines, and
/// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
/// it is possible to exceed this with combining glyphs that add a glyph
/// but take up no column since they combine with the previous one, as
/// well as with fonts that perform multi-substitutions for glyphs, which
/// can result in a similar situation where multiple glyphs reside in the
/// same column.
///
/// Allocations should nevertheless be exceedingly rare since hitting the
/// initial capacity of a list would require a row filled with underlined
/// struck through characters, at least one of which is a multi-glyph
/// composite.
///
/// Rows are indexed as Contents.fg_rows[y + 1], because the first list in
/// the pool is reserved for the cursor, which must be the first item in
/// the buffer.
///
/// Must be initialized by calling resize on the Contents struct before
/// calling any operations.
fg_rows: ArrayListPool(shaderpkg.CellText) = .{},
pub fn deinit(self: *Contents, alloc: Allocator) void {
alloc.free(self.bg_cells);
self.fg_rows.deinit(alloc);
}
/// Resize the cell contents for the given grid size. This will
/// always invalidate the entire cell contents.
pub fn resize(
self: *Contents,
alloc: Allocator,
size: renderer.GridSize,
) !void {
self.size = size;
const cell_count = @as(usize, size.columns) * @as(usize, size.rows);
const bg_cells = try alloc.alloc(shaderpkg.CellBg, cell_count);
errdefer alloc.free(bg_cells);
@memset(bg_cells, .{ 0, 0, 0, 0 });
// The foreground lists can hold 3 types of items:
// - Glyphs
// - Underlines
// - Strikethroughs
// So we give them an initial capacity of size.columns * 3, which will
// avoid any further allocations in the vast majority of cases. Sadly
// we can not assume capacity though, since with combining glyphs that
// form a single grapheme, and multi-substitutions in fonts, the number
// of glyphs in a row is theoretically unlimited.
//
// We have size.rows + 1 lists because index 0 is used for a special
// list containing the cursor cell which needs to be first in the buffer.
var fg_rows = try ArrayListPool(shaderpkg.CellText).init(alloc, size.rows + 1, size.columns * 3);
errdefer fg_rows.deinit(alloc);
alloc.free(self.bg_cells);
self.fg_rows.deinit(alloc);
self.bg_cells = bg_cells;
self.fg_rows = fg_rows;
// We don't need 3*cols worth of cells for the cursor list, so we can
// replace it with a smaller list. This is technically a tiny bit of
// extra work but resize is not a hot function so it's worth it to not
// waste the memory.
self.fg_rows.lists[0].deinit(alloc);
self.fg_rows.lists[0] = try std.ArrayListUnmanaged(shaderpkg.CellText).initCapacity(alloc, 1);
}
/// Reset the cell contents to an empty state without resizing.
pub fn reset(self: *Contents) void {
@memset(self.bg_cells, .{ 0, 0, 0, 0 });
self.fg_rows.reset();
}
/// Set the cursor value. If the value is null then the cursor is hidden.
pub fn setCursor(self: *Contents, v: ?shaderpkg.CellText) void {
self.fg_rows.lists[0].clearRetainingCapacity();
if (v) |cell| {
self.fg_rows.lists[0].appendAssumeCapacity(cell);
}
}
/// Access a background cell. Prefer this function over direct indexing
/// of `bg_cells` in order to avoid integer size bugs causing overflows.
pub inline fn bgCell(self: *Contents, row: usize, col: usize) *shaderpkg.CellBg {
return &self.bg_cells[row * self.size.columns + col];
}
/// Add a cell to the appropriate list. Adding the same cell twice will
/// result in duplication in the vertex buffer. The caller should clear
/// the corresponding row with Contents.clear to remove old cells first.
pub fn add(
self: *Contents,
alloc: Allocator,
comptime key: Key,
cell: key.CellType(),
) !void {
const y = cell.grid_pos[1];
assert(y < self.size.rows);
switch (key) {
.bg => comptime unreachable,
.text,
.underline,
.strikethrough,
.overline,
// We have a special list containing the cursor cell at the start
// of our fg row pool, so we need to add 1 to the y to get the
// correct index.
=> try self.fg_rows.lists[y + 1].append(alloc, cell),
}
}
/// Clear all of the cell contents for a given row.
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
assert(y < self.size.rows);
@memset(self.bg_cells[@as(usize, y) * self.size.columns ..][0..self.size.columns], .{ 0, 0, 0, 0 });
// We have a special list containing the cursor cell at the start
// of our fg row pool, so we need to add 1 to the y to get the
// correct index.
self.fg_rows.lists[y + 1].clearRetainingCapacity();
}
};

View File

@@ -1,310 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const gl = @import("opengl");
const Size = @import("../size.zig").Size;
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;
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,
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. 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,
/// 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,
/// The first time a frame was drawn. This is used to update
/// the time uniform.
first_frame_time: std.time.Instant,
/// The last time a 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 {
if (srcs.len == 0) return error.OneCustomShaderRequired;
// 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 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();
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 = .{},
.fbo = fbo,
.ubo = ubo,
.vao = vao,
.ebo = ebo,
.fb_texture = fb_tex,
.first_frame_time = try std.time.Instant.now(),
.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();
self.fb_texture.destroy();
self.fbo.destroy();
}
pub fn setScreenSize(self: *State, size: Size) !void {
// Update our uniforms
self.uniforms.resolution = .{
@floatFromInt(size.screen.width),
@floatFromInt(size.screen.height),
1,
};
try self.syncUniforms();
// Update our texture
const texbind = try self.fb_texture.bind(.@"2D");
try texbind.image2D(
0,
.rgb,
@intCast(size.screen.width),
@intCast(size.screen.height),
0,
.rgb,
.UnsignedByte,
null,
);
}
/// 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.first_frame_time;
const since_ns: f32 = @floatFromInt(now.since(self.first_frame_time));
const delta_ns: f32 = @floatFromInt(now.since(self.last_frame_time));
self.uniforms.time = since_ns / std.time.ns_per_s;
self.uniforms.time_delta = delta_ns / std.time.ns_per_s;
self.last_frame_time = now;
// 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
// 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);
// 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();
const ebo = try self.ebo.bind(.element_array);
errdefer ebo.unbind();
return .{
.vao = vao,
.ebo = ebo,
.fb_texture = texbind,
};
}
/// Copy the fbo's attached texture to the backbuffer.
pub fn copyFramebuffer(self: *State) !void {
const texbind = try self.fb_texture.bind(.@"2D");
errdefer texbind.unbind();
try texbind.copySubImage2D(
0,
0,
0,
0,
0,
@intFromFloat(self.uniforms.resolution[0]),
@intFromFloat(self.uniforms.resolution[1]),
);
}
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();
}
};
};
/// 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,
);
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();
}
/// 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();
return .{
.program = program,
};
}
pub const Binding = struct {
program: gl.Program.Binding,
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,
);
}
};
};

View File

@@ -3,6 +3,8 @@ const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const gl = @import("opengl");
const wuffs = @import("wuffs");
const OpenGL = @import("../OpenGL.zig");
const Texture = OpenGL.Texture;
/// Represents a single image placement on the grid. A placement is a
/// request to render an instance of an image.
@@ -59,15 +61,15 @@ pub const Image = union(enum) {
replace_rgba: Replace,
/// The image is uploaded and ready to be used.
ready: gl.Texture,
ready: Texture,
/// The image is uploaded but is scheduled to be unloaded.
unload_pending: []u8,
unload_ready: gl.Texture,
unload_replace: struct { []u8, gl.Texture },
unload_ready: Texture,
unload_replace: struct { []u8, Texture },
pub const Replace = struct {
texture: gl.Texture,
texture: Texture,
pending: Pending,
};
@@ -99,32 +101,32 @@ pub const Image = union(enum) {
.replace_gray => |r| {
alloc.free(r.pending.dataSlice(1));
r.texture.destroy();
r.texture.deinit();
},
.replace_gray_alpha => |r| {
alloc.free(r.pending.dataSlice(2));
r.texture.destroy();
r.texture.deinit();
},
.replace_rgb => |r| {
alloc.free(r.pending.dataSlice(3));
r.texture.destroy();
r.texture.deinit();
},
.replace_rgba => |r| {
alloc.free(r.pending.dataSlice(4));
r.texture.destroy();
r.texture.deinit();
},
.unload_replace => |r| {
alloc.free(r[0]);
r[1].destroy();
r[1].deinit();
},
.ready,
.unload_ready,
=> |tex| tex.destroy(),
=> |tex| tex.deinit(),
}
}
@@ -168,7 +170,7 @@ pub const Image = union(enum) {
// Get our existing texture. This switch statement will also handle
// scenarios where there is no existing texture and we can modify
// the self pointer directly.
const existing: gl.Texture = switch (self.*) {
const existing: Texture = switch (self.*) {
// For pending, we can free the old data and become pending ourselves.
.pending_gray => |p| {
alloc.free(p.dataSlice(1));
@@ -356,7 +358,10 @@ pub const Image = union(enum) {
pub fn upload(
self: *Image,
alloc: Allocator,
opengl: *const OpenGL,
) !void {
_ = opengl;
// Convert our data if we have to
try self.convert(alloc);
@@ -374,23 +379,15 @@ pub const Image = union(enum) {
};
// Create our texture
const tex = try gl.Texture.create();
errdefer tex.destroy();
const texbind = try 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,
formats.internal,
const tex = try Texture.init(
.{
.format = formats.format,
.internal_format = formats.internal,
.target = .Rectangle,
},
@intCast(p.width),
@intCast(p.height),
0,
formats.format,
.UnsignedByte,
p.data,
p.data[0 .. p.width * p.height * self.depth()],
);
// Uploaded. We can now clear our data and change our state.

View File

@@ -0,0 +1,310 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const math = @import("../../math.zig");
const Pipeline = @import("Pipeline.zig");
const log = std.log.scoped(.opengl);
/// This contains the state for the shaders used by the Metal renderer.
pub const Shaders = struct {
/// Renders cell foreground elements (text, decorations).
cell_text_pipeline: Pipeline,
/// The cell background shader is the shader used to render the
/// background of terminal cells.
cell_bg_pipeline: Pipeline,
/// The image shader is the shader used to render images for things
/// like the Kitty image protocol.
image_pipeline: Pipeline,
/// 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 Pipeline,
/// Set to true when deinited, if you try to deinit a defunct set
/// of shaders it will just be ignored, to prevent double-free.
defunct: bool = false,
/// 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,
post_shaders: []const [:0]const u8,
) !Shaders {
const cell_text_pipeline = try initCellTextPipeline();
errdefer cell_text_pipeline.deinit();
const cell_bg_pipeline = try initCellBgPipeline();
errdefer cell_bg_pipeline.deinit();
const image_pipeline = try initImagePipeline();
errdefer image_pipeline.deinit();
const post_pipelines: []const Pipeline = initPostPipelines(
alloc,
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.deinit();
alloc.free(post_pipelines);
};
return .{
.cell_text_pipeline = cell_text_pipeline,
.cell_bg_pipeline = cell_bg_pipeline,
.image_pipeline = image_pipeline,
.post_pipelines = post_pipelines,
};
}
pub fn deinit(self: *Shaders, alloc: Allocator) void {
if (self.defunct) return;
self.defunct = true;
// Release our primary shaders
self.cell_text_pipeline.deinit();
self.cell_bg_pipeline.deinit();
self.image_pipeline.deinit();
// Release our postprocess shaders
if (self.post_pipelines.len > 0) {
for (self.post_pipelines) |pipeline| {
pipeline.deinit();
}
alloc.free(self.post_pipelines);
}
}
};
/// Single parameter for the image shader. See shader for field details.
pub const Image = extern struct {
grid_pos: [2]f32 align(8),
cell_offset: [2]f32 align(8),
source_rect: [4]f32 align(16),
dest_size: [2]f32 align(8),
};
/// The uniforms that are passed to the terminal cell shader.
pub const Uniforms = extern struct {
/// The projection matrix for turning world coordinates to normalized.
/// This is calculated based on the size of the screen.
projection_matrix: math.Mat align(16),
/// Size of a single cell in pixels, unscaled.
cell_size: [2]f32 align(8),
/// Size of the grid in columns and rows.
grid_size: [2]u16 align(4),
/// The padding around the terminal grid in pixels. In order:
/// top, right, bottom, left.
grid_padding: [4]f32 align(16),
/// Bit mask defining which directions to
/// extend cell colors in to the padding.
/// Order, LSB first: left, right, up, down
padding_extend: PaddingExtend align(4),
/// The minimum contrast ratio for text. The contrast ratio is calculated
/// according to the WCAG 2.0 spec.
min_contrast: f32 align(4),
/// The cursor position and color.
cursor_pos: [2]u16 align(4),
cursor_color: [4]u8 align(4),
/// The background color for the whole surface.
bg_color: [4]u8 align(4),
/// Various booleans, in a packed struct for space efficiency.
bools: Bools align(4),
const Bools = packed struct(u32) {
/// Whether the cursor is 2 cells wide.
cursor_wide: bool,
/// Indicates that colors provided to the shader are already in
/// the P3 color space, so they don't need to be converted from
/// sRGB.
use_display_p3: bool,
/// Indicates that the color attachments for the shaders have
/// an `*_srgb` pixel format, which means the shaders need to
/// output linear RGB colors rather than gamma encoded colors,
/// since blending will be performed in linear space and then
/// Metal itself will re-encode the colors for storage.
use_linear_blending: bool,
/// Enables a weight correction step that makes text rendered
/// with linear alpha blending have a similar apparent weight
/// (thickness) to gamma-incorrect blending.
use_linear_correction: bool = false,
_padding: u28 = 0,
};
const PaddingExtend = packed struct(u32) {
left: bool = false,
right: bool = false,
up: bool = false,
down: bool = false,
_padding: u28 = 0,
};
};
/// 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 our custom shader pipelines. The shaders argument is a
/// set of shader source code, not file paths.
fn initPostPipelines(
alloc: Allocator,
shaders: []const [:0]const u8,
) ![]const Pipeline {
// 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(Pipeline, shaders.len);
errdefer {
for (pipelines[0..i]) |pipeline| {
pipeline.deinit();
}
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(source);
i += 1;
}
return pipelines;
}
/// Initialize a single custom shader pipeline from shader source.
fn initPostPipeline(data: [:0]const u8) !Pipeline {
return try Pipeline.init(null, .{
.vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
.fragment_fn = data,
});
}
/// This is a single parameter for the terminal cell shader.
pub const CellText = extern struct {
glyph_pos: [2]u32 align(8) = .{ 0, 0 },
glyph_size: [2]u32 align(8) = .{ 0, 0 },
bearings: [2]i16 align(4) = .{ 0, 0 },
grid_pos: [2]u16 align(4),
color: [4]u8 align(4),
mode: Mode align(4),
constraint_width: u32 align(4) = 0,
pub const Mode = enum(u32) {
fg = 1,
fg_constrained = 2,
fg_color = 3,
cursor = 4,
fg_powerline = 5,
};
// test {
// // Minimizing the size of this struct is important,
// // so we test it in order to be aware of any changes.
// try std.testing.expectEqual(32, @sizeOf(CellText));
// }
};
/// Initialize the cell render pipeline.
fn initCellTextPipeline() !Pipeline {
return try Pipeline.init(CellText, .{
.vertex_fn = loadShaderCode("../shaders/glsl/cell_text.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/cell_text.f.glsl"),
.step_fn = .per_instance,
});
}
/// This is a single parameter for the cell bg shader.
pub const CellBg = [4]u8;
/// Initialize the cell background render pipeline.
fn initCellBgPipeline() !Pipeline {
return try Pipeline.init(null, .{
.vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/cell_bg.f.glsl"),
});
}
/// Initialize the image render pipeline.
fn initImagePipeline() !Pipeline {
return try Pipeline.init(Image, .{
.vertex_fn = loadShaderCode("../shaders/glsl/image.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/image.f.glsl"),
.step_fn = .per_instance,
});
}
/// Load shader code from the target path, processing `#include` directives.
///
/// Comptime only for now, this code is really sloppy and makes a bunch of
/// assumptions about things being well formed and file names not containing
/// quote marks. If we ever want to process `#include`s for custom shaders
/// then we need to write something better than this for it.
fn loadShaderCode(comptime path: []const u8) [:0]const u8 {
return comptime processIncludes(@embedFile(path), std.fs.path.dirname(path).?);
}
/// Used by loadShaderCode
fn processIncludes(contents: [:0]const u8, basedir: []const u8) [:0]const u8 {
@setEvalBranchQuota(100_000);
var i: usize = 0;
while (i < contents.len) {
if (std.mem.startsWith(u8, contents[i..], "#include")) {
assert(std.mem.startsWith(u8, contents[i..], "#include \""));
const start = i + "#include \"".len;
const end = std.mem.indexOfScalarPos(u8, contents, start, '"').?;
return std.fmt.comptimePrint(
"{s}{s}{s}",
.{
contents[0..i],
@embedFile(basedir ++ "/" ++ contents[start..end]),
processIncludes(contents[end + 1 ..], basedir),
},
);
}
if (std.mem.indexOfPos(u8, contents, i, "\n#")) |j| {
i = (j + 1);
} else {
break;
}
}
return contents;
}

View File

@@ -1,53 +0,0 @@
#version 330 core
in vec2 glyph_tex_coords;
flat in uint mode;
// The color for this cell. If this is a background pass this is the
// background color. Otherwise, this is the foreground color.
flat in vec4 color;
// The position of the cells top-left corner.
flat in vec2 screen_cell_pos;
// Position the fragment coordinate to the upper left
layout(origin_upper_left) in vec4 gl_FragCoord;
// Must declare this output for some versions of OpenGL.
layout(location = 0) out vec4 out_FragColor;
// Font texture
uniform sampler2D text;
uniform sampler2D text_color;
// Dimensions of the cell
uniform vec2 cell_size;
// See vertex shader
const uint MODE_BG = 1u;
const uint MODE_FG = 2u;
const uint MODE_FG_CONSTRAINED = 3u;
const uint MODE_FG_COLOR = 7u;
const uint MODE_FG_POWERLINE = 15u;
void main() {
float a;
switch (mode) {
case MODE_BG:
out_FragColor = color;
break;
case MODE_FG:
case MODE_FG_CONSTRAINED:
case MODE_FG_POWERLINE:
a = texture(text, glyph_tex_coords).r;
vec3 premult = color.rgb * color.a;
out_FragColor = vec4(premult.rgb*a, a);
break;
case MODE_FG_COLOR:
out_FragColor = texture(text_color, glyph_tex_coords);
break;
}
}

View File

@@ -249,20 +249,12 @@ vertex CellBgVertexOut cell_bg_vertex(
fragment float4 cell_bg_fragment(
CellBgVertexOut in [[stage_in]],
constant uchar4 *cells [[buffer(0)]],
constant Uniforms& uniforms [[buffer(1)]]
constant Uniforms& uniforms [[buffer(1)]],
constant uchar4 *cells [[buffer(2)]]
) {
int2 grid_pos = int2(floor((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size));
float4 bg = float4(0.0);
// If we have any background transparency then we render bg-colored cells as
// fully transparent, since the background is handled by the layer bg color
// and we don't want to double up our bg color, but if our bg color is fully
// opaque then our layer is opaque and can't handle transparency, so we need
// to return the bg color directly instead.
if (uniforms.bg_color.a == 255) {
bg = in.bg_color;
}
float4 bg = in.bg_color;
// Clamp x position, extends edge bg colors in to padding on sides.
if (grid_pos.x < 0) {
@@ -374,19 +366,23 @@ vertex CellTextVertexOut cell_text_vertex(
// Convert the grid x, y into world space x, y by accounting for cell size
float2 cell_pos = uniforms.cell_size * float2(in.grid_pos);
// Turn the cell position into a vertex point depending on the
// vertex ID. Since we use instanced drawing, we have 4 vertices
// for each corner of the cell. We can use vertex ID to determine
// which one we're looking at. Using this, we can use 1 or 0 to keep
// or discard the value for the vertex.
// We use a triangle strip with 4 vertices to render quads,
// so we determine which corner of the cell this vertex is in
// based on the vertex ID.
//
// 0 = top-right
// 1 = bot-right
// 2 = bot-left
// 3 = top-left
// 0 --> 1
// | .'|
// | / |
// | L |
// 2 --> 3
//
// 0 = top-left (0, 0)
// 1 = top-right (1, 0)
// 2 = bot-left (0, 1)
// 3 = bot-right (1, 1)
float2 corner;
corner.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
corner.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
corner.x = float(vid == 1 || vid == 3);
corner.y = float(vid == 2 || vid == 3);
CellTextVertexOut out;
out.mode = in.mode;
@@ -502,7 +498,7 @@ fragment float4 cell_text_fragment(
CellTextVertexOut in [[stage_in]],
texture2d<float> textureGrayscale [[texture(0)]],
texture2d<float> textureColor [[texture(1)]],
constant Uniforms& uniforms [[buffer(2)]]
constant Uniforms& uniforms [[buffer(1)]]
) {
constexpr sampler textureSampler(
coord::pixel,
@@ -621,19 +617,23 @@ vertex ImageVertexOut image_vertex(
texture2d<uint> image [[texture(0)]],
constant Uniforms& uniforms [[buffer(1)]]
) {
// Turn the image position into a vertex point depending on the
// vertex ID. Since we use instanced drawing, we have 4 vertices
// for each corner of the cell. We can use vertex ID to determine
// which one we're looking at. Using this, we can use 1 or 0 to keep
// or discard the value for the vertex.
// We use a triangle strip with 4 vertices to render quads,
// so we determine which corner of the cell this vertex is in
// based on the vertex ID.
//
// 0 = top-right
// 1 = bot-right
// 2 = bot-left
// 3 = top-left
// 0 --> 1
// | .'|
// | / |
// | L |
// 2 --> 3
//
// 0 = top-left (0, 0)
// 1 = top-right (1, 0)
// 2 = bot-left (0, 1)
// 3 = bot-right (1, 1)
float2 corner;
corner.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
corner.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
corner.x = float(vid == 1 || vid == 3);
corner.y = float(vid == 2 || vid == 3);
// The texture coordinates start at our source x/y
// and add the width/height depending on the corner.

View File

@@ -1,258 +0,0 @@
#version 330 core
// These are the possible modes that "mode" can be set to. This is
// used to multiplex multiple render modes into a single shader.
//
// NOTE: this must be kept in sync with the fragment shader
const uint MODE_BG = 1u;
const uint MODE_FG = 2u;
const uint MODE_FG_CONSTRAINED = 3u;
const uint MODE_FG_COLOR = 7u;
const uint MODE_FG_POWERLINE = 15u;
// The grid coordinates (x, y) where x < columns and y < rows
layout (location = 0) in vec2 grid_coord;
// Position of the glyph in the texture.
layout (location = 1) in vec2 glyph_pos;
// Width/height of the glyph
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 color for this cell in RGBA (0 to 1.0). Background or foreground
// depends on mode.
layout (location = 4) in vec4 color_in;
// Only set for MODE_FG, this is the background color of the FG text.
// This is used to detect minimal contrast for the text.
layout (location = 5) in vec4 bg_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;
// The width in cells of this item.
layout (location = 7) in uint grid_width;
// The background or foreground color for the fragment, depending on
// whether this is a background or foreground pass.
flat out vec4 color;
// The x/y coordinate for the glyph representing the font.
out vec2 glyph_tex_coords;
// The position of the cell top-left corner in screen cords. z and w
// are width and height.
flat out vec2 screen_cell_pos;
// Pass the mode forward to the fragment shader.
flat out uint mode;
uniform sampler2D text;
uniform sampler2D text_color;
uniform vec2 cell_size;
uniform vec2 grid_size;
uniform vec4 grid_padding;
uniform bool padding_vertical_top;
uniform bool padding_vertical_bottom;
uniform mat4 projection;
uniform float min_contrast;
/********************************************************************
* Modes
*
*-------------------------------------------------------------------
* MODE_BG
*
* In MODE_BG, this shader renders only the background color for the
* cell. This is a simple mode where we generate a simple rectangle
* made up of 4 vertices and then it is filled. In this mode, the output
* "color" is the fill color for the bg.
*
*-------------------------------------------------------------------
* MODE_FG
*
* In MODE_FG, the shader renders the glyph onto this cell and utilizes
* the glyph texture "text". In this mode, the output "color" is the
* fg color to use for the glyph.
*
*/
//-------------------------------------------------------------------
// Color Functions
//-------------------------------------------------------------------
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
float luminance_component(float c) {
if (c <= 0.03928) {
return c / 12.92;
} else {
return pow((c + 0.055) / 1.055, 2.4);
}
}
float relative_luminance(vec3 color) {
vec3 color_adjusted = vec3(
luminance_component(color.r),
luminance_component(color.g),
luminance_component(color.b)
);
vec3 weights = vec3(0.2126, 0.7152, 0.0722);
return dot(color_adjusted, weights);
}
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
float contrast_ratio(vec3 color1, vec3 color2) {
float luminance1 = relative_luminance(color1) + 0.05;
float luminance2 = relative_luminance(color2) + 0.05;
return max(luminance1, luminance2) / min(luminance1, luminance2);
}
// Return the fg if the contrast ratio is greater than min, otherwise
// return a color that satisfies the contrast ratio. Currently, the color
// is always white or black, whichever has the highest contrast ratio.
vec4 contrasted_color(float min_ratio, vec4 fg, vec4 bg) {
vec3 fg_premult = fg.rgb * fg.a;
vec3 bg_premult = bg.rgb * bg.a;
float ratio = contrast_ratio(fg_premult, bg_premult);
if (ratio < min_ratio) {
float white_ratio = contrast_ratio(vec3(1.0, 1.0, 1.0), bg_premult);
float black_ratio = contrast_ratio(vec3(0.0, 0.0, 0.0), bg_premult);
if (white_ratio > black_ratio) {
return vec4(1.0, 1.0, 1.0, fg.a);
} else {
return vec4(0.0, 0.0, 0.0, fg.a);
}
}
return fg;
}
//-------------------------------------------------------------------
// Main
//-------------------------------------------------------------------
void main() {
// We always forward our mode unmasked because the fragment
// shader doesn't use any of the masks.
mode = mode_in;
// Top-left cell coordinates converted to world space
// Example: (1,0) with a 30 wide cell is converted to (30,0)
vec2 cell_pos = cell_size * grid_coord;
// Our Z value. For now we just use grid_z directly but we pull it
// out here so the variable name is more uniform to our cell_pos and
// in case we want to do any other math later.
float cell_z = 0.0;
// Turn the cell position into a vertex point depending on the
// gl_VertexID. Since we use instanced drawing, we have 4 vertices
// for each corner of the cell. We can use gl_VertexID to determine
// which one we're looking at. Using this, we can use 1 or 0 to keep
// or discard the value for the vertex.
//
// 0 = top-right
// 1 = bot-right
// 2 = bot-left
// 3 = top-left
vec2 position;
position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.;
position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.;
// Scaled for wide chars
vec2 cell_size_scaled = cell_size;
cell_size_scaled.x = cell_size_scaled.x * grid_width;
switch (mode) {
case MODE_BG:
// If we're at the edge of the grid, we add our padding to the background
// to extend it. Note: grid_padding is top/right/bottom/left.
if (grid_coord.y == 0 && padding_vertical_top) {
cell_pos.y -= grid_padding.r;
cell_size_scaled.y += grid_padding.r;
} else if (grid_coord.y == grid_size.y - 1 && padding_vertical_bottom) {
cell_size_scaled.y += grid_padding.b;
}
if (grid_coord.x == 0) {
cell_pos.x -= grid_padding.a;
cell_size_scaled.x += grid_padding.a;
} else if (grid_coord.x == grid_size.x - 1) {
cell_size_scaled.x += grid_padding.g;
}
// Calculate the final position of our cell in world space.
// We have to add our cell size since our vertices are offset
// one cell up and to the left. (Do the math to verify yourself)
cell_pos = cell_pos + cell_size_scaled * position;
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
color = color_in / 255.0;
break;
case MODE_FG:
case MODE_FG_CONSTRAINED:
case MODE_FG_COLOR:
case MODE_FG_POWERLINE:
vec2 glyph_offset_calc = glyph_offset;
// The glyph_offset.y is the y bearing, a y value that when added
// to the baseline is the offset (+y is up). Our grid goes down.
// So we flip it with `cell_size.y - glyph_offset.y`.
glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y;
// If this is a constrained mode, we need to constrain it!
vec2 glyph_size_calc = glyph_size;
if (mode == MODE_FG_CONSTRAINED) {
if (glyph_size.x > cell_size_scaled.x) {
float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x);
glyph_offset_calc.y = glyph_offset_calc.y + ((glyph_size.y - new_y) / 2);
glyph_size_calc.y = new_y;
glyph_size_calc.x = cell_size_scaled.x;
}
}
// Calculate the final position of the cell.
cell_pos = cell_pos + (glyph_size_calc * position) + glyph_offset_calc;
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
// We need to convert our texture position and size to normalized
// device coordinates (0 to 1.0) by dividing by the size of the texture.
ivec2 text_size;
switch(mode) {
case MODE_FG_CONSTRAINED:
case MODE_FG_POWERLINE:
case MODE_FG:
text_size = textureSize(text, 0);
break;
case MODE_FG_COLOR:
text_size = textureSize(text_color, 0);
break;
}
vec2 glyph_tex_pos = glyph_pos / text_size;
vec2 glyph_tex_size = glyph_size / text_size;
glyph_tex_coords = glyph_tex_pos + glyph_tex_size * position;
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
// We only apply this adjustment to "normal" text with MODE_FG,
// since we want color glyphs to appear in their original color
// and Powerline glyphs to be unaffected (else parts of the line would
// have different colors as some parts are displayed via background colors).
vec4 color_final = color_in / 255.0;
if (min_contrast > 1.0 && mode == MODE_FG) {
vec4 bg_color = bg_color_in / 255.0;
color_final = contrasted_color(min_contrast, color_final, bg_color);
}
color = color_final;
break;
}
}

View File

@@ -1,8 +0,0 @@
#version 330 core
void main(){
vec2 position;
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);
}

View File

@@ -0,0 +1,61 @@
#include "common.glsl"
// Position the origin to the upper left
layout(origin_upper_left, pixel_center_integer) in vec4 gl_FragCoord;
// Must declare this output for some versions of OpenGL.
layout(location = 0) out vec4 out_FragColor;
layout(binding = 1, std430) readonly buffer bg_cells {
uint cells[];
};
vec4 cell_bg() {
uvec2 grid_size = unpack2u16(grid_size_packed_2u16);
ivec2 grid_pos = ivec2(floor((gl_FragCoord.xy - grid_padding.wx) / cell_size));
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
vec4 bg = load_color(unpack4u8(bg_color_packed_4u8), use_linear_blending);
// Clamp x position, extends edge bg colors in to padding on sides.
if (grid_pos.x < 0) {
if ((padding_extend & EXTEND_LEFT) != 0) {
grid_pos.x = 0;
} else {
return bg;
}
} else if (grid_pos.x > grid_size.x - 1) {
if ((padding_extend & EXTEND_RIGHT) != 0) {
grid_pos.x = int(grid_size.x) - 1;
} else {
return bg;
}
}
// Clamp y position if we should extend, otherwise discard if out of bounds.
if (grid_pos.y < 0) {
if ((padding_extend & EXTEND_UP) != 0) {
grid_pos.y = 0;
} else {
return bg;
}
} else if (grid_pos.y > grid_size.y - 1) {
if ((padding_extend & EXTEND_DOWN) != 0) {
grid_pos.y = int(grid_size.y) - 1;
} else {
return bg;
}
}
// Load the color for the cell.
vec4 cell_color = load_color(
unpack4u8(cells[grid_pos.y * grid_size.x + grid_pos.x]),
use_linear_blending
);
return cell_color;
}
void main() {
out_FragColor = cell_bg();
}

View File

@@ -0,0 +1,109 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2DRect atlas_grayscale;
layout(binding = 1) uniform sampler2DRect atlas_color;
in CellTextVertexOut {
flat uint mode;
flat vec4 color;
flat vec4 bg_color;
vec2 tex_coord;
} in_data;
// These are the possible modes that "mode" can be set to. This is
// used to multiplex multiple render modes into a single shader.
//
// NOTE: this must be kept in sync with the fragment shader
const uint MODE_TEXT = 1u;
const uint MODE_TEXT_CONSTRAINED = 2u;
const uint MODE_TEXT_COLOR = 3u;
const uint MODE_TEXT_CURSOR = 4u;
const uint MODE_TEXT_POWERLINE = 5u;
// Must declare this output for some versions of OpenGL.
layout(location = 0) out vec4 out_FragColor;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
bool use_linear_correction = (bools & USE_LINEAR_CORRECTION) != 0;
switch (in_data.mode) {
default:
case MODE_TEXT_CURSOR:
case MODE_TEXT_CONSTRAINED:
case MODE_TEXT_POWERLINE:
case MODE_TEXT:
{
// Our input color is always linear.
vec4 color = in_data.color;
// If we're not doing linear blending, then we need to
// re-apply the gamma encoding to our color manually.
//
// Since the alpha is premultiplied, we need to divide
// it out before unlinearizing and re-multiply it after.
if (!use_linear_blending) {
color.rgb /= vec3(color.a);
color = unlinearize(color);
color.rgb *= vec3(color.a);
}
// Fetch our alpha mask for this pixel.
float a = texture(atlas_grayscale, in_data.tex_coord).r;
// Linear blending weight correction corrects the alpha value to
// produce blending results which match gamma-incorrect blending.
if (use_linear_correction) {
// Short explanation of how this works:
//
// We get the luminances of the foreground and background colors,
// and then unlinearize them and perform blending on them. This
// gives us our desired luminance, which we derive our new alpha
// value from by mapping the range [bg_l, fg_l] to [0, 1], since
// our final blend will be a linear interpolation from bg to fg.
//
// This yields virtually identical results for grayscale blending,
// and very similar but non-identical results for color blending.
vec4 bg = in_data.bg_color;
float fg_l = luminance(color.rgb);
float bg_l = luminance(bg.rgb);
// To avoid numbers going haywire, we don't apply correction
// when the bg and fg luminances are within 0.001 of each other.
if (abs(fg_l - bg_l) > 0.001) {
float blend_l = linearize(unlinearize(fg_l) * a + unlinearize(bg_l) * (1.0 - a));
a = clamp((blend_l - bg_l) / (fg_l - bg_l), 0.0, 1.0);
}
}
// Multiply our whole color by the alpha mask.
// Since we use premultiplied alpha, this is
// the correct way to apply the mask.
color *= a;
out_FragColor = color;
return;
}
case MODE_TEXT_COLOR:
{
// For now, we assume that color glyphs
// are already premultiplied sRGB colors.
vec4 color = texture(atlas_color, in_data.tex_coord);
// If we aren't doing linear blending, we can return this right away.
if (!use_linear_blending) {
out_FragColor = color;
return;
}
// Otherwise we need to linearize the color. Since the alpha is
// premultiplied, we need to divide it out before linearizing.
color.rgb /= vec3(color.a);
color = linearize(color);
color.rgb *= vec3(color.a);
out_FragColor = color;
return;
}
}
}

View File

@@ -0,0 +1,162 @@
#include "common.glsl"
// The position of the glyph in the texture (x, y)
layout(location = 0) in uvec2 glyph_pos;
// The size of the glyph in the texture (w, h)
layout(location = 1) in uvec2 glyph_size;
// The left and top bearings for the glyph (x, y)
layout(location = 2) in ivec2 bearings;
// The grid coordinates (x, y) where x < columns and y < rows
layout(location = 3) in uvec2 grid_pos;
// The color of the rendered text glyph.
layout(location = 4) in uvec4 color;
// The mode for this cell.
layout(location = 5) in uint mode;
// The width to constrain the glyph to, in cells, or 0 for no constraint.
layout(location = 6) in uint constraint_width;
// These are the possible modes that "mode" can be set to. This is
// used to multiplex multiple render modes into a single shader.
const uint MODE_TEXT = 1u;
const uint MODE_TEXT_CONSTRAINED = 2u;
const uint MODE_TEXT_COLOR = 3u;
const uint MODE_TEXT_CURSOR = 4u;
const uint MODE_TEXT_POWERLINE = 5u;
out CellTextVertexOut {
flat uint mode;
flat vec4 color;
flat vec4 bg_color;
vec2 tex_coord;
} out_data;
layout(binding = 1, std430) readonly buffer bg_cells {
uint bg_colors[];
};
void main() {
uvec2 grid_size = unpack2u16(grid_size_packed_2u16);
uvec2 cursor_pos = unpack2u16(cursor_pos_packed_2u16);
bool cursor_wide = (bools & CURSOR_WIDE) != 0;
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
// Convert the grid x, y into world space x, y by accounting for cell size
vec2 cell_pos = cell_size * vec2(grid_pos);
int vid = gl_VertexID;
// We use a triangle strip with 4 vertices to render quads,
// so we determine which corner of the cell this vertex is in
// based on the vertex ID.
//
// 0 --> 1
// | .'|
// | / |
// | L |
// 2 --> 3
//
// 0 = top-left (0, 0)
// 1 = top-right (1, 0)
// 2 = bot-left (0, 1)
// 3 = bot-right (1, 1)
vec2 corner;
corner.x = float(vid == 1 || vid == 3);
corner.y = float(vid == 2 || vid == 3);
out_data.mode = mode;
// === Grid Cell ===
// +X
// 0,0--...->
// |
// . offset.x = bearings.x
// +Y. .|.
// . | |
// | cell_pos -> +-------+ _.
// v ._| |_. _|- offset.y = cell_size.y - bearings.y
// | | .###. | |
// | | #...# | |
// glyph_size.y -+ | ##### | |
// | | #.... | +- bearings.y
// |_| .#### | |
// | |_|
// +-------+
// |_._|
// |
// glyph_size.x
//
// In order to get the top left of the glyph, we compute an offset based on
// the bearings. The Y bearing is the distance from the bottom of the cell
// to the top of the glyph, so we subtract it from the cell height to get
// the y offset. The X bearing is the distance from the left of the cell
// to the left of the glyph, so it works as the x offset directly.
vec2 size = vec2(glyph_size);
vec2 offset = vec2(bearings);
offset.y = cell_size.y - offset.y;
// If we're constrained then we need to scale the glyph.
if (mode == MODE_TEXT_CONSTRAINED) {
float max_width = cell_size.x * constraint_width;
// If this glyph is wider than the constraint width,
// fit it to the width and remove its horizontal offset.
if (size.x > max_width) {
float new_y = size.y * (max_width / size.x);
offset.y += (size.y - new_y) / 2.0;
offset.x = 0.0;
size.y = new_y;
size.x = max_width;
} else if (max_width - size.x > offset.x) {
// However, if it does fit in the constraint width, make
// sure the offset is small enough to not push it over the
// right edge of the constraint width.
offset.x = max_width - size.x;
}
}
// Calculate the final position of the cell which uses our glyph size
// and glyph offset to create the correct bounding box for the glyph.
cell_pos = cell_pos + size * corner + offset;
gl_Position = projection_matrix * vec4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
// Calculate the texture coordinate in pixels. This is NOT normalized
// (between 0.0 and 1.0), and does not need to be, since the texture will
// be sampled with pixel coordinate mode.
out_data.tex_coord = vec2(glyph_pos) + vec2(glyph_size) * corner;
// Get our color. We always fetch a linearized version to
// make it easier to handle minimum contrast calculations.
out_data.color = load_color(color, true);
// Get the BG color
out_data.bg_color = load_color(
unpack4u8(bg_colors[grid_pos.y * grid_size.x + grid_pos.x]),
true
);
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
// We only apply this adjustment to "normal" text with MODE_TEXT,
// since we want color glyphs to appear in their original color
// and Powerline glyphs to be unaffected (else parts of the line would
// have different colors as some parts are displayed via background colors).
if (min_contrast > 1.0f && mode == MODE_TEXT) {
// Ensure our minimum contrast
out_data.color = contrasted_color(min_contrast, out_data.color, out_data.bg_color);
}
// Check if current position is under cursor (including wide cursor)
bool is_cursor_pos = ((grid_pos.x == cursor_pos.x) || (cursor_wide && (grid_pos.x == (cursor_pos.x + 1)))) && (grid_pos.y == cursor_pos.y);
// If this cell is the cursor cell, then we need to change the color.
if (mode != MODE_TEXT_CURSOR && is_cursor_pos) {
out_data.color = load_color(unpack4u8(cursor_color_packed_4u8), use_linear_blending);
}
}

View File

@@ -0,0 +1,155 @@
#version 430 core
// These are common definitions to be shared across shaders, the first
// line of any shader that needs these should be `#include "common.glsl"`.
//
// Included in this file are:
// - The interface block for the global uniforms.
// - Functions for unpacking values.
// - Functions for working with colors.
//----------------------------------------------------------------------------//
// Global Uniforms
//----------------------------------------------------------------------------//
layout(binding = 1, std140) uniform Globals {
uniform mat4 projection_matrix;
uniform vec2 cell_size;
uniform uint grid_size_packed_2u16;
uniform vec4 grid_padding;
uniform uint padding_extend;
uniform float min_contrast;
uniform uint cursor_pos_packed_2u16;
uniform uint cursor_color_packed_4u8;
uniform uint bg_color_packed_4u8;
uniform uint bools;
};
// Bools
const uint CURSOR_WIDE = 1u;
const uint USE_DISPLAY_P3 = 2u;
const uint USE_LINEAR_BLENDING = 4u;
const uint USE_LINEAR_CORRECTION = 8u;
// Padding extend enum
const uint EXTEND_LEFT = 1u;
const uint EXTEND_RIGHT = 2u;
const uint EXTEND_UP = 4u;
const uint EXTEND_DOWN = 8u;
//----------------------------------------------------------------------------//
// Functions for Unpacking Values
//----------------------------------------------------------------------------//
// NOTE: These unpack functions assume little-endian.
// If this ever becomes a problem... oh dear!
uvec4 unpack4u8(uint packed_value) {
return uvec4(
uint(packed_value >> 0) & uint(0xFF),
uint(packed_value >> 8) & uint(0xFF),
uint(packed_value >> 16) & uint(0xFF),
uint(packed_value >> 24) & uint(0xFF)
);
}
uvec2 unpack2u16(uint packed_value) {
return uvec2(
uint(packed_value >> 0) & uint(0xFFFF),
uint(packed_value >> 16) & uint(0xFFFF)
);
}
ivec2 unpack2i16(int packed_value) {
return ivec2(
(packed_value << 16) >> 16,
(packed_value << 0) >> 16
);
}
//----------------------------------------------------------------------------//
// Color Functions
//----------------------------------------------------------------------------//
// Compute the luminance of the provided color.
//
// Takes colors in linear RGB space. If your colors are gamma
// encoded, linearize them before using them with this function.
float luminance(vec3 color) {
return dot(color, vec3(0.2126f, 0.7152f, 0.0722f));
}
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
//
// Takes colors in linear RGB space. If your colors are gamma
// encoded, linearize them before using them with this function.
float contrast_ratio(vec3 color1, vec3 color2) {
float luminance1 = luminance(color1) + 0.05;
float luminance2 = luminance(color2) + 0.05;
return max(luminance1, luminance2) / min(luminance1, luminance2);
}
// Return the fg if the contrast ratio is greater than min, otherwise
// return a color that satisfies the contrast ratio. Currently, the color
// is always white or black, whichever has the highest contrast ratio.
//
// Takes colors in linear RGB space. If your colors are gamma
// encoded, linearize them before using them with this function.
vec4 contrasted_color(float min_ratio, vec4 fg, vec4 bg) {
float ratio = contrast_ratio(fg.rgb, bg.rgb);
if (ratio < min_ratio) {
float white_ratio = contrast_ratio(vec3(1.0, 1.0, 1.0), bg.rgb);
float black_ratio = contrast_ratio(vec3(0.0, 0.0, 0.0), bg.rgb);
if (white_ratio > black_ratio) {
return vec4(1.0);
} else {
return vec4(0.0);
}
}
return fg;
}
// Converts a color from sRGB gamma encoding to linear.
vec4 linearize(vec4 srgb) {
bvec3 cutoff = lessThanEqual(srgb.rgb, vec3(0.04045));
vec3 higher = pow((srgb.rgb + vec3(0.055)) / vec3(1.055), vec3(2.4));
vec3 lower = srgb.rgb / vec3(12.92);
return vec4(mix(higher, lower, cutoff), srgb.a);
}
float linearize(float v) {
return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
}
// Converts a color from linear to sRGB gamma encoding.
vec4 unlinearize(vec4 linear) {
bvec3 cutoff = lessThanEqual(linear.rgb, vec3(0.0031308));
vec3 higher = pow(linear.rgb, vec3(1.0 / 2.4)) * vec3(1.055) - vec3(0.055);
vec3 lower = linear.rgb * vec3(12.92);
return vec4(mix(higher, lower, cutoff), linear.a);
}
float unlinearize(float v) {
return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055;
}
// Load a 4 byte RGBA non-premultiplied color and linearize
// and convert it as necessary depending on the provided info.
//
// `linear` controls whether the returned color is linear or gamma encoded.
vec4 load_color(
uvec4 in_color,
bool linear
) {
// 0 .. 255 -> 0.0 .. 1.0
vec4 color = vec4(in_color) / vec4(255.0f);
// Linearize if necessary.
if (linear) color = linearize(color);
// Premultiply our color by its alpha.
color.rgb *= color.a;
return color;
}
//----------------------------------------------------------------------------//

View File

@@ -0,0 +1,24 @@
#version 330 core
void main() {
vec4 position;
position.x = (gl_VertexID == 2) ? 3.0 : -1.0;
position.y = (gl_VertexID == 0) ? -3.0 : 1.0;
position.z = 1.0;
position.w = 1.0;
// Single triangle is clipped to viewport.
//
// X <- vid == 0: (-1, -3)
// |\
// | \
// | \
// |###\
// |#+# \ `+` is (0, 0). `#`s are viewport area.
// |### \
// X------X <- vid == 2: (3, 1)
// ^
// vid == 1: (-1, 1)
gl_Position = position;
}

View File

@@ -0,0 +1,21 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2DRect image;
in vec2 tex_coord;
layout(location = 0) out vec4 out_FragColor;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
vec4 rgba = texture(image, tex_coord);
if (!use_linear_blending) {
rgba = unlinearize(rgba);
}
rgba.rgb *= vec3(rgba.a);
out_FragColor = rgba;
}

View File

@@ -0,0 +1,46 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2DRect image;
layout(location = 0) in vec2 grid_pos;
layout(location = 1) in vec2 cell_offset;
layout(location = 2) in vec4 source_rect;
layout(location = 3) in vec2 dest_size;
out vec2 tex_coord;
void main() {
int vid = gl_VertexID;
// We use a triangle strip with 4 vertices to render quads,
// so we determine which corner of the cell this vertex is in
// based on the vertex ID.
//
// 0 --> 1
// | .'|
// | / |
// | L |
// 2 --> 3
//
// 0 = top-left (0, 0)
// 1 = top-right (1, 0)
// 2 = bot-left (0, 1)
// 3 = bot-right (1, 1)
vec2 corner;
corner.x = float(vid == 1 || vid == 3);
corner.y = float(vid == 2 || vid == 3);
// The texture coordinates start at our source x/y
// and add the width/height depending on the corner.
//
// We don't need to normalize because we use pixel addressing for our sampler.
tex_coord = source_rect.xy;
tex_coord += source_rect.zw * corner;
// The position of our image starts at the top-left of the grid cell and
// adds the source rect width/height components.
vec2 image_pos = (cell_size * grid_pos) + cell_offset;
image_pos += dest_size * corner;
gl_Position = projection_matrix * vec4(image_pos.xy, 1.0, 1.0);
}

View File

@@ -1,29 +0,0 @@
#version 330 core
in vec2 tex_coord;
layout(location = 0) out vec4 out_FragColor;
uniform sampler2D image;
// Converts a color from linear to sRGB gamma encoding.
vec4 unlinearize(vec4 linear) {
bvec3 cutoff = lessThan(linear.rgb, vec3(0.0031308));
vec3 higher = pow(linear.rgb, vec3(1.0/2.4)) * vec3(1.055) - vec3(0.055);
vec3 lower = linear.rgb * vec3(12.92);
return vec4(mix(higher, lower, cutoff), linear.a);
}
void main() {
vec4 color = texture(image, tex_coord);
// Our texture is stored with an sRGB internal format,
// which means that the values are linearized when we
// sample the texture, but for now we actually want to
// output the color with gamma compression, so we do
// that.
color = unlinearize(color);
out_FragColor = vec4(color.rgb * color.a, color.a);
}

View File

@@ -1,44 +0,0 @@
#version 330 core
layout (location = 0) in vec2 grid_pos;
layout (location = 1) in vec2 cell_offset;
layout (location = 2) in vec4 source_rect;
layout (location = 3) in vec2 dest_size;
out vec2 tex_coord;
uniform sampler2D image;
uniform vec2 cell_size;
uniform mat4 projection;
void main() {
// The size of the image in pixels
vec2 image_size = textureSize(image, 0);
// Turn the cell position into a vertex point depending on the
// gl_VertexID. Since we use instanced drawing, we have 4 vertices
// for each corner of the cell. We can use gl_VertexID to determine
// which one we're looking at. Using this, we can use 1 or 0 to keep
// or discard the value for the vertex.
//
// 0 = top-right
// 1 = bot-right
// 2 = bot-left
// 3 = top-left
vec2 position;
position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.;
position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.;
// The texture coordinates start at our source x/y, then add the width/height
// as enabled by our instance id, then normalize to [0, 1]
tex_coord = source_rect.xy;
tex_coord += source_rect.zw * position;
tex_coord /= image_size;
// The position of our image starts at the top-left of the grid cell and
// adds the source rect width/height components.
vec2 image_pos = (cell_size * grid_pos) + cell_offset;
image_pos += dest_size * position;
gl_Position = projection * vec4(image_pos.xy, 0, 1.0);
}

View File

@@ -1,24 +1,24 @@
#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 = 1, std140) 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 = 0) 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(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;

View File

@@ -205,18 +205,25 @@ 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);
const c = spvcross.c;
return try spvCross(alloc, spvcross.c.SPVC_BACKEND_MSL, spv, (struct {
fn setOptions(options: c.spvc_compiler_options) error{SpvcFailed}!void {
// We enable decoration binding, because we need this
// to properly locate the uniform block to index 1.
if (c.spvc_compiler_options_set_bool(
options,
c.SPVC_COMPILER_OPTION_MSL_ENABLE_DECORATION_BINDING,
c.SPVC_TRUE,
) != c.SPVC_SUCCESS) {
return error.SpvcFailed;
}
}
}).setOptions);
}
/// Convert SPIR-V binary to GLSL..
/// 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 GLSL_VERSION = 430;
const c = spvcross.c;
return try spvCross(alloc, c.SPVC_BACKEND_GLSL, spv, (struct {

View File

@@ -1364,6 +1364,13 @@ pub const ReadThread = struct {
// Always close our end of the pipe when we exit.
defer posix.close(quit);
// Right now, on Darwin, `std.Thread.setName` can only name the current
// thread, and we have no way to get the current thread from within it,
// so instead we use this code to name the thread instead.
if (builtin.os.tag.isDarwin()) {
internal_os.macos.pthread_setname_np(&"io-reader".*);
}
// Setup our crash metadata
crash.sentry.thread_state = .{
.type = .io,

View File

@@ -16,6 +16,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const builtin = @import("builtin");
const xev = @import("../global.zig").xev;
const crash = @import("../crash/main.zig");
const internal_os = @import("../os/main.zig");
const termio = @import("../termio.zig");
const renderer = @import("../renderer.zig");
const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue;
@@ -202,6 +203,13 @@ pub fn threadMain(self: *Thread, io: *termio.Termio) void {
fn threadMain_(self: *Thread, io: *termio.Termio) !void {
defer log.debug("IO thread exited", .{});
// Right now, on Darwin, `std.Thread.setName` can only name the current
// thread, and we have no way to get the current thread from within it,
// so instead we use this code to name the thread instead.
if (builtin.os.tag.isDarwin()) {
internal_os.macos.pthread_setname_np(&"io".*);
}
// Setup our crash metadata
crash.sentry.thread_state = .{
.type = .io,

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
#include <glad/gl.h>

304
vendor/glad/src/gl.c vendored
View File

@@ -90,6 +90,7 @@ static void glad_gl_load_GL_VERSION_1_1(GladGLContext *context, GLADuserptrloadf
context->DrawArrays = (PFNGLDRAWARRAYSPROC) load(userptr, "glDrawArrays");
context->DrawElements = (PFNGLDRAWELEMENTSPROC) load(userptr, "glDrawElements");
context->GenTextures = (PFNGLGENTEXTURESPROC) load(userptr, "glGenTextures");
context->GetPointerv = (PFNGLGETPOINTERVPROC) load(userptr, "glGetPointerv");
context->IsTexture = (PFNGLISTEXTUREPROC) load(userptr, "glIsTexture");
context->PolygonOffset = (PFNGLPOLYGONOFFSETPROC) load(userptr, "glPolygonOffset");
context->TexSubImage1D = (PFNGLTEXSUBIMAGE1DPROC) load(userptr, "glTexSubImage1D");
@@ -411,39 +412,229 @@ static void glad_gl_load_GL_VERSION_3_3(GladGLContext *context, GLADuserptrloadf
context->VertexAttribP4ui = (PFNGLVERTEXATTRIBP4UIPROC) load(userptr, "glVertexAttribP4ui");
context->VertexAttribP4uiv = (PFNGLVERTEXATTRIBP4UIVPROC) load(userptr, "glVertexAttribP4uiv");
}
static void glad_gl_load_GL_VERSION_4_0(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {
if(!context->VERSION_4_0) return;
context->BeginQueryIndexed = (PFNGLBEGINQUERYINDEXEDPROC) load(userptr, "glBeginQueryIndexed");
context->BindTransformFeedback = (PFNGLBINDTRANSFORMFEEDBACKPROC) load(userptr, "glBindTransformFeedback");
context->BlendEquationSeparatei = (PFNGLBLENDEQUATIONSEPARATEIPROC) load(userptr, "glBlendEquationSeparatei");
context->BlendEquationi = (PFNGLBLENDEQUATIONIPROC) load(userptr, "glBlendEquationi");
context->BlendFuncSeparatei = (PFNGLBLENDFUNCSEPARATEIPROC) load(userptr, "glBlendFuncSeparatei");
context->BlendFunci = (PFNGLBLENDFUNCIPROC) load(userptr, "glBlendFunci");
context->DeleteTransformFeedbacks = (PFNGLDELETETRANSFORMFEEDBACKSPROC) load(userptr, "glDeleteTransformFeedbacks");
context->DrawArraysIndirect = (PFNGLDRAWARRAYSINDIRECTPROC) load(userptr, "glDrawArraysIndirect");
context->DrawElementsIndirect = (PFNGLDRAWELEMENTSINDIRECTPROC) load(userptr, "glDrawElementsIndirect");
context->DrawTransformFeedback = (PFNGLDRAWTRANSFORMFEEDBACKPROC) load(userptr, "glDrawTransformFeedback");
context->DrawTransformFeedbackStream = (PFNGLDRAWTRANSFORMFEEDBACKSTREAMPROC) load(userptr, "glDrawTransformFeedbackStream");
context->EndQueryIndexed = (PFNGLENDQUERYINDEXEDPROC) load(userptr, "glEndQueryIndexed");
context->GenTransformFeedbacks = (PFNGLGENTRANSFORMFEEDBACKSPROC) load(userptr, "glGenTransformFeedbacks");
context->GetActiveSubroutineName = (PFNGLGETACTIVESUBROUTINENAMEPROC) load(userptr, "glGetActiveSubroutineName");
context->GetActiveSubroutineUniformName = (PFNGLGETACTIVESUBROUTINEUNIFORMNAMEPROC) load(userptr, "glGetActiveSubroutineUniformName");
context->GetActiveSubroutineUniformiv = (PFNGLGETACTIVESUBROUTINEUNIFORMIVPROC) load(userptr, "glGetActiveSubroutineUniformiv");
context->GetProgramStageiv = (PFNGLGETPROGRAMSTAGEIVPROC) load(userptr, "glGetProgramStageiv");
context->GetQueryIndexediv = (PFNGLGETQUERYINDEXEDIVPROC) load(userptr, "glGetQueryIndexediv");
context->GetSubroutineIndex = (PFNGLGETSUBROUTINEINDEXPROC) load(userptr, "glGetSubroutineIndex");
context->GetSubroutineUniformLocation = (PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC) load(userptr, "glGetSubroutineUniformLocation");
context->GetUniformSubroutineuiv = (PFNGLGETUNIFORMSUBROUTINEUIVPROC) load(userptr, "glGetUniformSubroutineuiv");
context->GetUniformdv = (PFNGLGETUNIFORMDVPROC) load(userptr, "glGetUniformdv");
context->IsTransformFeedback = (PFNGLISTRANSFORMFEEDBACKPROC) load(userptr, "glIsTransformFeedback");
context->MinSampleShading = (PFNGLMINSAMPLESHADINGPROC) load(userptr, "glMinSampleShading");
context->PatchParameterfv = (PFNGLPATCHPARAMETERFVPROC) load(userptr, "glPatchParameterfv");
context->PatchParameteri = (PFNGLPATCHPARAMETERIPROC) load(userptr, "glPatchParameteri");
context->PauseTransformFeedback = (PFNGLPAUSETRANSFORMFEEDBACKPROC) load(userptr, "glPauseTransformFeedback");
context->ResumeTransformFeedback = (PFNGLRESUMETRANSFORMFEEDBACKPROC) load(userptr, "glResumeTransformFeedback");
context->Uniform1d = (PFNGLUNIFORM1DPROC) load(userptr, "glUniform1d");
context->Uniform1dv = (PFNGLUNIFORM1DVPROC) load(userptr, "glUniform1dv");
context->Uniform2d = (PFNGLUNIFORM2DPROC) load(userptr, "glUniform2d");
context->Uniform2dv = (PFNGLUNIFORM2DVPROC) load(userptr, "glUniform2dv");
context->Uniform3d = (PFNGLUNIFORM3DPROC) load(userptr, "glUniform3d");
context->Uniform3dv = (PFNGLUNIFORM3DVPROC) load(userptr, "glUniform3dv");
context->Uniform4d = (PFNGLUNIFORM4DPROC) load(userptr, "glUniform4d");
context->Uniform4dv = (PFNGLUNIFORM4DVPROC) load(userptr, "glUniform4dv");
context->UniformMatrix2dv = (PFNGLUNIFORMMATRIX2DVPROC) load(userptr, "glUniformMatrix2dv");
context->UniformMatrix2x3dv = (PFNGLUNIFORMMATRIX2X3DVPROC) load(userptr, "glUniformMatrix2x3dv");
context->UniformMatrix2x4dv = (PFNGLUNIFORMMATRIX2X4DVPROC) load(userptr, "glUniformMatrix2x4dv");
context->UniformMatrix3dv = (PFNGLUNIFORMMATRIX3DVPROC) load(userptr, "glUniformMatrix3dv");
context->UniformMatrix3x2dv = (PFNGLUNIFORMMATRIX3X2DVPROC) load(userptr, "glUniformMatrix3x2dv");
context->UniformMatrix3x4dv = (PFNGLUNIFORMMATRIX3X4DVPROC) load(userptr, "glUniformMatrix3x4dv");
context->UniformMatrix4dv = (PFNGLUNIFORMMATRIX4DVPROC) load(userptr, "glUniformMatrix4dv");
context->UniformMatrix4x2dv = (PFNGLUNIFORMMATRIX4X2DVPROC) load(userptr, "glUniformMatrix4x2dv");
context->UniformMatrix4x3dv = (PFNGLUNIFORMMATRIX4X3DVPROC) load(userptr, "glUniformMatrix4x3dv");
context->UniformSubroutinesuiv = (PFNGLUNIFORMSUBROUTINESUIVPROC) load(userptr, "glUniformSubroutinesuiv");
}
static void glad_gl_load_GL_VERSION_4_1(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {
if(!context->VERSION_4_1) return;
context->ActiveShaderProgram = (PFNGLACTIVESHADERPROGRAMPROC) load(userptr, "glActiveShaderProgram");
context->BindProgramPipeline = (PFNGLBINDPROGRAMPIPELINEPROC) load(userptr, "glBindProgramPipeline");
context->ClearDepthf = (PFNGLCLEARDEPTHFPROC) load(userptr, "glClearDepthf");
context->CreateShaderProgramv = (PFNGLCREATESHADERPROGRAMVPROC) load(userptr, "glCreateShaderProgramv");
context->DeleteProgramPipelines = (PFNGLDELETEPROGRAMPIPELINESPROC) load(userptr, "glDeleteProgramPipelines");
context->DepthRangeArrayv = (PFNGLDEPTHRANGEARRAYVPROC) load(userptr, "glDepthRangeArrayv");
context->DepthRangeIndexed = (PFNGLDEPTHRANGEINDEXEDPROC) load(userptr, "glDepthRangeIndexed");
context->DepthRangef = (PFNGLDEPTHRANGEFPROC) load(userptr, "glDepthRangef");
context->GenProgramPipelines = (PFNGLGENPROGRAMPIPELINESPROC) load(userptr, "glGenProgramPipelines");
context->GetDoublei_v = (PFNGLGETDOUBLEI_VPROC) load(userptr, "glGetDoublei_v");
context->GetFloati_v = (PFNGLGETFLOATI_VPROC) load(userptr, "glGetFloati_v");
context->GetProgramBinary = (PFNGLGETPROGRAMBINARYPROC) load(userptr, "glGetProgramBinary");
context->GetProgramPipelineInfoLog = (PFNGLGETPROGRAMPIPELINEINFOLOGPROC) load(userptr, "glGetProgramPipelineInfoLog");
context->GetProgramPipelineiv = (PFNGLGETPROGRAMPIPELINEIVPROC) load(userptr, "glGetProgramPipelineiv");
context->GetShaderPrecisionFormat = (PFNGLGETSHADERPRECISIONFORMATPROC) load(userptr, "glGetShaderPrecisionFormat");
context->GetVertexAttribLdv = (PFNGLGETVERTEXATTRIBLDVPROC) load(userptr, "glGetVertexAttribLdv");
context->IsProgramPipeline = (PFNGLISPROGRAMPIPELINEPROC) load(userptr, "glIsProgramPipeline");
context->ProgramBinary = (PFNGLPROGRAMBINARYPROC) load(userptr, "glProgramBinary");
context->ProgramParameteri = (PFNGLPROGRAMPARAMETERIPROC) load(userptr, "glProgramParameteri");
context->ProgramUniform1d = (PFNGLPROGRAMUNIFORM1DPROC) load(userptr, "glProgramUniform1d");
context->ProgramUniform1dv = (PFNGLPROGRAMUNIFORM1DVPROC) load(userptr, "glProgramUniform1dv");
context->ProgramUniform1f = (PFNGLPROGRAMUNIFORM1FPROC) load(userptr, "glProgramUniform1f");
context->ProgramUniform1fv = (PFNGLPROGRAMUNIFORM1FVPROC) load(userptr, "glProgramUniform1fv");
context->ProgramUniform1i = (PFNGLPROGRAMUNIFORM1IPROC) load(userptr, "glProgramUniform1i");
context->ProgramUniform1iv = (PFNGLPROGRAMUNIFORM1IVPROC) load(userptr, "glProgramUniform1iv");
context->ProgramUniform1ui = (PFNGLPROGRAMUNIFORM1UIPROC) load(userptr, "glProgramUniform1ui");
context->ProgramUniform1uiv = (PFNGLPROGRAMUNIFORM1UIVPROC) load(userptr, "glProgramUniform1uiv");
context->ProgramUniform2d = (PFNGLPROGRAMUNIFORM2DPROC) load(userptr, "glProgramUniform2d");
context->ProgramUniform2dv = (PFNGLPROGRAMUNIFORM2DVPROC) load(userptr, "glProgramUniform2dv");
context->ProgramUniform2f = (PFNGLPROGRAMUNIFORM2FPROC) load(userptr, "glProgramUniform2f");
context->ProgramUniform2fv = (PFNGLPROGRAMUNIFORM2FVPROC) load(userptr, "glProgramUniform2fv");
context->ProgramUniform2i = (PFNGLPROGRAMUNIFORM2IPROC) load(userptr, "glProgramUniform2i");
context->ProgramUniform2iv = (PFNGLPROGRAMUNIFORM2IVPROC) load(userptr, "glProgramUniform2iv");
context->ProgramUniform2ui = (PFNGLPROGRAMUNIFORM2UIPROC) load(userptr, "glProgramUniform2ui");
context->ProgramUniform2uiv = (PFNGLPROGRAMUNIFORM2UIVPROC) load(userptr, "glProgramUniform2uiv");
context->ProgramUniform3d = (PFNGLPROGRAMUNIFORM3DPROC) load(userptr, "glProgramUniform3d");
context->ProgramUniform3dv = (PFNGLPROGRAMUNIFORM3DVPROC) load(userptr, "glProgramUniform3dv");
context->ProgramUniform3f = (PFNGLPROGRAMUNIFORM3FPROC) load(userptr, "glProgramUniform3f");
context->ProgramUniform3fv = (PFNGLPROGRAMUNIFORM3FVPROC) load(userptr, "glProgramUniform3fv");
context->ProgramUniform3i = (PFNGLPROGRAMUNIFORM3IPROC) load(userptr, "glProgramUniform3i");
context->ProgramUniform3iv = (PFNGLPROGRAMUNIFORM3IVPROC) load(userptr, "glProgramUniform3iv");
context->ProgramUniform3ui = (PFNGLPROGRAMUNIFORM3UIPROC) load(userptr, "glProgramUniform3ui");
context->ProgramUniform3uiv = (PFNGLPROGRAMUNIFORM3UIVPROC) load(userptr, "glProgramUniform3uiv");
context->ProgramUniform4d = (PFNGLPROGRAMUNIFORM4DPROC) load(userptr, "glProgramUniform4d");
context->ProgramUniform4dv = (PFNGLPROGRAMUNIFORM4DVPROC) load(userptr, "glProgramUniform4dv");
context->ProgramUniform4f = (PFNGLPROGRAMUNIFORM4FPROC) load(userptr, "glProgramUniform4f");
context->ProgramUniform4fv = (PFNGLPROGRAMUNIFORM4FVPROC) load(userptr, "glProgramUniform4fv");
context->ProgramUniform4i = (PFNGLPROGRAMUNIFORM4IPROC) load(userptr, "glProgramUniform4i");
context->ProgramUniform4iv = (PFNGLPROGRAMUNIFORM4IVPROC) load(userptr, "glProgramUniform4iv");
context->ProgramUniform4ui = (PFNGLPROGRAMUNIFORM4UIPROC) load(userptr, "glProgramUniform4ui");
context->ProgramUniform4uiv = (PFNGLPROGRAMUNIFORM4UIVPROC) load(userptr, "glProgramUniform4uiv");
context->ProgramUniformMatrix2dv = (PFNGLPROGRAMUNIFORMMATRIX2DVPROC) load(userptr, "glProgramUniformMatrix2dv");
context->ProgramUniformMatrix2fv = (PFNGLPROGRAMUNIFORMMATRIX2FVPROC) load(userptr, "glProgramUniformMatrix2fv");
context->ProgramUniformMatrix2x3dv = (PFNGLPROGRAMUNIFORMMATRIX2X3DVPROC) load(userptr, "glProgramUniformMatrix2x3dv");
context->ProgramUniformMatrix2x3fv = (PFNGLPROGRAMUNIFORMMATRIX2X3FVPROC) load(userptr, "glProgramUniformMatrix2x3fv");
context->ProgramUniformMatrix2x4dv = (PFNGLPROGRAMUNIFORMMATRIX2X4DVPROC) load(userptr, "glProgramUniformMatrix2x4dv");
context->ProgramUniformMatrix2x4fv = (PFNGLPROGRAMUNIFORMMATRIX2X4FVPROC) load(userptr, "glProgramUniformMatrix2x4fv");
context->ProgramUniformMatrix3dv = (PFNGLPROGRAMUNIFORMMATRIX3DVPROC) load(userptr, "glProgramUniformMatrix3dv");
context->ProgramUniformMatrix3fv = (PFNGLPROGRAMUNIFORMMATRIX3FVPROC) load(userptr, "glProgramUniformMatrix3fv");
context->ProgramUniformMatrix3x2dv = (PFNGLPROGRAMUNIFORMMATRIX3X2DVPROC) load(userptr, "glProgramUniformMatrix3x2dv");
context->ProgramUniformMatrix3x2fv = (PFNGLPROGRAMUNIFORMMATRIX3X2FVPROC) load(userptr, "glProgramUniformMatrix3x2fv");
context->ProgramUniformMatrix3x4dv = (PFNGLPROGRAMUNIFORMMATRIX3X4DVPROC) load(userptr, "glProgramUniformMatrix3x4dv");
context->ProgramUniformMatrix3x4fv = (PFNGLPROGRAMUNIFORMMATRIX3X4FVPROC) load(userptr, "glProgramUniformMatrix3x4fv");
context->ProgramUniformMatrix4dv = (PFNGLPROGRAMUNIFORMMATRIX4DVPROC) load(userptr, "glProgramUniformMatrix4dv");
context->ProgramUniformMatrix4fv = (PFNGLPROGRAMUNIFORMMATRIX4FVPROC) load(userptr, "glProgramUniformMatrix4fv");
context->ProgramUniformMatrix4x2dv = (PFNGLPROGRAMUNIFORMMATRIX4X2DVPROC) load(userptr, "glProgramUniformMatrix4x2dv");
context->ProgramUniformMatrix4x2fv = (PFNGLPROGRAMUNIFORMMATRIX4X2FVPROC) load(userptr, "glProgramUniformMatrix4x2fv");
context->ProgramUniformMatrix4x3dv = (PFNGLPROGRAMUNIFORMMATRIX4X3DVPROC) load(userptr, "glProgramUniformMatrix4x3dv");
context->ProgramUniformMatrix4x3fv = (PFNGLPROGRAMUNIFORMMATRIX4X3FVPROC) load(userptr, "glProgramUniformMatrix4x3fv");
context->ReleaseShaderCompiler = (PFNGLRELEASESHADERCOMPILERPROC) load(userptr, "glReleaseShaderCompiler");
context->ScissorArrayv = (PFNGLSCISSORARRAYVPROC) load(userptr, "glScissorArrayv");
context->ScissorIndexed = (PFNGLSCISSORINDEXEDPROC) load(userptr, "glScissorIndexed");
context->ScissorIndexedv = (PFNGLSCISSORINDEXEDVPROC) load(userptr, "glScissorIndexedv");
context->ShaderBinary = (PFNGLSHADERBINARYPROC) load(userptr, "glShaderBinary");
context->UseProgramStages = (PFNGLUSEPROGRAMSTAGESPROC) load(userptr, "glUseProgramStages");
context->ValidateProgramPipeline = (PFNGLVALIDATEPROGRAMPIPELINEPROC) load(userptr, "glValidateProgramPipeline");
context->VertexAttribL1d = (PFNGLVERTEXATTRIBL1DPROC) load(userptr, "glVertexAttribL1d");
context->VertexAttribL1dv = (PFNGLVERTEXATTRIBL1DVPROC) load(userptr, "glVertexAttribL1dv");
context->VertexAttribL2d = (PFNGLVERTEXATTRIBL2DPROC) load(userptr, "glVertexAttribL2d");
context->VertexAttribL2dv = (PFNGLVERTEXATTRIBL2DVPROC) load(userptr, "glVertexAttribL2dv");
context->VertexAttribL3d = (PFNGLVERTEXATTRIBL3DPROC) load(userptr, "glVertexAttribL3d");
context->VertexAttribL3dv = (PFNGLVERTEXATTRIBL3DVPROC) load(userptr, "glVertexAttribL3dv");
context->VertexAttribL4d = (PFNGLVERTEXATTRIBL4DPROC) load(userptr, "glVertexAttribL4d");
context->VertexAttribL4dv = (PFNGLVERTEXATTRIBL4DVPROC) load(userptr, "glVertexAttribL4dv");
context->VertexAttribLPointer = (PFNGLVERTEXATTRIBLPOINTERPROC) load(userptr, "glVertexAttribLPointer");
context->ViewportArrayv = (PFNGLVIEWPORTARRAYVPROC) load(userptr, "glViewportArrayv");
context->ViewportIndexedf = (PFNGLVIEWPORTINDEXEDFPROC) load(userptr, "glViewportIndexedf");
context->ViewportIndexedfv = (PFNGLVIEWPORTINDEXEDFVPROC) load(userptr, "glViewportIndexedfv");
}
static void glad_gl_load_GL_VERSION_4_2(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {
if(!context->VERSION_4_2) return;
context->BindImageTexture = (PFNGLBINDIMAGETEXTUREPROC) load(userptr, "glBindImageTexture");
context->DrawArraysInstancedBaseInstance = (PFNGLDRAWARRAYSINSTANCEDBASEINSTANCEPROC) load(userptr, "glDrawArraysInstancedBaseInstance");
context->DrawElementsInstancedBaseInstance = (PFNGLDRAWELEMENTSINSTANCEDBASEINSTANCEPROC) load(userptr, "glDrawElementsInstancedBaseInstance");
context->DrawElementsInstancedBaseVertexBaseInstance = (PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXBASEINSTANCEPROC) load(userptr, "glDrawElementsInstancedBaseVertexBaseInstance");
context->DrawTransformFeedbackInstanced = (PFNGLDRAWTRANSFORMFEEDBACKINSTANCEDPROC) load(userptr, "glDrawTransformFeedbackInstanced");
context->DrawTransformFeedbackStreamInstanced = (PFNGLDRAWTRANSFORMFEEDBACKSTREAMINSTANCEDPROC) load(userptr, "glDrawTransformFeedbackStreamInstanced");
context->GetActiveAtomicCounterBufferiv = (PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC) load(userptr, "glGetActiveAtomicCounterBufferiv");
context->GetInternalformativ = (PFNGLGETINTERNALFORMATIVPROC) load(userptr, "glGetInternalformativ");
context->MemoryBarrier = (PFNGLMEMORYBARRIERPROC) load(userptr, "glMemoryBarrier");
context->TexStorage1D = (PFNGLTEXSTORAGE1DPROC) load(userptr, "glTexStorage1D");
context->TexStorage2D = (PFNGLTEXSTORAGE2DPROC) load(userptr, "glTexStorage2D");
context->TexStorage3D = (PFNGLTEXSTORAGE3DPROC) load(userptr, "glTexStorage3D");
}
static void glad_gl_load_GL_VERSION_4_3(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {
if(!context->VERSION_4_3) return;
context->BindVertexBuffer = (PFNGLBINDVERTEXBUFFERPROC) load(userptr, "glBindVertexBuffer");
context->ClearBufferData = (PFNGLCLEARBUFFERDATAPROC) load(userptr, "glClearBufferData");
context->ClearBufferSubData = (PFNGLCLEARBUFFERSUBDATAPROC) load(userptr, "glClearBufferSubData");
context->CopyImageSubData = (PFNGLCOPYIMAGESUBDATAPROC) load(userptr, "glCopyImageSubData");
context->DebugMessageCallback = (PFNGLDEBUGMESSAGECALLBACKPROC) load(userptr, "glDebugMessageCallback");
context->DebugMessageControl = (PFNGLDEBUGMESSAGECONTROLPROC) load(userptr, "glDebugMessageControl");
context->DebugMessageInsert = (PFNGLDEBUGMESSAGEINSERTPROC) load(userptr, "glDebugMessageInsert");
context->DispatchCompute = (PFNGLDISPATCHCOMPUTEPROC) load(userptr, "glDispatchCompute");
context->DispatchComputeIndirect = (PFNGLDISPATCHCOMPUTEINDIRECTPROC) load(userptr, "glDispatchComputeIndirect");
context->FramebufferParameteri = (PFNGLFRAMEBUFFERPARAMETERIPROC) load(userptr, "glFramebufferParameteri");
context->GetDebugMessageLog = (PFNGLGETDEBUGMESSAGELOGPROC) load(userptr, "glGetDebugMessageLog");
context->GetFramebufferParameteriv = (PFNGLGETFRAMEBUFFERPARAMETERIVPROC) load(userptr, "glGetFramebufferParameteriv");
context->GetInternalformati64v = (PFNGLGETINTERNALFORMATI64VPROC) load(userptr, "glGetInternalformati64v");
context->GetObjectLabel = (PFNGLGETOBJECTLABELPROC) load(userptr, "glGetObjectLabel");
context->GetObjectPtrLabel = (PFNGLGETOBJECTPTRLABELPROC) load(userptr, "glGetObjectPtrLabel");
context->GetPointerv = (PFNGLGETPOINTERVPROC) load(userptr, "glGetPointerv");
context->GetProgramInterfaceiv = (PFNGLGETPROGRAMINTERFACEIVPROC) load(userptr, "glGetProgramInterfaceiv");
context->GetProgramResourceIndex = (PFNGLGETPROGRAMRESOURCEINDEXPROC) load(userptr, "glGetProgramResourceIndex");
context->GetProgramResourceLocation = (PFNGLGETPROGRAMRESOURCELOCATIONPROC) load(userptr, "glGetProgramResourceLocation");
context->GetProgramResourceLocationIndex = (PFNGLGETPROGRAMRESOURCELOCATIONINDEXPROC) load(userptr, "glGetProgramResourceLocationIndex");
context->GetProgramResourceName = (PFNGLGETPROGRAMRESOURCENAMEPROC) load(userptr, "glGetProgramResourceName");
context->GetProgramResourceiv = (PFNGLGETPROGRAMRESOURCEIVPROC) load(userptr, "glGetProgramResourceiv");
context->InvalidateBufferData = (PFNGLINVALIDATEBUFFERDATAPROC) load(userptr, "glInvalidateBufferData");
context->InvalidateBufferSubData = (PFNGLINVALIDATEBUFFERSUBDATAPROC) load(userptr, "glInvalidateBufferSubData");
context->InvalidateFramebuffer = (PFNGLINVALIDATEFRAMEBUFFERPROC) load(userptr, "glInvalidateFramebuffer");
context->InvalidateSubFramebuffer = (PFNGLINVALIDATESUBFRAMEBUFFERPROC) load(userptr, "glInvalidateSubFramebuffer");
context->InvalidateTexImage = (PFNGLINVALIDATETEXIMAGEPROC) load(userptr, "glInvalidateTexImage");
context->InvalidateTexSubImage = (PFNGLINVALIDATETEXSUBIMAGEPROC) load(userptr, "glInvalidateTexSubImage");
context->MultiDrawArraysIndirect = (PFNGLMULTIDRAWARRAYSINDIRECTPROC) load(userptr, "glMultiDrawArraysIndirect");
context->MultiDrawElementsIndirect = (PFNGLMULTIDRAWELEMENTSINDIRECTPROC) load(userptr, "glMultiDrawElementsIndirect");
context->ObjectLabel = (PFNGLOBJECTLABELPROC) load(userptr, "glObjectLabel");
context->ObjectPtrLabel = (PFNGLOBJECTPTRLABELPROC) load(userptr, "glObjectPtrLabel");
context->PopDebugGroup = (PFNGLPOPDEBUGGROUPPROC) load(userptr, "glPopDebugGroup");
context->PushDebugGroup = (PFNGLPUSHDEBUGGROUPPROC) load(userptr, "glPushDebugGroup");
context->ShaderStorageBlockBinding = (PFNGLSHADERSTORAGEBLOCKBINDINGPROC) load(userptr, "glShaderStorageBlockBinding");
context->TexBufferRange = (PFNGLTEXBUFFERRANGEPROC) load(userptr, "glTexBufferRange");
context->TexStorage2DMultisample = (PFNGLTEXSTORAGE2DMULTISAMPLEPROC) load(userptr, "glTexStorage2DMultisample");
context->TexStorage3DMultisample = (PFNGLTEXSTORAGE3DMULTISAMPLEPROC) load(userptr, "glTexStorage3DMultisample");
context->TextureView = (PFNGLTEXTUREVIEWPROC) load(userptr, "glTextureView");
context->VertexAttribBinding = (PFNGLVERTEXATTRIBBINDINGPROC) load(userptr, "glVertexAttribBinding");
context->VertexAttribFormat = (PFNGLVERTEXATTRIBFORMATPROC) load(userptr, "glVertexAttribFormat");
context->VertexAttribIFormat = (PFNGLVERTEXATTRIBIFORMATPROC) load(userptr, "glVertexAttribIFormat");
context->VertexAttribLFormat = (PFNGLVERTEXATTRIBLFORMATPROC) load(userptr, "glVertexAttribLFormat");
context->VertexBindingDivisor = (PFNGLVERTEXBINDINGDIVISORPROC) load(userptr, "glVertexBindingDivisor");
}
#if defined(GL_ES_VERSION_3_0) || defined(GL_VERSION_3_0)
#define GLAD_GL_IS_SOME_NEW_VERSION 1
#else
#define GLAD_GL_IS_SOME_NEW_VERSION 0
#endif
static int glad_gl_get_extensions(GladGLContext *context, int version, const char **out_exts, unsigned int *out_num_exts_i, char ***out_exts_i) {
#if GLAD_GL_IS_SOME_NEW_VERSION
if(GLAD_VERSION_MAJOR(version) < 3) {
#else
GLAD_UNUSED(version);
GLAD_UNUSED(out_num_exts_i);
GLAD_UNUSED(out_exts_i);
#endif
if (context->GetString == NULL) {
return 0;
static void glad_gl_free_extensions(char **exts_i) {
if (exts_i != NULL) {
unsigned int index;
for(index = 0; exts_i[index]; index++) {
free((void *) (exts_i[index]));
}
*out_exts = (const char *)context->GetString(GL_EXTENSIONS);
#if GLAD_GL_IS_SOME_NEW_VERSION
} else {
free((void *)exts_i);
exts_i = NULL;
}
}
static int glad_gl_get_extensions(GladGLContext *context, const char **out_exts, char ***out_exts_i) {
#if defined(GL_ES_VERSION_3_0) || defined(GL_VERSION_3_0)
if (context->GetStringi != NULL && context->GetIntegerv != NULL) {
unsigned int index = 0;
unsigned int num_exts_i = 0;
char **exts_i = NULL;
if (context->GetStringi == NULL || context->GetIntegerv == NULL) {
return 0;
}
context->GetIntegerv(GL_NUM_EXTENSIONS, (int*) &num_exts_i);
if (num_exts_i > 0) {
exts_i = (char **) malloc(num_exts_i * (sizeof *exts_i));
}
exts_i = (char **) malloc((num_exts_i + 1) * (sizeof *exts_i));
if (exts_i == NULL) {
return 0;
}
@@ -452,31 +643,40 @@ static int glad_gl_get_extensions(GladGLContext *context, int version, const cha
size_t len = strlen(gl_str_tmp) + 1;
char *local_str = (char*) malloc(len * sizeof(char));
if(local_str != NULL) {
memcpy(local_str, gl_str_tmp, len * sizeof(char));
if(local_str == NULL) {
exts_i[index] = NULL;
glad_gl_free_extensions(exts_i);
return 0;
}
memcpy(local_str, gl_str_tmp, len * sizeof(char));
exts_i[index] = local_str;
}
exts_i[index] = NULL;
*out_num_exts_i = num_exts_i;
*out_exts_i = exts_i;
return 1;
}
#else
GLAD_UNUSED(out_exts_i);
#endif
if (context->GetString == NULL) {
return 0;
}
*out_exts = (const char *)context->GetString(GL_EXTENSIONS);
return 1;
}
static void glad_gl_free_extensions(char **exts_i, unsigned int num_exts_i) {
if (exts_i != NULL) {
static int glad_gl_has_extension(const char *exts, char **exts_i, const char *ext) {
if(exts_i) {
unsigned int index;
for(index = 0; index < num_exts_i; index++) {
free((void *) (exts_i[index]));
for(index = 0; exts_i[index]; index++) {
const char *e = exts_i[index];
if(strcmp(e, ext) == 0) {
return 1;
}
}
free((void *)exts_i);
exts_i = NULL;
}
}
static int glad_gl_has_extension(int version, const char *exts, unsigned int num_exts_i, char **exts_i, const char *ext) {
if(GLAD_VERSION_MAJOR(version) < 3 || !GLAD_GL_IS_SOME_NEW_VERSION) {
} else {
const char *extensions;
const char *loc;
const char *terminator;
@@ -496,14 +696,6 @@ static int glad_gl_has_extension(int version, const char *exts, unsigned int num
}
extensions = terminator;
}
} else {
unsigned int index;
for(index = 0; index < num_exts_i; index++) {
const char *e = exts_i[index];
if(strcmp(e, ext) == 0) {
return 1;
}
}
}
return 0;
}
@@ -512,15 +704,14 @@ static GLADapiproc glad_gl_get_proc_from_userptr(void *userptr, const char* name
return (GLAD_GNUC_EXTENSION (GLADapiproc (*)(const char *name)) userptr)(name);
}
static int glad_gl_find_extensions_gl(GladGLContext *context, int version) {
static int glad_gl_find_extensions_gl(GladGLContext *context) {
const char *exts = NULL;
unsigned int num_exts_i = 0;
char **exts_i = NULL;
if (!glad_gl_get_extensions(context, version, &exts, &num_exts_i, &exts_i)) return 0;
if (!glad_gl_get_extensions(context, &exts, &exts_i)) return 0;
GLAD_UNUSED(glad_gl_has_extension);
GLAD_UNUSED(&glad_gl_has_extension);
glad_gl_free_extensions(exts_i, num_exts_i);
glad_gl_free_extensions(exts_i);
return 1;
}
@@ -561,6 +752,10 @@ static int glad_gl_find_core_gl(GladGLContext *context) {
context->VERSION_3_1 = (major == 3 && minor >= 1) || major > 3;
context->VERSION_3_2 = (major == 3 && minor >= 2) || major > 3;
context->VERSION_3_3 = (major == 3 && minor >= 3) || major > 3;
context->VERSION_4_0 = (major == 4 && minor >= 0) || major > 4;
context->VERSION_4_1 = (major == 4 && minor >= 1) || major > 4;
context->VERSION_4_2 = (major == 4 && minor >= 2) || major > 4;
context->VERSION_4_3 = (major == 4 && minor >= 3) || major > 4;
return GLAD_MAKE_VERSION(major, minor);
}
@@ -570,7 +765,6 @@ int gladLoadGLContextUserPtr(GladGLContext *context, GLADuserptrloadfunc load, v
context->GetString = (PFNGLGETSTRINGPROC) load(userptr, "glGetString");
if(context->GetString == NULL) return 0;
if(context->GetString(GL_VERSION) == NULL) return 0;
version = glad_gl_find_core_gl(context);
glad_gl_load_GL_VERSION_1_0(context, load, userptr);
@@ -585,8 +779,12 @@ int gladLoadGLContextUserPtr(GladGLContext *context, GLADuserptrloadfunc load, v
glad_gl_load_GL_VERSION_3_1(context, load, userptr);
glad_gl_load_GL_VERSION_3_2(context, load, userptr);
glad_gl_load_GL_VERSION_3_3(context, load, userptr);
glad_gl_load_GL_VERSION_4_0(context, load, userptr);
glad_gl_load_GL_VERSION_4_1(context, load, userptr);
glad_gl_load_GL_VERSION_4_2(context, load, userptr);
glad_gl_load_GL_VERSION_4_3(context, load, userptr);
if (!glad_gl_find_extensions_gl(context, version)) return 0;
if (!glad_gl_find_extensions_gl(context)) return 0;